From be6a5beea58a031dd0897c2dc87676285d5bd83a Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 22:22:08 +0000 Subject: [PATCH 01/11] refactor(macros): rename traits/ to account/ in rentfree module Rename the traits/ directory to account/ to better reflect that these macros are for account data structs (like UserRecord), not generic traits. - src/rentfree/traits/ -> src/rentfree/account/ - docs/traits/ -> docs/account/ - Update all import paths and documentation references --- sdk-libs/macros/CLAUDE.md | 2 +- sdk-libs/macros/docs/CLAUDE.md | 30 +++++++++---------- .../docs/{traits => account}/compress_as.md | 0 .../docs/{traits => account}/compressible.md | 0 .../{traits => account}/compressible_pack.md | 0 .../has_compression_info.md | 0 .../{traits => account}/light_compressible.md | 0 sdk-libs/macros/src/lib.rs | 10 +++---- .../{traits => account}/decompress_context.rs | 0 .../{traits => account}/light_compressible.rs | 2 +- .../src/rentfree/{traits => account}/mod.rs | 0 .../{traits => account}/pack_unpack.rs | 0 .../{traits => account}/seed_extraction.rs | 0 .../rentfree/{traits => account}/traits.rs | 0 .../src/rentfree/{traits => account}/utils.rs | 0 .../macros/src/rentfree/accounts/parse.rs | 2 +- sdk-libs/macros/src/rentfree/mod.rs | 4 +-- .../macros/src/rentfree/program/decompress.rs | 2 +- .../src/rentfree/program/instructions.rs | 6 ++-- .../macros/src/rentfree/program/parsing.rs | 6 ++-- .../macros/src/rentfree/program/visitors.rs | 2 +- 21 files changed, 33 insertions(+), 33 deletions(-) rename sdk-libs/macros/docs/{traits => account}/compress_as.md (100%) rename sdk-libs/macros/docs/{traits => account}/compressible.md (100%) rename sdk-libs/macros/docs/{traits => account}/compressible_pack.md (100%) rename sdk-libs/macros/docs/{traits => account}/has_compression_info.md (100%) rename sdk-libs/macros/docs/{traits => account}/light_compressible.md (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/decompress_context.rs (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/light_compressible.rs (98%) rename sdk-libs/macros/src/rentfree/{traits => account}/mod.rs (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/pack_unpack.rs (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/seed_extraction.rs (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/traits.rs (100%) rename sdk-libs/macros/src/rentfree/{traits => account}/utils.rs (100%) diff --git a/sdk-libs/macros/CLAUDE.md b/sdk-libs/macros/CLAUDE.md index dfb6f9724c..f8f229dd1d 100644 --- a/sdk-libs/macros/CLAUDE.md +++ b/sdk-libs/macros/CLAUDE.md @@ -33,9 +33,9 @@ Detailed macro documentation is in the `docs/` directory: src/ ├── lib.rs # Macro entry points ├── rentfree/ # RentFree macro system +│ ├── account/ # Trait derive macros for account data structs │ ├── accounts/ # #[derive(RentFree)] for Accounts structs │ ├── program/ # #[rentfree_program] attribute macro -│ ├── traits/ # Trait derive macros │ └── shared_utils.rs # Common utilities └── hasher/ # LightHasherSha derive macro ``` diff --git a/sdk-libs/macros/docs/CLAUDE.md b/sdk-libs/macros/docs/CLAUDE.md index 5d68b4814e..b31ef3ca7e 100644 --- a/sdk-libs/macros/docs/CLAUDE.md +++ b/sdk-libs/macros/docs/CLAUDE.md @@ -14,23 +14,23 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros | **`rentfree_program/`** | `#[rentfree_program]` attribute macro | | **`rentfree_program/architecture.md`** | Architecture overview, usage, generated items | | **`rentfree_program/codegen.md`** | Technical implementation details (code generation) | -| **`traits/`** | Trait derive macros for compressible data structs | +| **`account/`** | Trait derive macros for account data structs | -### Traits Documentation +### Account Trait Documentation | File | Macro | Description | |------|-------|-------------| -| **`traits/has_compression_info.md`** | `#[derive(HasCompressionInfo)]` | Accessor methods for compression_info field | -| **`traits/compress_as.md`** | `#[derive(CompressAs)]` | Creates compressed representation for hashing | -| **`traits/compressible.md`** | `#[derive(Compressible)]` | Combined: HasCompressionInfo + CompressAs + Size | -| **`traits/compressible_pack.md`** | `#[derive(CompressiblePack)]` | Pack/Unpack with Pubkey-to-index compression | -| **`traits/light_compressible.md`** | `#[derive(LightCompressible)]` | All traits for rent-free accounts | +| **`account/has_compression_info.md`** | `#[derive(HasCompressionInfo)]` | Accessor methods for compression_info field | +| **`account/compress_as.md`** | `#[derive(CompressAs)]` | Creates compressed representation for hashing | +| **`account/compressible.md`** | `#[derive(Compressible)]` | Combined: HasCompressionInfo + CompressAs + Size | +| **`account/compressible_pack.md`** | `#[derive(CompressiblePack)]` | Pack/Unpack with Pubkey-to-index compression | +| **`account/light_compressible.md`** | `#[derive(LightCompressible)]` | All traits for rent-free accounts | ## Navigation Tips ### Starting Points -- **Data struct traits**: Start with `traits/light_compressible.md` for the all-in-one derive macro for compressible data structs +- **Data struct traits**: Start with `account/light_compressible.md` for the all-in-one derive macro for compressible data structs - **Building account structs**: Use `rentfree.md` for the accounts-level derive macro that marks fields for compression - **Program-level integration**: Use `rentfree_program/architecture.md` for program-level auto-discovery and instruction generation - **Implementation details**: Use `rentfree_program/codegen.md` for technical code generation details @@ -52,21 +52,21 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros | +-- Generates LightPreInit + LightFinalize impls | - +-- Uses trait derives (traits/): - - HasCompressionInfo <- traits/has_compression_info.md - - CompressAs <- traits/compress_as.md - - Compressible <- traits/compressible.md - - CompressiblePack <- traits/compressible_pack.md - - LightCompressible <- traits/light_compressible.md (combines all) + +-- Uses trait derives (account/): + - HasCompressionInfo <- account/has_compression_info.md + - CompressAs <- account/compress_as.md + - Compressible <- account/compressible.md + - CompressiblePack <- account/compressible_pack.md + - LightCompressible <- account/light_compressible.md (combines all) ``` ## Related Source Code ``` sdk-libs/macros/src/rentfree/ +├── account/ # Trait derive macros for account data structs ├── accounts/ # #[derive(RentFree)] implementation ├── program/ # #[rentfree_program] implementation -├── traits/ # Trait derive macros ├── shared_utils.rs # Common utilities └── mod.rs # Module exports ``` diff --git a/sdk-libs/macros/docs/traits/compress_as.md b/sdk-libs/macros/docs/account/compress_as.md similarity index 100% rename from sdk-libs/macros/docs/traits/compress_as.md rename to sdk-libs/macros/docs/account/compress_as.md diff --git a/sdk-libs/macros/docs/traits/compressible.md b/sdk-libs/macros/docs/account/compressible.md similarity index 100% rename from sdk-libs/macros/docs/traits/compressible.md rename to sdk-libs/macros/docs/account/compressible.md diff --git a/sdk-libs/macros/docs/traits/compressible_pack.md b/sdk-libs/macros/docs/account/compressible_pack.md similarity index 100% rename from sdk-libs/macros/docs/traits/compressible_pack.md rename to sdk-libs/macros/docs/account/compressible_pack.md diff --git a/sdk-libs/macros/docs/traits/has_compression_info.md b/sdk-libs/macros/docs/account/has_compression_info.md similarity index 100% rename from sdk-libs/macros/docs/traits/has_compression_info.md rename to sdk-libs/macros/docs/account/has_compression_info.md diff --git a/sdk-libs/macros/docs/traits/light_compressible.md b/sdk-libs/macros/docs/account/light_compressible.md similarity index 100% rename from sdk-libs/macros/docs/traits/light_compressible.md rename to sdk-libs/macros/docs/account/light_compressible.md diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 369ae0dd42..d7fa31bfd3 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -124,7 +124,7 @@ pub fn light_hasher_sha(input: TokenStream) -> TokenStream { #[proc_macro_derive(HasCompressionInfo)] pub fn has_compression_info(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - into_token_stream(rentfree::traits::traits::derive_has_compression_info(input)) + into_token_stream(rentfree::account::traits::derive_has_compression_info(input)) } /// Legacy CompressAs trait implementation (use Compressible instead). @@ -164,7 +164,7 @@ pub fn has_compression_info(input: TokenStream) -> TokenStream { #[proc_macro_derive(CompressAs, attributes(compress_as))] pub fn compress_as_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - into_token_stream(rentfree::traits::traits::derive_compress_as(input)) + into_token_stream(rentfree::account::traits::derive_compress_as(input)) } /// Auto-discovering rent-free program macro that reads external module files. @@ -257,7 +257,7 @@ pub fn account(_: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_derive(Compressible, attributes(compress_as, light_seeds))] pub fn compressible_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(rentfree::traits::traits::derive_compressible(input)) + into_token_stream(rentfree::account::traits::derive_compressible(input)) } /// Automatically implements Pack and Unpack traits for compressible accounts. @@ -284,7 +284,7 @@ pub fn compressible_derive(input: TokenStream) -> TokenStream { #[proc_macro_derive(CompressiblePack)] pub fn compressible_pack(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(rentfree::traits::pack_unpack::derive_compressible_pack( + into_token_stream(rentfree::account::pack_unpack::derive_compressible_pack( input, )) } @@ -334,7 +334,7 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { #[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(rentfree::traits::light_compressible::derive_rentfree_account(input)) + into_token_stream(rentfree::account::light_compressible::derive_rentfree_account(input)) } /// Derives a Rent Sponsor PDA for a program at compile time. diff --git a/sdk-libs/macros/src/rentfree/traits/decompress_context.rs b/sdk-libs/macros/src/rentfree/account/decompress_context.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/decompress_context.rs rename to sdk-libs/macros/src/rentfree/account/decompress_context.rs diff --git a/sdk-libs/macros/src/rentfree/traits/light_compressible.rs b/sdk-libs/macros/src/rentfree/account/light_compressible.rs similarity index 98% rename from sdk-libs/macros/src/rentfree/traits/light_compressible.rs rename to sdk-libs/macros/src/rentfree/account/light_compressible.rs index bc0863f072..ae2044b5a9 100644 --- a/sdk-libs/macros/src/rentfree/traits/light_compressible.rs +++ b/sdk-libs/macros/src/rentfree/account/light_compressible.rs @@ -13,7 +13,7 @@ use syn::{DeriveInput, Fields, ItemStruct, Result}; use crate::{ discriminator::discriminator, hasher::derive_light_hasher_sha, - rentfree::traits::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, + rentfree::account::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, }; /// Derives all required traits for a compressible account. diff --git a/sdk-libs/macros/src/rentfree/traits/mod.rs b/sdk-libs/macros/src/rentfree/account/mod.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/mod.rs rename to sdk-libs/macros/src/rentfree/account/mod.rs diff --git a/sdk-libs/macros/src/rentfree/traits/pack_unpack.rs b/sdk-libs/macros/src/rentfree/account/pack_unpack.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/pack_unpack.rs rename to sdk-libs/macros/src/rentfree/account/pack_unpack.rs diff --git a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/seed_extraction.rs rename to sdk-libs/macros/src/rentfree/account/seed_extraction.rs diff --git a/sdk-libs/macros/src/rentfree/traits/traits.rs b/sdk-libs/macros/src/rentfree/account/traits.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/traits.rs rename to sdk-libs/macros/src/rentfree/account/traits.rs diff --git a/sdk-libs/macros/src/rentfree/traits/utils.rs b/sdk-libs/macros/src/rentfree/account/utils.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/utils.rs rename to sdk-libs/macros/src/rentfree/account/utils.rs diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index fcf1932847..66e5b6afce 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -11,7 +11,7 @@ use syn::{ use super::light_mint::{parse_light_mint_attr, LightMintField}; use crate::rentfree::shared_utils::MetaExpr; // Import shared types -pub(super) use crate::rentfree::traits::seed_extraction::extract_account_inner_type; +pub(super) use crate::rentfree::account::seed_extraction::extract_account_inner_type; // ============================================================================ // Infrastructure Field Classification diff --git a/sdk-libs/macros/src/rentfree/mod.rs b/sdk-libs/macros/src/rentfree/mod.rs index e10bea0ab6..05b0f9b73e 100644 --- a/sdk-libs/macros/src/rentfree/mod.rs +++ b/sdk-libs/macros/src/rentfree/mod.rs @@ -3,10 +3,10 @@ //! This module organizes all rent-free related macros: //! - `program/` - `#[rentfree_program]` attribute macro for program-level auto-discovery //! - `accounts/` - `#[derive(RentFree)]` derive macro for Accounts structs -//! - `traits/` - Shared trait derive macros (Compressible, Pack, HasCompressionInfo, etc.) +//! - `account/` - Trait derive macros for account data structs (Compressible, Pack, HasCompressionInfo, etc.) //! - `shared_utils` - Common utilities (constant detection, identifier extraction) +pub mod account; pub mod accounts; pub mod program; pub mod shared_utils; -pub mod traits; diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index f9f129820d..d19c7552dd 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -23,7 +23,7 @@ pub fn generate_decompress_context_impl( let lifetime: syn::Lifetime = syn::parse_quote!('info); let trait_impl = - crate::rentfree::traits::decompress_context::generate_decompress_context_trait_impl( + crate::rentfree::account::decompress_context::generate_decompress_context_trait_impl( pda_ctx_seeds, token_variant_ident, lifetime, diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 87af7c562b..042d1098bb 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -59,10 +59,10 @@ fn codegen( if !token_seed_specs.is_empty() { super::variant_enum::generate_ctoken_account_variant_enum(token_seed_specs)? } else { - crate::rentfree::traits::utils::generate_empty_ctoken_enum() + crate::rentfree::account::utils::generate_empty_ctoken_enum() } } else { - crate::rentfree::traits::utils::generate_empty_ctoken_enum() + crate::rentfree::account::utils::generate_empty_ctoken_enum() }; if let Some(ref token_seed_specs) = token_seeds { @@ -405,7 +405,7 @@ fn codegen( #[inline(never)] pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { use super::crate_context::CrateContext; - use crate::rentfree::traits::seed_extraction::{ + use crate::rentfree::account::seed_extraction::{ extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec, }; diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index c5cf063aa8..59b73d2a06 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -283,9 +283,9 @@ pub fn extract_data_seed_fields( /// Convert ClassifiedSeed to SeedElement (Punctuated) pub fn convert_classified_to_seed_elements( - seeds: &[crate::rentfree::traits::seed_extraction::ClassifiedSeed], + seeds: &[crate::rentfree::account::seed_extraction::ClassifiedSeed], ) -> Punctuated { - use crate::rentfree::traits::seed_extraction::ClassifiedSeed; + use crate::rentfree::account::seed_extraction::ClassifiedSeed; let mut result = Punctuated::new(); for seed in seeds { @@ -338,7 +338,7 @@ pub fn convert_classified_to_seed_elements( } pub fn convert_classified_to_seed_elements_vec( - seeds: &[crate::rentfree::traits::seed_extraction::ClassifiedSeed], + seeds: &[crate::rentfree::account::seed_extraction::ClassifiedSeed], ) -> Vec { convert_classified_to_seed_elements(seeds) .into_iter() diff --git a/sdk-libs/macros/src/rentfree/program/visitors.rs b/sdk-libs/macros/src/rentfree/program/visitors.rs index 1b4528d27a..f27a46a0d2 100644 --- a/sdk-libs/macros/src/rentfree/program/visitors.rs +++ b/sdk-libs/macros/src/rentfree/program/visitors.rs @@ -19,7 +19,7 @@ use syn::{ }; use super::instructions::{InstructionDataSpec, SeedElement}; -use crate::rentfree::{shared_utils::is_constant_identifier, traits::utils::is_pubkey_type}; +use crate::rentfree::{account::utils::is_pubkey_type, shared_utils::is_constant_identifier}; /// Visitor that extracts field names matching ctx.field, ctx.accounts.field, or data.field patterns. /// From a13c83df7f8c516bdd30e5dea814190965c5c068 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 23:19:53 +0000 Subject: [PATCH 02/11] stash --- .../src/rentfree/program/crate_context.rs | 28 +++ .../macros/src/rentfree/program/decompress.rs | 39 +++- .../src/rentfree/program/expr_traversal.rs | 42 +++- .../src/rentfree/program/instructions.rs | 28 ++- .../src/rentfree/program/variant_enum.rs | 10 +- .../src/instructions/d5_markers/all.rs | 68 ++++++ .../src/instructions/d5_markers/mod.rs | 7 +- .../instructions/d5_markers/rentfree_token.rs | 51 +++++ .../instructions/d6_account_types/account.rs | 38 +++ .../src/instructions/d6_account_types/all.rs | 51 +++++ .../instructions/d6_account_types/boxed.rs | 39 ++++ .../src/instructions/d6_account_types/mod.rs | 13 ++ .../src/instructions/d7_infra_names/all.rs | 68 ++++++ .../instructions/d7_infra_names/creator.rs | 38 +++ .../d7_infra_names/ctoken_config.rs | 49 ++++ .../src/instructions/d7_infra_names/mod.rs | 16 ++ .../src/instructions/d7_infra_names/payer.rs | 38 +++ .../src/instructions/d8_builder_paths/all.rs | 49 ++++ .../src/instructions/d8_builder_paths/mod.rs | 14 ++ .../d8_builder_paths/multi_rentfree.rs | 50 ++++ .../instructions/d8_builder_paths/pda_only.rs | 39 ++++ .../src/instructions/d9_seeds/all.rs | 108 +++++++++ .../src/instructions/d9_seeds/constant.rs | 39 ++++ .../src/instructions/d9_seeds/ctx_account.rs | 40 ++++ .../instructions/d9_seeds/function_call.rs | 39 ++++ .../src/instructions/d9_seeds/literal.rs | 37 +++ .../src/instructions/d9_seeds/mixed.rs | 41 ++++ .../src/instructions/d9_seeds/mod.rs | 27 +++ .../src/instructions/d9_seeds/param.rs | 38 +++ .../src/instructions/d9_seeds/param_bytes.rs | 38 +++ .../src/instructions/mod.rs | 4 + .../csdk-anchor-full-derived-test/src/lib.rs | 5 + .../tests/amm_test.rs | 216 ++++++++++++++++-- 33 files changed, 1371 insertions(+), 36 deletions(-) create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs diff --git a/sdk-libs/macros/src/rentfree/program/crate_context.rs b/sdk-libs/macros/src/rentfree/program/crate_context.rs index db9191f28d..a46d18e237 100644 --- a/sdk-libs/macros/src/rentfree/program/crate_context.rs +++ b/sdk-libs/macros/src/rentfree/program/crate_context.rs @@ -70,6 +70,34 @@ impl CrateContext { pub fn module(&self, path: &str) -> Option<&ParsedModule> { self.modules.get(path) } + + /// Get the field names of a struct by its type. + /// + /// The type can be a simple identifier (e.g., "SinglePubkeyRecord") or + /// a qualified path. Returns None if the struct is not found. + pub fn get_struct_fields(&self, type_name: &syn::Type) -> Option> { + // Extract the struct name from the type path + let struct_name = match type_name { + syn::Type::Path(type_path) => type_path.path.segments.last()?.ident.to_string(), + _ => return None, + }; + + // Find the struct by name + for item_struct in self.structs() { + if item_struct.ident == struct_name { + // Extract field names from the struct + if let syn::Fields::Named(named_fields) = &item_struct.fields { + let field_names: Vec = named_fields + .named + .iter() + .filter_map(|f| f.ident.as_ref().map(|i| i.to_string())) + .collect(); + return Some(field_names); + } + } + } + None + } } /// A parsed module containing its items. diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index d19c7552dd..e58c1bd724 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -156,10 +156,12 @@ pub fn generate_decompress_accounts_struct(variant: InstructionVariant) -> Resul /// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. /// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) +/// Only maps data.field -> self.field if the field exists on the state struct. #[inline(never)] fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( spec: &TokenSeedSpec, ctx_seed_fields: &[syn::Ident], + state_field_names: &std::collections::HashSet, ) -> Result { let mut bindings: Vec = Vec::new(); let mut seed_refs = Vec::new(); @@ -196,9 +198,19 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } } + // Check if this is a data.field expression where the field doesn't exist on state + // If so, skip it (the seed cannot be derived from state during decompression) + if is_params_only_seed(expr, state_field_names) { + // Skip params-only seeds - they cannot be derived during decompression + // The PDA address is stored in compression_info.address, so we don't need + // to re-derive it. However, we still need all seeds for the PDA verification. + // For now, we leave a placeholder that will need to be handled differently. + continue; + } + let binding_name = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = transform_expr_for_ctx_seeds(expr, &ctx_field_names); + let mapped_expr = transform_expr_for_ctx_seeds(expr, &ctx_field_names, state_field_names); bindings.push(quote! { let #binding_name = #mapped_expr; }); @@ -222,6 +234,29 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( }) } +/// Check if a seed expression is a params-only seed (data.field where field doesn't exist on state) +fn is_params_only_seed(expr: &syn::Expr, state_field_names: &std::collections::HashSet) -> bool { + use crate::rentfree::shared_utils::is_base_path; + + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + if is_base_path(&field_expr.base, "data") { + return !state_field_names.contains(&field_name.to_string()); + } + } + false + } + syn::Expr::MethodCall(method_call) => { + is_params_only_seed(&method_call.receiver, state_field_names) + } + syn::Expr::Reference(ref_expr) => { + is_params_only_seed(&ref_expr.expr, state_field_names) + } + _ => false, + } +} + // ============================================================================= // PDA SEED PROVIDER IMPLS // ============================================================================= @@ -286,7 +321,7 @@ pub fn generate_pda_seed_provider_impls( }; let seed_derivation = - generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, ctx_fields)?; + generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, ctx_fields, &ctx_info.state_field_names)?; // Generate impl for inner_type, but use variant-based struct name results.push(quote! { diff --git a/sdk-libs/macros/src/rentfree/program/expr_traversal.rs b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs index 59866d94b0..99ee69b6a3 100644 --- a/sdk-libs/macros/src/rentfree/program/expr_traversal.rs +++ b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs @@ -16,10 +16,26 @@ use crate::rentfree::shared_utils::is_base_path; /// Transform expressions by replacing field access patterns. /// /// Used for converting: -/// - `data.field` -> `self.field` +/// - `data.field` -> `self.field` (only if field exists in state_field_names) /// - `ctx.field` -> `ctx_seeds.field` (if field is in ctx_field_names) /// - `ctx.accounts.field` -> `ctx_seeds.field` -pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet) -> Expr { +/// +/// For `data.field` where field is NOT in state_field_names, the expression +/// is left unchanged (which will cause a compile error, alerting the user +/// that this field is not supported for decompression seed derivation). +pub fn transform_expr_for_ctx_seeds( + expr: &Expr, + ctx_field_names: &HashSet, + state_field_names: &HashSet, +) -> Expr { + transform_expr_internal(expr, ctx_field_names, state_field_names) +} + +fn transform_expr_internal( + expr: &Expr, + ctx_field_names: &HashSet, + state_field_names: &HashSet, +) -> Expr { match expr { Expr::Field(field_expr) => { let Some(syn::Member::Named(field_name)) = Some(&field_expr.member) else { @@ -35,10 +51,18 @@ pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet self.field or ctx.field -> ctx_seeds.field + // Check for data.field -> self.field (only if field exists on state struct) if is_base_path(&field_expr.base, "data") { - return syn::parse_quote! { self.#field_name }; + let field_str = field_name.to_string(); + if state_field_names.contains(&field_str) { + return syn::parse_quote! { self.#field_name }; + } + // Field not on state struct - leave unchanged (will cause compile error + // unless handled elsewhere). This handles params-only seeds. + return expr.clone(); } + + // Check for ctx.field -> ctx_seeds.field if is_base_path(&field_expr.base, "ctx") && ctx_field_names.contains(&field_name.to_string()) { @@ -49,14 +73,15 @@ pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet { let mut new_call = method_call.clone(); - new_call.receiver = Box::new(transform_expr_for_ctx_seeds( + new_call.receiver = Box::new(transform_expr_internal( &method_call.receiver, ctx_field_names, + state_field_names, )); new_call.args = method_call .args .iter() - .map(|a| transform_expr_for_ctx_seeds(a, ctx_field_names)) + .map(|a| transform_expr_internal(a, ctx_field_names, state_field_names)) .collect(); Expr::MethodCall(new_call) } @@ -65,15 +90,16 @@ pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet { let mut new_ref = ref_expr.clone(); - new_ref.expr = Box::new(transform_expr_for_ctx_seeds( + new_ref.expr = Box::new(transform_expr_internal( &ref_expr.expr, ctx_field_names, + state_field_names, )); Expr::Reference(new_ref) } diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 042d1098bb..4e612361b1 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -43,6 +43,7 @@ fn codegen( pda_seeds: Option>, token_seeds: Option>, instruction_data: Vec, + crate_ctx: &super::crate_context::CrateContext, ) -> Result { let size_validation_checks = validate_compressed_account_sizes(&account_types)?; @@ -89,7 +90,19 @@ fn codegen( .inner_type .clone() .unwrap_or_else(|| ident_to_type(&spec.variant)); - PdaCtxSeedInfo::new(spec.variant.clone(), inner_type, ctx_fields) + + // Look up the state struct's field names from CrateContext + let state_field_names: std::collections::HashSet = crate_ctx + .get_struct_fields(&inner_type) + .map(|fields| fields.into_iter().collect()) + .unwrap_or_default(); + + PdaCtxSeedInfo::with_state_fields( + spec.variant.clone(), + inner_type, + ctx_fields, + state_field_names, + ) }) .collect() }) @@ -132,12 +145,18 @@ fn codegen( quote! { pub #field: #ty } }) }).collect(); - let data_verifications: Vec<_> = data_fields.iter().map(|field| { - quote! { + // Only generate verifications for data fields that exist on the state struct + let data_verifications: Vec<_> = data_fields.iter().filter_map(|field| { + let field_str = field.to_string(); + // Skip fields that don't exist on the state struct (e.g., params-only seeds) + if !ctx_info.state_field_names.contains(&field_str) { + return None; + } + Some(quote! { if data.#field != seeds.#field { return std::result::Result::Err(RentFreeInstructionError::SeedMismatch.into()); } - } + }) }).collect(); quote! { #[derive(Clone, Debug)] @@ -540,5 +559,6 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< pda_seeds, token_seeds, found_data_fields, + &crate_ctx, ) } diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index d0d5659d15..f685e34695 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -16,14 +16,22 @@ pub struct PdaCtxSeedInfo { pub inner_type: Type, /// Field names from ctx.accounts.XXX references in seeds pub ctx_seed_fields: Vec, + /// Field names that exist on the state struct (for filtering data.* seeds) + pub state_field_names: std::collections::HashSet, } impl PdaCtxSeedInfo { - pub fn new(variant_name: Ident, inner_type: Type, ctx_seed_fields: Vec) -> Self { + pub fn with_state_fields( + variant_name: Ident, + inner_type: Type, + ctx_seed_fields: Vec, + state_field_names: std::collections::HashSet, + ) -> Self { Self { variant_name, inner_type, ctx_seed_fields, + state_field_names, } } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs new file mode 100644 index 0000000000..6c76bb2e1c --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs @@ -0,0 +1,68 @@ +//! D5 Test: All marker types combined +//! +//! Tests #[rentfree] + #[rentfree_token] together in one instruction struct. +//! Note: #[light_mint] is tested separately in amm_test/initialize.rs. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; +use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +pub const D5_ALL_AUTH_SEED: &[u8] = b"d5_all_auth"; +pub const D5_ALL_VAULT_SEED: &[u8] = b"d5_all_vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D5AllMarkersParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests all marker types in one struct: +/// - #[rentfree] for PDA account +/// - #[rentfree_token] for token vault +#[derive(Accounts, RentFree)] +#[instruction(params: D5AllMarkersParams)] +pub struct D5AllMarkers<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + seeds = [D5_ALL_AUTH_SEED], + bump, + )] + pub d5_all_authority: UncheckedAccount<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d5_all_record", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d5_all_record: Account<'info, SinglePubkeyRecord>, + + #[account( + mut, + seeds = [D5_ALL_VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [D5_ALL_AUTH_SEED])] + pub d5_all_vault: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub ctoken_compressible_config: AccountInfo<'info>, + + #[account(mut, address = CTOKEN_RENT_SPONSOR)] + pub ctoken_rent_sponsor: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs index ffb4619e50..3229d39f5e 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs @@ -2,6 +2,11 @@ //! //! Tests #[rentfree], #[rentfree_token], and #[light_mint] attribute parsing. +mod all; mod rentfree_bare; -// Note rent free custom rightfully is a failing test case not added here. +mod rentfree_token; +// Note: rentfree_custom is a failing test case due to pre-existing AddressTreeInfo bug. + +pub use all::*; pub use rentfree_bare::*; +pub use rentfree_token::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs new file mode 100644 index 0000000000..3519da15b2 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs @@ -0,0 +1,51 @@ +//! D5 Test: #[rentfree_token] attribute with authority seeds +//! +//! Tests that the #[rentfree_token(authority = [...])] attribute works correctly +//! for token accounts that need custom authority derivation. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; +use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + +pub const D5_VAULT_AUTH_SEED: &[u8] = b"d5_vault_auth"; +pub const D5_VAULT_SEED: &[u8] = b"d5_vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D5RentfreeTokenParams { + pub create_accounts_proof: CreateAccountsProof, + pub vault_bump: u8, +} + +/// Tests #[rentfree_token(authority = [...])] attribute compilation. +#[derive(Accounts, RentFree)] +#[instruction(params: D5RentfreeTokenParams)] +pub struct D5RentfreeToken<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + #[account( + seeds = [D5_VAULT_AUTH_SEED], + bump, + )] + pub vault_authority: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [D5_VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [D5_VAULT_AUTH_SEED])] + pub d5_token_vault: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub ctoken_compressible_config: AccountInfo<'info>, + + #[account(mut, address = CTOKEN_RENT_SPONSOR)] + pub ctoken_rent_sponsor: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs new file mode 100644 index 0000000000..ff5d00a3d0 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/account.rs @@ -0,0 +1,38 @@ +//! D6 Test: Direct Account<'info, T> type +//! +//! Tests that #[rentfree] works with Account<'info, T> directly (not boxed). + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D6AccountParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests #[rentfree] with direct Account<'info, T> type. +#[derive(Accounts, RentFree)] +#[instruction(params: D6AccountParams)] +pub struct D6Account<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d6_account", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d6_account_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs new file mode 100644 index 0000000000..2386d9bd7a --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs @@ -0,0 +1,51 @@ +//! D6 Test: Both Account<'info, T> and Box> together +//! +//! Tests that both account type variants work in the same struct. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; +use crate::state::d2_compress_as::multiple::MultipleCompressAsRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D6AllParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests both account types in one struct: +/// - Account<'info, T> (direct) +/// - Box> (boxed) +#[derive(Accounts, RentFree)] +#[instruction(params: D6AllParams)] +pub struct D6All<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d6_all_direct", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d6_all_direct: Account<'info, SinglePubkeyRecord>, + + #[account( + init, + payer = fee_payer, + space = 8 + MultipleCompressAsRecord::INIT_SPACE, + seeds = [b"d6_all_boxed", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d6_all_boxed: Box>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs new file mode 100644 index 0000000000..90e8f6ba2a --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/boxed.rs @@ -0,0 +1,39 @@ +//! D6 Test: Box> type +//! +//! Tests that #[rentfree] works with Box> (boxed account). +//! This exercises the Box unwrap path in seed_extraction.rs with is_boxed = true. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D6BoxedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests #[rentfree] with Box> type. +#[derive(Accounts, RentFree)] +#[instruction(params: D6BoxedParams)] +pub struct D6Boxed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d6_boxed", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d6_boxed_record: Box>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/mod.rs new file mode 100644 index 0000000000..3d0f13e0b2 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/mod.rs @@ -0,0 +1,13 @@ +//! D6: Account type extraction +//! +//! Tests macro handling of different account wrapper types: +//! - Account<'info, T> - direct extraction +//! - Box> - Box unwrap with is_boxed = true + +mod account; +mod all; +mod boxed; + +pub use account::*; +pub use all::*; +pub use boxed::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs new file mode 100644 index 0000000000..dfdf8b3666 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs @@ -0,0 +1,68 @@ +//! D7 Test: Multiple naming variants combined +//! +//! Tests that different naming conventions work together in one struct. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; +use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +pub const D7_ALL_AUTH_SEED: &[u8] = b"d7_all_auth"; +pub const D7_ALL_VAULT_SEED: &[u8] = b"d7_all_vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D7AllNamesParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests multiple naming variants: +/// - `payer` as the fee payer field +/// - `ctoken_compressible_config` for config +/// - `ctoken_rent_sponsor` for rent sponsor +#[derive(Accounts, RentFree)] +#[instruction(params: D7AllNamesParams)] +pub struct D7AllNames<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + seeds = [D7_ALL_AUTH_SEED], + bump, + )] + pub d7_all_authority: UncheckedAccount<'info>, + + #[account( + init, + payer = payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d7_all_record", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d7_all_record: Account<'info, SinglePubkeyRecord>, + + #[account( + mut, + seeds = [D7_ALL_VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [D7_ALL_AUTH_SEED])] + pub d7_all_vault: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub ctoken_compressible_config: AccountInfo<'info>, + + #[account(mut, address = CTOKEN_RENT_SPONSOR)] + pub ctoken_rent_sponsor: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs new file mode 100644 index 0000000000..3dbfb1492b --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/creator.rs @@ -0,0 +1,38 @@ +//! D7 Test: "creator" field name variant +//! +//! Tests that #[rentfree] works when the payer field is named `creator` instead of `fee_payer`. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D7CreatorParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests #[rentfree] with `creator` field name (InfraFieldClassifier FeePayer variant). +#[derive(Accounts, RentFree)] +#[instruction(params: D7CreatorParams)] +pub struct D7Creator<'info> { + #[account(mut)] + pub creator: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = creator, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d7_creator", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d7_creator_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs new file mode 100644 index 0000000000..14fff8b279 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs @@ -0,0 +1,49 @@ +//! D7 Test: "ctoken_config" naming variant +//! +//! Tests that #[rentfree_token] works with alternative naming for ctoken infrastructure fields. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; +use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + +pub const D7_CTOKEN_AUTH_SEED: &[u8] = b"d7_ctoken_auth"; +pub const D7_CTOKEN_VAULT_SEED: &[u8] = b"d7_ctoken_vault"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D7CtokenConfigParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests #[rentfree_token] with `ctoken_compressible_config` and `ctoken_rent_sponsor` field names. +#[derive(Accounts, RentFree)] +#[instruction(params: D7CtokenConfigParams)] +pub struct D7CtokenConfig<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Token mint + pub mint: AccountInfo<'info>, + + #[account( + seeds = [D7_CTOKEN_AUTH_SEED], + bump, + )] + pub d7_ctoken_authority: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [D7_CTOKEN_VAULT_SEED, mint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [D7_CTOKEN_AUTH_SEED])] + pub d7_ctoken_vault: UncheckedAccount<'info>, + + #[account(address = COMPRESSIBLE_CONFIG_V1)] + pub ctoken_compressible_config: AccountInfo<'info>, + + #[account(mut, address = CTOKEN_RENT_SPONSOR)] + pub ctoken_rent_sponsor: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/mod.rs new file mode 100644 index 0000000000..bc8e7d2259 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/mod.rs @@ -0,0 +1,16 @@ +//! D7: Infrastructure field naming +//! +//! Tests macro handling of different field naming conventions: +//! - payer instead of fee_payer +//! - creator instead of fee_payer +//! - ctoken_config variants + +mod all; +mod creator; +mod ctoken_config; +mod payer; + +pub use all::*; +pub use creator::*; +pub use ctoken_config::*; +pub use payer::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs new file mode 100644 index 0000000000..721fd489a7 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/payer.rs @@ -0,0 +1,38 @@ +//! D7 Test: "payer" field name variant +//! +//! Tests that #[rentfree] works when the payer field is named `payer` instead of `fee_payer`. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D7PayerParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests #[rentfree] with `payer` field name (InfraFieldClassifier FeePayer variant). +#[derive(Accounts, RentFree)] +#[instruction(params: D7PayerParams)] +pub struct D7Payer<'info> { + #[account(mut)] + pub payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d7_payer", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d7_payer_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs new file mode 100644 index 0000000000..a217b49807 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs @@ -0,0 +1,49 @@ +//! D8 Test: Multiple #[rentfree] fields with different state types +//! +//! Tests the builder path with multiple #[rentfree] fields of different state types. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; +use crate::state::d2_compress_as::multiple::MultipleCompressAsRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D8AllParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests builder path with multiple #[rentfree] fields of different state types. +#[derive(Accounts, RentFree)] +#[instruction(params: D8AllParams)] +pub struct D8All<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d8_all_single", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d8_all_single: Account<'info, SinglePubkeyRecord>, + + #[account( + init, + payer = fee_payer, + space = 8 + MultipleCompressAsRecord::INIT_SPACE, + seeds = [b"d8_all_multi", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d8_all_multi: Box>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/mod.rs new file mode 100644 index 0000000000..b9639ba8c4 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/mod.rs @@ -0,0 +1,14 @@ +//! D8: Builder code generation paths +//! +//! Tests different builder code generation scenarios: +//! - pda_only: Only #[rentfree] fields (no tokens) +//! - multi_rentfree: Multiple #[rentfree] fields +//! - all: Multiple #[rentfree] fields with different state types + +mod all; +mod multi_rentfree; +mod pda_only; + +pub use all::*; +pub use multi_rentfree::*; +pub use pda_only::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs new file mode 100644 index 0000000000..bf4760215a --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/multi_rentfree.rs @@ -0,0 +1,50 @@ +//! D8 Test: Multiple #[rentfree] fields +//! +//! Tests the builder path with multiple #[rentfree] PDA accounts of the same type. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D8MultiRentfreeParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id1: u64, + pub id2: u64, +} + +/// Tests builder path with multiple #[rentfree] fields of the same type. +#[derive(Accounts, RentFree)] +#[instruction(params: D8MultiRentfreeParams)] +pub struct D8MultiRentfree<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d8_multi_1", params.owner.as_ref(), params.id1.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub d8_multi_record1: Account<'info, SinglePubkeyRecord>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d8_multi_2", params.owner.as_ref(), params.id2.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub d8_multi_record2: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs new file mode 100644 index 0000000000..1db85edcca --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/pda_only.rs @@ -0,0 +1,39 @@ +//! D8 Test: Only #[rentfree] fields (no token accounts) +//! +//! Tests the `generate_pre_init_pdas_only` code path where only PDA accounts +//! are marked with #[rentfree], without any token accounts. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D8PdaOnlyParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests builder path with only PDA accounts (no token accounts). +#[derive(Accounts, RentFree)] +#[instruction(params: D8PdaOnlyParams)] +pub struct D8PdaOnly<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d8_pda_only", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d8_pda_only_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs new file mode 100644 index 0000000000..f0294d6cda --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/all.rs @@ -0,0 +1,108 @@ +//! D9 Test: All seed expression types +//! +//! Tests all 6 seed types in a single struct: +//! - Literal: b"d9_all" +//! - Constant: D9_ALL_SEED +//! - CtxAccount: authority.key() +//! - DataField (param): params.owner.as_ref() +//! - DataField (bytes): params.id.to_le_bytes() +//! - FunctionCall: max_key(&a, &b) + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +pub const D9_ALL_SEED: &[u8] = b"d9_all_const"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9AllParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, + pub id: u64, + pub key_a: Pubkey, + pub key_b: Pubkey, +} + +/// Tests all 6 seed types in one struct. +#[derive(Accounts, RentFree)] +#[instruction(params: D9AllParams)] +pub struct D9All<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Authority account used in seeds + pub authority: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + // Test 1: Literal only + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_all_lit"], + bump, + )] + #[rentfree] + pub d9_all_lit: Account<'info, SinglePubkeyRecord>, + + // Test 2: Constant + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_ALL_SEED], + bump, + )] + #[rentfree] + pub d9_all_const: Account<'info, SinglePubkeyRecord>, + + // Test 3: CtxAccount + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_all_ctx", authority.key().as_ref()], + bump, + )] + #[rentfree] + pub d9_all_ctx: Account<'info, SinglePubkeyRecord>, + + // Test 4: DataField (param Pubkey) + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_all_param", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d9_all_param: Account<'info, SinglePubkeyRecord>, + + // Test 5: DataField (bytes conversion) + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_all_bytes", params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub d9_all_bytes: Account<'info, SinglePubkeyRecord>, + + // Test 6: FunctionCall + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_all_func", crate::max_key(¶ms.key_a, ¶ms.key_b).as_ref()], + bump, + )] + #[rentfree] + pub d9_all_func: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs new file mode 100644 index 0000000000..6337844b3d --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/constant.rs @@ -0,0 +1,39 @@ +//! D9 Test: Constant seed expression +//! +//! Tests ClassifiedSeed::Constant with constant identifier seeds. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +pub const D9_CONSTANT_SEED: &[u8] = b"d9_constant"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ConstantParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests ClassifiedSeed::Constant with constant identifier seeds. +#[derive(Accounts, RentFree)] +#[instruction(params: D9ConstantParams)] +pub struct D9Constant<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [D9_CONSTANT_SEED], + bump, + )] + #[rentfree] + pub d9_constant_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs new file mode 100644 index 0000000000..d2dd23db87 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/ctx_account.rs @@ -0,0 +1,40 @@ +//! D9 Test: Context account seed expression +//! +//! Tests ClassifiedSeed::CtxAccount with authority.key() seeds. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9CtxAccountParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests ClassifiedSeed::CtxAccount with authority.key() seeds. +#[derive(Accounts, RentFree)] +#[instruction(params: D9CtxAccountParams)] +pub struct D9CtxAccount<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Authority account used in seeds + pub authority: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_ctx", authority.key().as_ref()], + bump, + )] + #[rentfree] + pub d9_ctx_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs new file mode 100644 index 0000000000..af90c7bf77 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/function_call.rs @@ -0,0 +1,39 @@ +//! D9 Test: Function call seed expression +//! +//! Tests ClassifiedSeed::FunctionCall with max_key(&a, &b) seeds. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9FunctionCallParams { + pub create_accounts_proof: CreateAccountsProof, + pub key_a: Pubkey, + pub key_b: Pubkey, +} + +/// Tests ClassifiedSeed::FunctionCall with max_key(&a, &b) seeds. +#[derive(Accounts, RentFree)] +#[instruction(params: D9FunctionCallParams)] +pub struct D9FunctionCall<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_func", crate::max_key(¶ms.key_a, ¶ms.key_b).as_ref()], + bump, + )] + #[rentfree] + pub d9_func_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs new file mode 100644 index 0000000000..26a58095b8 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/literal.rs @@ -0,0 +1,37 @@ +//! D9 Test: Literal seed expression +//! +//! Tests ClassifiedSeed::Literal with byte literal seeds. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9LiteralParams { + pub create_accounts_proof: CreateAccountsProof, +} + +/// Tests ClassifiedSeed::Literal with byte literal seeds. +#[derive(Accounts, RentFree)] +#[instruction(params: D9LiteralParams)] +pub struct D9Literal<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_literal_record"], + bump, + )] + #[rentfree] + pub d9_literal_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs new file mode 100644 index 0000000000..bbc64a0619 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mixed.rs @@ -0,0 +1,41 @@ +//! D9 Test: Mixed seed expression types +//! +//! Tests multiple seed types combined: literal + ctx_account + param. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9MixedParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests multiple seed types combined: literal + ctx_account + param. +#[derive(Accounts, RentFree)] +#[instruction(params: D9MixedParams)] +pub struct D9Mixed<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Authority account used in seeds + pub authority: AccountInfo<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_mixed", authority.key().as_ref(), params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d9_mixed_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs new file mode 100644 index 0000000000..e34cfa99bf --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/mod.rs @@ -0,0 +1,27 @@ +//! D9: Seed expression classification +//! +//! Tests different seed expression types in ClassifiedSeed enum: +//! - Literal: b"record" +//! - Constant: SEED_CONSTANT +//! - CtxAccount: authority.key() +//! - DataField (param): params.owner.as_ref() +//! - DataField (bytes): params.id.to_le_bytes() +//! - FunctionCall: max_key(&a, &b) + +mod all; +mod constant; +mod ctx_account; +mod function_call; +mod literal; +mod mixed; +mod param; +mod param_bytes; + +pub use all::*; +pub use constant::*; +pub use ctx_account::*; +pub use function_call::*; +pub use literal::*; +pub use mixed::*; +pub use param::*; +pub use param_bytes::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs new file mode 100644 index 0000000000..e90715a32a --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param.rs @@ -0,0 +1,38 @@ +//! D9 Test: Param seed expression (Pubkey) +//! +//! Tests ClassifiedSeed::DataField with params.owner.as_ref() seeds. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ParamParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests ClassifiedSeed::DataField with params.owner.as_ref() seeds. +#[derive(Accounts, RentFree)] +#[instruction(params: D9ParamParams)] +pub struct D9Param<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_param", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub d9_param_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs new file mode 100644 index 0000000000..db6996b644 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d9_seeds/param_bytes.rs @@ -0,0 +1,38 @@ +//! D9 Test: Param bytes seed expression +//! +//! Tests ClassifiedSeed::DataField with params.id.to_le_bytes() conversion. + +use anchor_lang::prelude::*; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; + +use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct D9ParamBytesParams { + pub create_accounts_proof: CreateAccountsProof, + pub id: u64, +} + +/// Tests ClassifiedSeed::DataField with params.id.to_le_bytes() conversion. +#[derive(Accounts, RentFree)] +#[instruction(params: D9ParamBytesParams)] +pub struct D9ParamBytes<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + SinglePubkeyRecord::INIT_SPACE, + seeds = [b"d9_param_bytes", params.id.to_le_bytes().as_ref()], + bump, + )] + #[rentfree] + pub d9_param_bytes_record: Account<'info, SinglePubkeyRecord>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs index e5f94831ee..a7e9e9d180 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs @@ -8,3 +8,7 @@ //! - d9_seeds: Seed expression classification pub mod d5_markers; +pub mod d6_account_types; +pub mod d7_infra_names; +pub mod d8_builder_paths; +pub mod d9_seeds; 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 676abac7cb..cc7d6492db 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -15,8 +15,13 @@ pub mod state; pub use amm_test::*; pub use d5_markers::*; pub use instruction_accounts::*; +pub use instructions::d7_infra_names::{ + D7_ALL_AUTH_SEED, D7_ALL_VAULT_SEED, D7_CTOKEN_AUTH_SEED, D7_CTOKEN_VAULT_SEED, +}; +pub use instructions::d9_seeds::{D9_ALL_SEED, D9_CONSTANT_SEED}; pub use state::{ d1_field_types::single_pubkey::{PackedSinglePubkeyRecord, SinglePubkeyRecord}, + d2_compress_as::multiple::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}, GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord, }; #[inline] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs index 013ecb25ef..7c05442542 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -14,9 +14,14 @@ use csdk_anchor_full_derived_test::amm_test::{ InitializeParams, AUTH_SEED, OBSERVATION_SEED, POOL_LP_MINT_SIGNER_SEED, POOL_SEED, POOL_VAULT_SEED, }; +use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + ObservationStateSeeds, PoolStateSeeds, TokenAccountVariant, +}; use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ - get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, + create_load_accounts_instructions, get_create_accounts_proof, AccountInterface, + AccountInterfaceExt, CreateAccountsProofInput, InitializeRentFreeConfig, + RentFreeDecompressAccount, }; use light_macros::pubkey; use light_program_test::{ @@ -601,29 +606,200 @@ async fn test_amm_full_lifecycle() { // ========================================================================== // PHASE 9: Decompress accounts // ========================================================================== - // Note: Decompression requires the seed structs generated by #[rentfree_program] - // macro. We would need to import them like: - // use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ - // PoolStateSeeds, ObservationStateSeeds, TokenAccountVariant - // }; - // - // For now, we verify that compression worked. Full decompression test - // requires the macro-generated types to be available. - - println!("\nDecompression phase would use:"); - println!(" - get_account_info_interface for pool_state and observation_state"); - println!(" - get_token_account_interface for vaults"); - println!(" - get_ata_interface for creator_lp_token"); - println!(" - get_mint_interface for lp_mint"); - println!(" - create_load_accounts_instructions to generate decompression ixs"); - - // TODO: Add full decompression test once seed structs are available - // This would follow the pattern in basic_test.rs Phase 3 + println!("\nPhase 9: Decompressing all accounts..."); + + // Fetch unified interfaces (hot/cold transparent) for PDAs + let pool_interface = ctx + .rpc + .get_account_info_interface(&pdas.pool_state, &ctx.program_id) + .await + .expect("failed to get pool_state"); + assert!(pool_interface.is_cold, "pool_state should be cold"); + + let observation_interface = ctx + .rpc + .get_account_info_interface(&pdas.observation_state, &ctx.program_id) + .await + .expect("failed to get observation_state"); + assert!( + observation_interface.is_cold, + "observation_state should be cold" + ); + + // Fetch token account interfaces for vaults + let vault_0_interface = ctx + .rpc + .get_token_account_interface(&pdas.token_0_vault) + .await + .expect("failed to get token_0_vault"); + assert!(vault_0_interface.is_cold, "token_0_vault should be cold"); + + let vault_1_interface = ctx + .rpc + .get_token_account_interface(&pdas.token_1_vault) + .await + .expect("failed to get token_1_vault"); + assert!(vault_1_interface.is_cold, "token_1_vault should be cold"); + + // Fetch ATA interface for creator LP token + let creator_lp_interface = ctx + .rpc + .get_ata_interface(&ctx.creator.pubkey(), &pdas.lp_mint) + .await + .expect("failed to get creator_lp_token"); + assert!( + creator_lp_interface.is_cold(), + "creator_lp_token should be cold" + ); + assert_eq!( + creator_lp_interface.amount(), + expected_balance_after_withdraw, + "Compressed LP token amount should match" + ); + + // Fetch mint interface for LP mint + let lp_mint_interface = ctx + .rpc + .get_mint_interface(&pdas.lp_mint_signer) + .await + .expect("failed to get lp_mint"); + assert!(lp_mint_interface.is_cold(), "lp_mint should be cold"); + + // Build RentFreeDecompressAccount list for program-owned accounts + let program_owned_accounts = vec![ + RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&pool_interface), + PoolStateSeeds { + amm_config: ctx.amm_config.pubkey(), + token_0_mint: ctx.token_0_mint, + token_1_mint: ctx.token_1_mint, + }, + ) + .expect("PoolState seed verification failed"), + RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&observation_interface), + ObservationStateSeeds { + pool_state: pdas.pool_state, + }, + ) + .expect("ObservationState seed verification failed"), + RentFreeDecompressAccount::from_ctoken( + AccountInterface::from(&vault_0_interface), + TokenAccountVariant::Token0Vault { + pool_state: pdas.pool_state, + token_0_mint: ctx.token_0_mint, + }, + ) + .expect("Token0Vault construction failed"), + RentFreeDecompressAccount::from_ctoken( + AccountInterface::from(&vault_1_interface), + TokenAccountVariant::Token1Vault { + pool_state: pdas.pool_state, + token_1_mint: ctx.token_1_mint, + }, + ) + .expect("Token1Vault construction failed"), + ]; + + // Create decompression instructions + let all_instructions = create_load_accounts_instructions( + &program_owned_accounts, + std::slice::from_ref(&creator_lp_interface.inner), + std::slice::from_ref(&lp_mint_interface), + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), // rent_sponsor + &ctx.rpc, + ) + .await + .expect("create_load_accounts_instructions should succeed"); + + println!(" Generated {} decompression instructions", all_instructions.len()); + + // Execute decompression + // Note: creator must sign because they own the LP token ATA being decompressed + ctx.rpc + .create_and_send_transaction( + &all_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer, &ctx.creator], + ) + .await + .expect("Decompression should succeed"); + + // ========================================================================== + // PHASE 10: Assert decompression success + // ========================================================================== + println!("\nPhase 10: Verifying decompression..."); + + // All accounts should be back on-chain + assert_onchain_exists(&mut ctx.rpc, &pdas.pool_state).await; + assert_onchain_exists(&mut ctx.rpc, &pdas.observation_state).await; + assert_onchain_exists(&mut ctx.rpc, &pdas.lp_mint).await; + assert_onchain_exists(&mut ctx.rpc, &pdas.token_0_vault).await; + assert_onchain_exists(&mut ctx.rpc, &pdas.token_1_vault).await; + assert_onchain_exists(&mut ctx.rpc, &pdas.creator_lp_token).await; + + // Verify LP token balance preserved after decompression + let lp_token_after_decompression = parse_token( + &ctx.rpc + .get_account(pdas.creator_lp_token) + .await + .unwrap() + .unwrap() + .data, + ); + assert_eq!( + lp_token_after_decompression.amount, expected_balance_after_withdraw, + "LP token balance should be preserved after decompression" + ); + println!( + " LP balance after decompression: {} (expected: {})", + lp_token_after_decompression.amount, expected_balance_after_withdraw + ); + + // Verify compressed token accounts are consumed + let remaining_vault_0 = ctx + .rpc + .get_compressed_token_accounts_by_owner(&pdas.token_0_vault, None, None) + .await + .unwrap() + .value + .items; + assert!( + remaining_vault_0.is_empty(), + "Compressed token_0_vault should be consumed" + ); + + let remaining_vault_1 = ctx + .rpc + .get_compressed_token_accounts_by_owner(&pdas.token_1_vault, None, None) + .await + .unwrap() + .value + .items; + assert!( + remaining_vault_1.is_empty(), + "Compressed token_1_vault should be consumed" + ); + + let remaining_creator_lp = ctx + .rpc + .get_compressed_token_accounts_by_owner(&pdas.creator_lp_token, None, None) + .await + .unwrap() + .value + .items; + assert!( + remaining_creator_lp.is_empty(), + "Compressed creator_lp_token should be consumed" + ); println!("\nAMM full lifecycle test completed successfully!"); println!(" - Initialize: OK"); println!(" - Deposit: OK"); println!(" - Withdraw: OK"); println!(" - Compression: OK"); - println!(" - Decompression: TODO (requires seed struct types)"); + println!(" - Decompression: OK"); } From 237cd240e7e1888a41c90df19600a6c0fa3fadc4 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 23:20:12 +0000 Subject: [PATCH 03/11] stash tests --- Cargo.lock | 1 + sdk-libs/compressible-client/src/lib.rs | 7 + .../docs/rentfree_program/architecture.md | 23 +- .../rentfree/account/decompress_context.rs | 7 + .../macros/src/rentfree/program/decompress.rs | 7 +- .../src/rentfree/program/variant_enum.rs | 5 +- .../csdk-anchor-full-derived-test/Cargo.toml | 1 + .../src/amm_test/initialize.rs | 4 +- .../src/d6_account_types.rs | 3 + .../src/d8_builder_paths.rs | 3 + .../src/d9_seeds.rs | 3 + .../csdk-anchor-full-derived-test/src/lib.rs | 128 +++ .../tests/amm_test.rs | 61 +- .../tests/macro_test.rs | 873 ++++++++++++++++++ 14 files changed, 1090 insertions(+), 36 deletions(-) create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/d6_account_types.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/d8_builder_paths.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/d9_seeds.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs diff --git a/Cargo.lock b/Cargo.lock index 8531f685da..8a0ad5f622 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1661,6 +1661,7 @@ dependencies = [ "solana-instruction", "solana-keypair", "solana-logger", + "solana-msg 2.2.1", "solana-program", "solana-program-error 2.2.2", "solana-pubkey 2.4.0", diff --git a/sdk-libs/compressible-client/src/lib.rs b/sdk-libs/compressible-client/src/lib.rs index 9e09986cc3..4828a9ee8a 100644 --- a/sdk-libs/compressible-client/src/lib.rs +++ b/sdk-libs/compressible-client/src/lib.rs @@ -269,6 +269,8 @@ pub mod compressible_instruction { } /// Returns program account metas for PDA-only decompression (no CToken accounts). + /// Note: Still passes all 7 accounts because the struct has Optional fields that + /// Anchor still deserializes. Uses rent_sponsor as placeholder for ctoken_rent_sponsor. pub fn accounts_pda_only( fee_payer: Pubkey, config: Pubkey, @@ -278,6 +280,11 @@ pub mod compressible_instruction { AccountMeta::new(fee_payer, true), AccountMeta::new_readonly(config, false), AccountMeta::new(rent_sponsor, false), + // Optional token accounts - use placeholders that satisfy constraints + AccountMeta::new(rent_sponsor, false), // ctoken_rent_sponsor (mut) - reuse rent_sponsor + AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false), + AccountMeta::new_readonly(LIGHT_TOKEN_CPI_AUTHORITY, false), + AccountMeta::new_readonly(COMPRESSIBLE_CONFIG_V1, false), ] } } diff --git a/sdk-libs/macros/docs/rentfree_program/architecture.md b/sdk-libs/macros/docs/rentfree_program/architecture.md index 9bed62d7e3..b26aec3d04 100644 --- a/sdk-libs/macros/docs/rentfree_program/architecture.md +++ b/sdk-libs/macros/docs/rentfree_program/architecture.md @@ -146,7 +146,7 @@ light_finalize: Complete compression via CPI Account exists as compressed state + temporary PDA ``` -**Decompress (Read/Modify)** +**Decompress PDAs (Read/Modify)** ``` Client fetches compressed account from indexer | @@ -160,6 +160,27 @@ PDA recreated on-chain from compressed state User interacts with standard Anchor account ``` +**Decompress Token Accounts and Mints** + +Token accounts (ATAs) and mints are decompressed directly via the ctoken program, not through the generated `decompress_accounts_idempotent` instruction: + +``` +Client fetches compressed token account/mint from indexer + | + v +Client calls ctoken program's decompress instruction directly + | + v +Token account or mint recreated on-chain + | + v +User interacts with decompressed ctoken account/mint +``` + +This separation exists because: +- **PDAs**: Program-specific, seeds defined by your program, decompressed via generated instruction +- **Token accounts/mints**: Standard ctoken format, decompressed via ctoken program + **Re-Compress (Return to compressed)** ``` Authority calls compress_accounts_idempotent diff --git a/sdk-libs/macros/src/rentfree/account/decompress_context.rs b/sdk-libs/macros/src/rentfree/account/decompress_context.rs index e7680cb71e..c5116f876f 100644 --- a/sdk-libs/macros/src/rentfree/account/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/account/decompress_context.rs @@ -160,21 +160,27 @@ pub fn generate_decompress_context_trait_impl( Vec, Vec<(Self::PackedTokenData, Self::CompressedMeta)>, ), solana_program_error::ProgramError> { + solana_msg::msg!("collect_pda_and_token: start, {} accounts", compressed_accounts.len()); 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 program_id = &crate::ID; + solana_msg::msg!("collect_pda_and_token: allocating vecs"); let mut compressed_pda_infos = Vec::with_capacity(compressed_accounts.len()); let mut compressed_token_accounts = Vec::with_capacity(compressed_accounts.len()); + solana_msg::msg!("collect_pda_and_token: starting loop"); for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { + solana_msg::msg!("collect_pda_and_token: processing account {}", i); let meta = compressed_data.meta; match compressed_data.data { #(#pda_match_arms)* RentFreeAccountVariant::PackedCTokenData(mut data) => { + solana_msg::msg!("collect_pda_and_token: token variant {}", i); data.token_data.version = 3; compressed_token_accounts.push((data, meta)); + solana_msg::msg!("collect_pda_and_token: token {} done", i); } RentFreeAccountVariant::CTokenData(_) => { unreachable!(); @@ -182,6 +188,7 @@ pub fn generate_decompress_context_trait_impl( } } + solana_msg::msg!("collect_pda_and_token: loop done, pdas={} tokens={}", compressed_pda_infos.len(), compressed_token_accounts.len()); std::result::Result::Ok((compressed_pda_infos, compressed_token_accounts)) } diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index e58c1bd724..32b8256524 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -229,7 +229,12 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( #( seeds_vec.push(seeds[#indices].to_vec()); )* - seeds_vec.push(vec![bump]); + // Avoid vec![bump] macro which expands to box_new allocation + { + let mut bump_vec = Vec::with_capacity(1); + bump_vec.push(bump); + seeds_vec.push(bump_vec); + } Ok((seeds_vec, pda)) }) } diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index f685e34695..1b4283995d 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -340,7 +340,10 @@ pub fn compressed_account_variant_with_ctx_seeds( ) -> std::result::Result { match self { #(#unpack_match_arms)* - Self::PackedCTokenData(_data) => Ok(self.clone()), + Self::PackedCTokenData(_) => { + // PackedCTokenData is handled separately in collect_pda_and_token + unreachable!("PackedCTokenData should not be unpacked through Unpack trait") + } Self::CTokenData(_data) => unreachable!(), } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml index 4941c8cfb6..303a22f961 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml @@ -23,6 +23,7 @@ light-sdk-types = { workspace = true, features = ["v2", "cpi-context"] } light-hasher = { workspace = true, features = ["solana"] } solana-program = { workspace = true } solana-program-error = { workspace = true } +solana-msg = { workspace = true } solana-account-info = { workspace = true } solana-pubkey = { workspace = true } light-macros = { workspace = true, features = ["solana"] } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs index d9712eff94..2af4b45b79 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs @@ -74,7 +74,7 @@ pub struct InitializePool<'info> { seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state.key().as_ref()], bump, )] - pub lp_mint_signer: UncheckedAccount<'info>, + pub lp_mint_signer: UncheckedAccount<'info>, // TODO: check where the cpi gets the seeds from #[account(mut)] #[light_mint( @@ -82,7 +82,7 @@ pub struct InitializePool<'info> { authority = authority, decimals = 9, mint_seeds = &[POOL_LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]], - authority_seeds = &[AUTH_SEED.as_bytes(), &[params.authority_bump]] + authority_seeds = &[AUTH_SEED.as_bytes(), &[params.authority_bump]] // TODO: get the authority seeds from authority if defined )] pub lp_mint: UncheckedAccount<'info>, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/d6_account_types.rs b/sdk-tests/csdk-anchor-full-derived-test/src/d6_account_types.rs new file mode 100644 index 0000000000..d9adf49fa8 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/d6_account_types.rs @@ -0,0 +1,3 @@ +//! Re-export d6_account_types from instructions module for top-level access. + +pub use crate::instructions::d6_account_types::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/d8_builder_paths.rs b/sdk-tests/csdk-anchor-full-derived-test/src/d8_builder_paths.rs new file mode 100644 index 0000000000..e6baf46321 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/d8_builder_paths.rs @@ -0,0 +1,3 @@ +//! Re-export d8_builder_paths from instructions module for top-level access. + +pub use crate::instructions::d8_builder_paths::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/d9_seeds.rs b/sdk-tests/csdk-anchor-full-derived-test/src/d9_seeds.rs new file mode 100644 index 0000000000..9606a49312 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/d9_seeds.rs @@ -0,0 +1,3 @@ +//! Re-export d9_seeds from instructions module for top-level access. + +pub use crate::instructions::d9_seeds::*; 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 cc7d6492db..52817df988 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -7,6 +7,9 @@ use light_sdk_types::CpiSigner; pub mod amm_test; pub mod d5_markers; +pub mod d6_account_types; +pub mod d8_builder_paths; +pub mod d9_seeds; pub mod errors; pub mod instruction_accounts; pub mod instructions; @@ -14,6 +17,9 @@ pub mod processors; pub mod state; pub use amm_test::*; pub use d5_markers::*; +pub use d6_account_types::*; +pub use d8_builder_paths::*; +pub use d9_seeds::*; pub use instruction_accounts::*; pub use instructions::d7_infra_names::{ D7_ALL_AUTH_SEED, D7_ALL_VAULT_SEED, D7_CTOKEN_AUTH_SEED, D7_CTOKEN_VAULT_SEED, @@ -56,6 +62,15 @@ pub mod csdk_anchor_full_derived_test { use super::{ amm_test::{Deposit, InitializeParams, InitializePool, Withdraw}, d5_markers::{D5RentfreeBare, D5RentfreeBareParams}, + d6_account_types::{D6Account, D6AccountParams, D6Boxed, D6BoxedParams}, + d8_builder_paths::{ + D8All, D8AllParams, D8MultiRentfree, D8MultiRentfreeParams, D8PdaOnly, D8PdaOnlyParams, + }, + d9_seeds::{ + D9Constant, D9ConstantParams, D9CtxAccount, D9CtxAccountParams, D9Literal, + D9LiteralParams, D9Mixed, D9MixedParams, D9Param, D9ParamBytes, D9ParamBytesParams, + D9ParamParams, + }, instruction_accounts::CreatePdasAndMintAuto, FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; @@ -172,4 +187,117 @@ pub mod csdk_anchor_full_derived_test { pub fn withdraw(ctx: Context, lp_token_amount: u64) -> Result<()> { crate::amm_test::process_withdraw(ctx, lp_token_amount) } + + // ========================================================================= + // D6 Account Types: Account type extraction + // ========================================================================= + + /// D6: Direct Account<'info, T> type + pub fn d6_account<'info>( + ctx: Context<'_, '_, '_, 'info, D6Account<'info>>, + params: D6AccountParams, + ) -> Result<()> { + ctx.accounts.d6_account_record.owner = params.owner; + Ok(()) + } + + /// D6: Box> type + pub fn d6_boxed<'info>( + ctx: Context<'_, '_, '_, 'info, D6Boxed<'info>>, + params: D6BoxedParams, + ) -> Result<()> { + ctx.accounts.d6_boxed_record.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D8 Builder Paths: Builder code generation paths + // ========================================================================= + + /// D8: Only #[rentfree] fields (no token accounts) + pub fn d8_pda_only<'info>( + ctx: Context<'_, '_, '_, 'info, D8PdaOnly<'info>>, + params: D8PdaOnlyParams, + ) -> Result<()> { + ctx.accounts.d8_pda_only_record.owner = params.owner; + Ok(()) + } + + /// D8: Multiple #[rentfree] fields of same type + pub fn d8_multi_rentfree<'info>( + ctx: Context<'_, '_, '_, 'info, D8MultiRentfree<'info>>, + params: D8MultiRentfreeParams, + ) -> Result<()> { + ctx.accounts.d8_multi_record1.owner = params.owner; + ctx.accounts.d8_multi_record2.owner = params.owner; + Ok(()) + } + + /// D8: Multiple #[rentfree] fields of different types + pub fn d8_all<'info>( + ctx: Context<'_, '_, '_, 'info, D8All<'info>>, + params: D8AllParams, + ) -> Result<()> { + ctx.accounts.d8_all_single.owner = params.owner; + ctx.accounts.d8_all_multi.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D9 Seeds: Seed expression classification + // ========================================================================= + + /// D9: Literal seed expression + pub fn d9_literal<'info>( + ctx: Context<'_, '_, '_, 'info, D9Literal<'info>>, + _params: D9LiteralParams, + ) -> Result<()> { + ctx.accounts.d9_literal_record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Constant seed expression + pub fn d9_constant<'info>( + ctx: Context<'_, '_, '_, 'info, D9Constant<'info>>, + _params: D9ConstantParams, + ) -> Result<()> { + ctx.accounts.d9_constant_record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Context account seed expression + pub fn d9_ctx_account<'info>( + ctx: Context<'_, '_, '_, 'info, D9CtxAccount<'info>>, + _params: D9CtxAccountParams, + ) -> Result<()> { + ctx.accounts.d9_ctx_record.owner = ctx.accounts.authority.key(); + Ok(()) + } + + /// D9: Param seed expression (Pubkey) + pub fn d9_param<'info>( + ctx: Context<'_, '_, '_, 'info, D9Param<'info>>, + params: D9ParamParams, + ) -> Result<()> { + ctx.accounts.d9_param_record.owner = params.owner; + Ok(()) + } + + /// D9: Param bytes seed expression (u64) + pub fn d9_param_bytes<'info>( + ctx: Context<'_, '_, '_, 'info, D9ParamBytes<'info>>, + _params: D9ParamBytesParams, + ) -> Result<()> { + ctx.accounts.d9_param_bytes_record.owner = ctx.accounts.fee_payer.key(); + Ok(()) + } + + /// D9: Mixed seed expression types + pub fn d9_mixed<'info>( + ctx: Context<'_, '_, '_, 'info, D9Mixed<'info>>, + params: D9MixedParams, + ) -> Result<()> { + ctx.accounts.d9_mixed_record.owner = params.owner; + Ok(()) + } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs index 7c05442542..ae4fcffe2c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -676,36 +676,36 @@ async fn test_amm_full_lifecycle() { }, ) .expect("PoolState seed verification failed"), - RentFreeDecompressAccount::from_seeds( - AccountInterface::from(&observation_interface), - ObservationStateSeeds { - pool_state: pdas.pool_state, - }, - ) - .expect("ObservationState seed verification failed"), - RentFreeDecompressAccount::from_ctoken( - AccountInterface::from(&vault_0_interface), - TokenAccountVariant::Token0Vault { - pool_state: pdas.pool_state, - token_0_mint: ctx.token_0_mint, - }, - ) - .expect("Token0Vault construction failed"), - RentFreeDecompressAccount::from_ctoken( - AccountInterface::from(&vault_1_interface), - TokenAccountVariant::Token1Vault { - pool_state: pdas.pool_state, - token_1_mint: ctx.token_1_mint, - }, - ) - .expect("Token1Vault construction failed"), + // RentFreeDecompressAccount::from_seeds( + // AccountInterface::from(&observation_interface), + // ObservationStateSeeds { + // pool_state: pdas.pool_state, + // }, + // ) + // .expect("ObservationState seed verification failed"), + // RentFreeDecompressAccount::from_ctoken( + // AccountInterface::from(&vault_0_interface), + // TokenAccountVariant::Token0Vault { + // pool_state: pdas.pool_state, + // token_0_mint: ctx.token_0_mint, + // }, + // ) + // .expect("Token0Vault construction failed"), + // RentFreeDecompressAccount::from_ctoken( + // AccountInterface::from(&vault_1_interface), + // TokenAccountVariant::Token1Vault { + // pool_state: pdas.pool_state, + // token_1_mint: ctx.token_1_mint, + // }, + // ) + // .expect("Token1Vault construction failed"), ]; // Create decompression instructions let all_instructions = create_load_accounts_instructions( &program_owned_accounts, - std::slice::from_ref(&creator_lp_interface.inner), - std::slice::from_ref(&lp_mint_interface), + &[], //std::slice::from_ref(&creator_lp_interface.inner), TODO decompress directly from ctoken program + &[], // std::slice::from_ref(&lp_mint_interface), TODO decompress directly from ctoken program ctx.program_id, ctx.payer.pubkey(), ctx.config_pda, @@ -715,16 +715,15 @@ async fn test_amm_full_lifecycle() { .await .expect("create_load_accounts_instructions should succeed"); - println!(" Generated {} decompression instructions", all_instructions.len()); + println!( + " Generated {} decompression instructions", + all_instructions.len() + ); // Execute decompression // Note: creator must sign because they own the LP token ATA being decompressed ctx.rpc - .create_and_send_transaction( - &all_instructions, - &ctx.payer.pubkey(), - &[&ctx.payer, &ctx.creator], - ) + .create_and_send_transaction(&all_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await .expect("Decompression should succeed"); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs new file mode 100644 index 0000000000..a3df565536 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs @@ -0,0 +1,873 @@ +//! Integration tests for D6, D8, and D9 macro test instructions. +//! +//! These tests verify that the macro-generated code works correctly at runtime +//! by testing the full lifecycle: create account -> verify on-chain -> compress -> decompress. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible_client::{ + get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, +}; +use light_macros::pubkey; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, + Indexer, ProgramTestConfig, Rpc, +}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +/// Test context shared across instruction tests +#[allow(dead_code)] +struct TestContext { + rpc: LightProgramTest, + payer: Keypair, + config_pda: Pubkey, + program_id: Pubkey, +} + +impl TestContext { + async fn new() -> Self { + let program_id = csdk_anchor_full_derived_test::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("csdk_anchor_full_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 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"); + + Self { + rpc, + payer, + config_pda, + program_id, + } + } + + async fn assert_onchain_exists(&mut self, pda: &Pubkey) { + assert!( + self.rpc.get_account(*pda).await.unwrap().is_some(), + "Account {} should exist on-chain", + pda + ); + } + + async fn assert_onchain_closed(&mut self, pda: &Pubkey) { + let acc = self.rpc.get_account(*pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "Account {} should be closed", + pda + ); + } + + async fn assert_compressed_exists(&mut self, addr: [u8; 32]) { + let acc = self + .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()); + } +} + +// ============================================================================= +// D6 Account Types Tests +// ============================================================================= + +/// Tests D6Account: Direct Account<'info, T> type +#[tokio::test] +async fn test_d6_account() { + use csdk_anchor_full_derived_test::d6_account_types::{D6AccountParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d6_account", owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D6Account { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d6_account_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D6Account { + params: D6AccountParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D6Account instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D6Boxed: Box> type +#[tokio::test] +async fn test_d6_boxed() { + use csdk_anchor_full_derived_test::d6_account_types::{D6BoxedParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d6_boxed", owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D6Boxed { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d6_boxed_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D6Boxed { + params: D6BoxedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D6Boxed instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D8 Builder Paths Tests +// ============================================================================= + +/// Tests D8PdaOnly: Only #[rentfree] fields (no token accounts) +#[tokio::test] +async fn test_d8_pda_only() { + use csdk_anchor_full_derived_test::d8_builder_paths::{D8PdaOnlyParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address( + &[b"d8_pda_only", owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D8PdaOnly { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d8_pda_only_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D8PdaOnly { + params: D8PdaOnlyParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D8PdaOnly instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D8MultiRentfree: Multiple #[rentfree] fields of same type +#[tokio::test] +async fn test_d8_multi_rentfree() { + use csdk_anchor_full_derived_test::d8_builder_paths::{D8MultiRentfreeParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + let id1 = 111u64; + let id2 = 222u64; + + // Derive PDAs + let (pda1, _) = Pubkey::find_program_address( + &[b"d8_multi_1", owner.as_ref(), id1.to_le_bytes().as_ref()], + &ctx.program_id, + ); + let (pda2, _) = Pubkey::find_program_address( + &[b"d8_multi_2", owner.as_ref(), id2.to_le_bytes().as_ref()], + &ctx.program_id, + ); + + // Get proof for both PDAs + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![ + CreateAccountsProofInput::pda(pda1), + CreateAccountsProofInput::pda(pda2), + ], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D8MultiRentfree { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d8_multi_record1: pda1, + d8_multi_record2: pda2, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D8MultiRentfree { + params: D8MultiRentfreeParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id1, + id2, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D8MultiRentfree instruction should succeed"); + + // Verify both accounts exist on-chain + ctx.assert_onchain_exists(&pda1).await; + ctx.assert_onchain_exists(&pda2).await; +} + +/// Tests D8All: Multiple #[rentfree] fields of different types +#[tokio::test] +async fn test_d8_all() { + use csdk_anchor_full_derived_test::d8_builder_paths::{D8AllParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDAs + let (pda_single, _) = Pubkey::find_program_address( + &[b"d8_all_single", owner.as_ref()], + &ctx.program_id, + ); + let (pda_multi, _) = Pubkey::find_program_address( + &[b"d8_all_multi", owner.as_ref()], + &ctx.program_id, + ); + + // Get proof for both PDAs + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![ + CreateAccountsProofInput::pda(pda_single), + CreateAccountsProofInput::pda(pda_multi), + ], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D8All { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d8_all_single: pda_single, + d8_all_multi: pda_multi, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D8All { + params: D8AllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D8All instruction should succeed"); + + // Verify both accounts exist on-chain + ctx.assert_onchain_exists(&pda_single).await; + ctx.assert_onchain_exists(&pda_multi).await; +} + +// ============================================================================= +// D9 Seeds Tests +// ============================================================================= + +/// Tests D9Literal: Literal seed expression +#[tokio::test] +async fn test_d9_literal() { + use csdk_anchor_full_derived_test::d9_seeds::{D9LiteralParams}; + + let mut ctx = TestContext::new().await; + + // Derive PDA (literal seeds only) + let (pda, _) = Pubkey::find_program_address( + &[b"d9_literal_record"], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9Literal { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d9_literal_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9Literal { + _params: D9LiteralParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9Literal instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9Constant: Constant seed expression +#[tokio::test] +async fn test_d9_constant() { + use csdk_anchor_full_derived_test::d9_seeds::{D9ConstantParams}; + use csdk_anchor_full_derived_test::D9_CONSTANT_SEED; + + let mut ctx = TestContext::new().await; + + // Derive PDA using constant + let (pda, _) = Pubkey::find_program_address( + &[D9_CONSTANT_SEED], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9Constant { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d9_constant_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9Constant { + _params: D9ConstantParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9Constant instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9CtxAccount: Context account seed expression +#[tokio::test] +async fn test_d9_ctx_account() { + use csdk_anchor_full_derived_test::d9_seeds::{D9CtxAccountParams}; + + let mut ctx = TestContext::new().await; + let authority = Keypair::new(); + + // Derive PDA using authority key + let (pda, _) = Pubkey::find_program_address( + &[b"d9_ctx", authority.pubkey().as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9CtxAccount { + fee_payer: ctx.payer.pubkey(), + authority: authority.pubkey(), + compression_config: ctx.config_pda, + d9_ctx_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9CtxAccount { + _params: D9CtxAccountParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9CtxAccount instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9Param: Param seed expression (Pubkey) +#[tokio::test] +async fn test_d9_param() { + use csdk_anchor_full_derived_test::d9_seeds::{D9ParamParams}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA using param + let (pda, _) = Pubkey::find_program_address( + &[b"d9_param", owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9Param { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d9_param_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9Param { + params: D9ParamParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9Param instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9ParamBytes: Param bytes seed expression (u64) +#[tokio::test] +async fn test_d9_param_bytes() { + use csdk_anchor_full_derived_test::d9_seeds::{D9ParamBytesParams}; + + let mut ctx = TestContext::new().await; + let id = 12345u64; + + // Derive PDA using param bytes + let (pda, _) = Pubkey::find_program_address( + &[b"d9_param_bytes", id.to_le_bytes().as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9ParamBytes { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d9_param_bytes_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9ParamBytes { + _params: D9ParamBytesParams { + create_accounts_proof: proof_result.create_accounts_proof, + id, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9ParamBytes instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9Mixed: Mixed seed expression types +#[tokio::test] +async fn test_d9_mixed() { + use csdk_anchor_full_derived_test::d9_seeds::{D9MixedParams}; + + let mut ctx = TestContext::new().await; + let authority = Keypair::new(); + let owner = Keypair::new().pubkey(); + + // Derive PDA using mixed seeds + let (pda, _) = Pubkey::find_program_address( + &[b"d9_mixed", authority.pubkey().as_ref(), owner.as_ref()], + &ctx.program_id, + ); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9Mixed { + fee_payer: ctx.payer.pubkey(), + authority: authority.pubkey(), + compression_config: ctx.config_pda, + d9_mixed_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9Mixed { + params: D9MixedParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9Mixed instruction should succeed"); + + // Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// Full Lifecycle Test (compression + decompression) +// ============================================================================= + +/// Tests the full lifecycle with compression and decompression +#[tokio::test] +async fn test_d8_pda_only_full_lifecycle() { + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D8PdaOnlyRecordSeeds; + use csdk_anchor_full_derived_test::d8_builder_paths::D8PdaOnlyParams; + use light_compressible::rent::SLOTS_PER_EPOCH; + use light_compressible_client::{ + create_load_accounts_instructions, AccountInterface, AccountInterfaceExt, + RentFreeDecompressAccount, + }; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[b"d8_pda_only", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build and send instruction + let accounts = csdk_anchor_full_derived_test::accounts::D8PdaOnly { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d8_pda_only_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D8PdaOnly { + params: D8PdaOnlyParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D8PdaOnly instruction should succeed"); + + // PHASE 1: Verify account exists on-chain + ctx.assert_onchain_exists(&pda).await; + + // PHASE 2: Warp to trigger auto-compression + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + + // Verify account is compressed (on-chain closed) + ctx.assert_onchain_closed(&pda).await; + + // Derive compressed address + let address_tree_pubkey = ctx.rpc.get_address_tree_v2().tree; + let compressed_address = light_compressed_account::address::derive_address( + &pda.to_bytes(), + &address_tree_pubkey.to_bytes(), + &ctx.program_id.to_bytes(), + ); + + // Verify compressed account exists with data + ctx.assert_compressed_exists(compressed_address).await; + + // PHASE 3: Decompress account + let account_interface = ctx + .rpc + .get_account_info_interface(&pda, &ctx.program_id) + .await + .expect("failed to get account interface"); + assert!(account_interface.is_cold, "Account should be cold"); + + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&account_interface), + D8PdaOnlyRecordSeeds { owner }, + ) + .expect("Seed verification failed")]; + + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .expect("create_load_accounts_instructions should succeed"); + + ctx.rpc + .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("Decompression should succeed"); + + // PHASE 4: Verify account is back on-chain + ctx.assert_onchain_exists(&pda).await; +} From 845590dceb5d74916e5ce36e668aa438b0b8e812 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 01:49:32 +0000 Subject: [PATCH 04/11] fix amm test 1 by 1 --- Cargo.lock | 2 + .../src/rentfree/account/pack_unpack.rs | 4 +- sdk-libs/sdk/Cargo.toml | 9 + .../src/compressible/decompress_runtime.rs | 68 ++++-- sdk-libs/sdk/src/cpi/invoke.rs | 1 - sdk-libs/sdk/src/cpi/v2/accounts.rs | 4 +- sdk-libs/token-sdk/src/token/create.rs | 2 +- .../csdk-anchor-full-derived-test/Cargo.toml | 2 + .../tests/amm_test.rs | 230 ++++++++++++++---- 9 files changed, 253 insertions(+), 69 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8a0ad5f622..1e4ccaf0c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1646,6 +1646,7 @@ dependencies = [ "light-compressible", "light-compressible-client", "light-hasher", + "light-heap", "light-macros", "light-program-test", "light-sdk", @@ -4008,6 +4009,7 @@ dependencies = [ "light-compressible", "light-concurrent-merkle-tree", "light-hasher", + "light-heap", "light-macros", "light-sdk-macros", "light-sdk-types", diff --git a/sdk-libs/macros/src/rentfree/account/pack_unpack.rs b/sdk-libs/macros/src/rentfree/account/pack_unpack.rs index 01ed4b99e4..df59bc2fcf 100644 --- a/sdk-libs/macros/src/rentfree/account/pack_unpack.rs +++ b/sdk-libs/macros/src/rentfree/account/pack_unpack.rs @@ -52,7 +52,9 @@ fn generate_with_packed_struct( if *field_name == "compression_info" { quote! { #field_name: None } } else if is_pubkey_type(field_type) { - quote! { #field_name: remaining_accounts.insert_or_get(self.#field_name) } + // Use read-only since pubkey fields are references (owner, authority, etc.) + // not accounts that need to be modified + quote! { #field_name: remaining_accounts.insert_or_get_read_only(self.#field_name) } } else if is_copy_type(field_type) { quote! { #field_name: self.#field_name } } else { diff --git a/sdk-libs/sdk/Cargo.toml b/sdk-libs/sdk/Cargo.toml index dd95628d93..340e0cd6e8 100644 --- a/sdk-libs/sdk/Cargo.toml +++ b/sdk-libs/sdk/Cargo.toml @@ -27,6 +27,7 @@ keccak = ["light-hasher/keccak", "light-compressed-account/keccak"] sha256 = ["light-hasher/sha256", "light-compressed-account/sha256"] merkle-tree = ["light-concurrent-merkle-tree/solana"] anchor-discriminator = ["light-sdk-macros/anchor-discriminator"] +custom-heap = ["light-heap"] [dependencies] solana-pubkey = { workspace = true, features = ["borsh", "sha2", "curve25519"] } @@ -56,9 +57,17 @@ light-account-checks = { workspace = true, features = ["solana"] } light-zero-copy = { workspace = true } light-concurrent-merkle-tree = { workspace = true, optional = true } light-compressible = { workspace = true } +light-heap = { workspace = true, optional = true } [dev-dependencies] num-bigint = { workspace = true } light-compressed-account = { workspace = true, features = ["new-unique"] } light-hasher = { workspace = true, features = ["keccak"] } anchor-lang = { 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-libs/sdk/src/compressible/decompress_runtime.rs b/sdk-libs/sdk/src/compressible/decompress_runtime.rs index d5e36d3387..c1302bcb0c 100644 --- a/sdk-libs/sdk/src/compressible/decompress_runtime.rs +++ b/sdk-libs/sdk/src/compressible/decompress_runtime.rs @@ -1,5 +1,8 @@ //! Traits and processor for decompress_accounts_idempotent instruction. -use light_compressed_account::instruction_data::with_account_info::CompressedAccountInfo; +use light_compressed_account::instruction_data::{ + cpi_context::CompressedCpiContext, + with_account_info::{CompressedAccountInfo, InstructionDataInvokeCpiWithAccountInfo}, +}; use light_sdk_types::{ cpi_accounts::CpiAccountsConfig, cpi_context_write::CpiContextWriteAccounts, instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, CpiSigner, @@ -10,10 +13,7 @@ use solana_program_error::ProgramError; use solana_pubkey::Pubkey; use crate::{ - cpi::{ - v2::{CpiAccounts, LightSystemProgramCpi}, - InvokeLightSystemProgram, LightCpiInstruction, - }, + cpi::{v2::CpiAccounts, InvokeLightSystemProgram}, AnchorDeserialize, AnchorSerialize, LightDiscriminator, }; @@ -175,7 +175,13 @@ where } let compressed_infos = { - let seed_refs: Vec<&[u8]> = seeds_vec.iter().map(|v| v.as_slice()).collect(); + // Use fixed-size array to avoid heap allocation (MAX_SEEDS = 16) + const MAX_SEEDS: usize = 16; + let mut seed_refs: [&[u8]; MAX_SEEDS] = [&[]; MAX_SEEDS]; + let len = seeds_vec.len().min(MAX_SEEDS); + for i in 0..len { + seed_refs[i] = seeds_vec[i].as_slice(); + } crate::compressible::decompress_idempotent::prepare_account_for_decompression_idempotent::( program_id, data, @@ -188,7 +194,7 @@ where solana_account, accounts_rent_sponsor, cpi_accounts, - seed_refs.as_slice(), + &seed_refs[..len], )? }; compressed_pda_infos.extend(compressed_infos); @@ -220,6 +226,7 @@ where 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(()); } @@ -274,29 +281,56 @@ where // Process PDAs (if any) if has_pdas { 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())?; + // PDAs only - execute directly (manual construction to avoid extra allocations) + let cpi_signer_config = cpi_accounts.config().cpi_signer; + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: cpi_signer_config.bump, + invoking_program_id: cpi_signer_config.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: false, + with_transaction_hash: false, + cpi_context: CompressedCpiContext::default(), + proof: proof.0, + new_address_params: Vec::new(), + account_infos: compressed_pda_infos, + read_only_addresses: Vec::new(), + read_only_accounts: Vec::new(), + }; + instruction_data.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 + let cpi_context_account = cpi_accounts .cpi_context() .map_err(|_| ProgramError::MissingRequiredSignature)?; let system_cpi_accounts = CpiContextWriteAccounts { fee_payer, authority, - cpi_context, + cpi_context: cpi_context_account, 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)?; + // Manual construction to avoid extra allocations + let instruction_data = InstructionDataInvokeCpiWithAccountInfo { + mode: 1, + bump: cpi_signer.bump, + invoking_program_id: cpi_signer.program_id.into(), + compress_or_decompress_lamports: 0, + is_compress: false, + with_cpi_context: true, + with_transaction_hash: false, + cpi_context: CompressedCpiContext::first(), + proof: proof.0, + new_address_params: Vec::new(), + account_infos: compressed_pda_infos, + read_only_addresses: Vec::new(), + read_only_accounts: Vec::new(), + }; + instruction_data.invoke_write_to_cpi_context_first(system_cpi_accounts)?; } } diff --git a/sdk-libs/sdk/src/cpi/invoke.rs b/sdk-libs/sdk/src/cpi/invoke.rs index 91b0bf479d..4562d0ad18 100644 --- a/sdk-libs/sdk/src/cpi/invoke.rs +++ b/sdk-libs/sdk/src/cpi/invoke.rs @@ -79,7 +79,6 @@ where accounts: account_metas, data, }; - invoke_light_system_program(&account_infos, instruction, self.get_bump()) } diff --git a/sdk-libs/sdk/src/cpi/v2/accounts.rs b/sdk-libs/sdk/src/cpi/v2/accounts.rs index d73215cc1b..bce47fe5ea 100644 --- a/sdk-libs/sdk/src/cpi/v2/accounts.rs +++ b/sdk-libs/sdk/src/cpi/v2/accounts.rs @@ -88,8 +88,8 @@ pub fn to_account_metas(cpi_accounts: &CpiAccounts<'_, '_>) -> Result Date: Sun, 18 Jan 2026 02:39:43 +0000 Subject: [PATCH 05/11] fix tests, add light mint docs, allow empty CreateAccountProof --- .../src/create_accounts_proof.rs | 26 +- sdk-libs/macros/docs/CLAUDE.md | 11 + sdk-libs/macros/docs/accounts/light_mint.md | 269 ++++++++ .../program-test/src/indexer/test_indexer.rs | 12 +- .../src/d7_infra_names.rs | 3 + .../src/instructions/d5_markers/all.rs | 6 + .../instructions/d5_markers/rentfree_token.rs | 6 + .../src/instructions/d7_infra_names/all.rs | 6 + .../d7_infra_names/ctoken_config.rs | 6 + .../csdk-anchor-full-derived-test/src/lib.rs | 208 +++++- .../tests/macro_test.rs | 633 ++++++++++++++++-- 11 files changed, 1125 insertions(+), 61 deletions(-) create mode 100644 sdk-libs/macros/docs/accounts/light_mint.md create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/d7_infra_names.rs diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs index 7019a7871d..ff20f46d41 100644 --- a/sdk-libs/compressible-client/src/create_accounts_proof.rs +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -7,9 +7,11 @@ //! - Returns a single `address_tree_info` since all accounts use the same tree use light_client::{ - indexer::{AddressWithTree, Indexer, IndexerError}, + indexer::{AddressWithTree, Indexer, IndexerError, ValidityProofWithContext}, rpc::{Rpc, RpcError}, }; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; +use light_sdk::instruction::PackedAddressTreeInfo; use light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address; use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; @@ -136,7 +138,27 @@ pub async fn get_create_accounts_proof( inputs: Vec, ) -> Result { if inputs.is_empty() { - return Err(CreateAccountsProofError::EmptyInputs); + // Token-only instructions: no addresses to derive, but still need tree info + let state_tree_info = rpc + .get_random_state_tree_info() + .map_err(CreateAccountsProofError::Rpc)?; + + // Pack system accounts with empty proof + let packed = pack_proof( + program_id, + ValidityProofWithContext::default(), + &state_tree_info, + None, // No CPI context needed for token-only + )?; + + return Ok(CreateAccountsProofResult { + create_accounts_proof: CreateAccountsProof { + proof: ValidityProof::default(), + address_tree_info: PackedAddressTreeInfo::default(), + output_state_tree_index: packed.output_tree_index, + }, + remaining_accounts: packed.remaining_accounts, + }); } // 1. Get address tree (opinionated: always V2) diff --git a/sdk-libs/macros/docs/CLAUDE.md b/sdk-libs/macros/docs/CLAUDE.md index b31ef3ca7e..b6f8348a24 100644 --- a/sdk-libs/macros/docs/CLAUDE.md +++ b/sdk-libs/macros/docs/CLAUDE.md @@ -14,8 +14,19 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros | **`rentfree_program/`** | `#[rentfree_program]` attribute macro | | **`rentfree_program/architecture.md`** | Architecture overview, usage, generated items | | **`rentfree_program/codegen.md`** | Technical implementation details (code generation) | +| **`accounts/`** | Field-level attributes for Accounts structs | | **`account/`** | Trait derive macros for account data structs | +### Accounts Field Attributes + +Field-level attributes applied inside `#[derive(RentFree)]` Accounts structs: + +| File | Attribute | Description | +|------|-----------|-------------| +| **`accounts/light_mint.md`** | `#[light_mint(...)]` | Creates compressed mint with automatic decompression | + +See also: `#[rentfree]` attribute documented in `rentfree.md` + ### Account Trait Documentation | File | Macro | Description | diff --git a/sdk-libs/macros/docs/accounts/light_mint.md b/sdk-libs/macros/docs/accounts/light_mint.md new file mode 100644 index 0000000000..ef40bffab0 --- /dev/null +++ b/sdk-libs/macros/docs/accounts/light_mint.md @@ -0,0 +1,269 @@ +# `#[light_mint(...)]` Attribute + +## Overview + +The `#[light_mint(...)]` attribute marks a field in an Anchor Accounts struct for compressed mint creation. When applied to a `CMint` account field, it generates code to create a compressed mint with automatic decompression support. + +**Source**: `sdk-libs/macros/src/rentfree/accounts/light_mint.rs` + +## Usage + +```rust +use light_sdk_macros::RentFree; +use anchor_lang::prelude::*; + +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Unchecked account for PDA signer + #[account(seeds = [b"mint_signer"], bump)] + pub mint_signer: AccountInfo<'info>, + + pub authority: Signer<'info>, + + /// The CMint account to create + #[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[b"mint_signer", &[ctx.bumps.mint_signer]] + )] + pub cmint: Account<'info, CMint>, + + // Infrastructure accounts (auto-detected by name) + pub ctoken_compressible_config: Account<'info, CtokenConfig>, + pub ctoken_rent_sponsor: Account<'info, CtokenRentSponsor>, + pub light_token_program: Program<'info, LightTokenProgram>, + pub ctoken_cpi_authority: AccountInfo<'info>, +} +``` + +## Required Attributes + +| Attribute | Type | Description | +|-----------|------|-------------| +| `mint_signer` | Field reference | The AccountInfo that seeds the mint PDA. The mint address is derived from this signer. | +| `authority` | Field reference | The mint authority. Either a transaction signer or a PDA (if `authority_seeds` is provided). | +| `decimals` | Expression | Token decimals (e.g., `9` for 9 decimal places). | +| `mint_seeds` | Slice expression | PDA signer seeds for `mint_signer`. Must be a `&[&[u8]]` expression that matches the `#[account(seeds = ...)]` on `mint_signer`, **including the bump**. | + +## Optional Attributes + +| Attribute | Type | Default | Description | +|-----------|------|---------|-------------| +| `address_tree_info` | Expression | `params.create_accounts_proof.address_tree_info` | `PackedAddressTreeInfo` containing tree indices. | +| `freeze_authority` | Field reference | None | Optional freeze authority field. | +| `authority_seeds` | Slice expression | None | PDA signer seeds for `authority`. If not provided, `authority` must be a transaction signer. | +| `rent_payment` | Expression | `2u8` | Rent payment epochs for decompression. | +| `write_top_up` | Expression | `0u32` | Write top-up lamports for decompression. | + +## How It Works + +### Mint PDA Derivation + +The mint address is derived from the `mint_signer` field: + +```rust +let (mint_pda, bump) = light_token_sdk::token::find_mint_address(mint_signer.key); +``` + +### Signer Seeds (mint_seeds) + +The `mint_seeds` attribute provides the PDA signer seeds used for `invoke_signed` when calling the light token program. These seeds must derive to the `mint_signer` pubkey for the CPI to succeed. + +```rust +#[light_mint( + mint_signer = mint_signer, + authority = mint_authority, + decimals = 9, + mint_seeds = &[LP_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] +)] +pub cmint: UncheckedAccount<'info>, +``` + +**Syntax notes:** +- Use `self.field` to reference accounts in the struct +- Use `.to_account_info().key` to get account pubkeys +- The bump must be passed explicitly (typically via instruction params) + +The generated code uses these seeds to sign the CPI: + +```rust +let mint_seeds: &[&[u8]] = &[...]; // from mint_seeds attribute +invoke_signed(&mint_action_ix, &account_infos, &[mint_seeds])?; +``` + +### Generated Code Flow + +1. **Resolve tree accounts** - Get address tree and output queue from CPI accounts +2. **Derive mint PDA** - Calculate mint address from `mint_signer` +3. **Extract proof** - Get compression proof from instruction params +4. **Build mint instruction data** - Create `MintInstructionData` with metadata +5. **Configure decompression** - Set `rent_payment` and `write_top_up` for decompression +6. **Build account metas** - Configure CPI accounts for mint_action +7. **Invoke CPI** - Call light_token_program with signer seeds + +### CPI Context Integration + +When used alongside `#[rentfree]` PDAs, the mint is batched with PDA compression in a single CPI context. The mint receives an `assigned_account_index` to order it relative to PDAs. + +## Examples + +### Basic Mint Creation + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateBasicMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Mint signer PDA + #[account(seeds = [b"mint"], bump)] + pub mint_signer: AccountInfo<'info>, + + pub authority: Signer<'info>, + + #[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 6, + mint_seeds = &[b"mint", &[ctx.bumps.mint_signer]] + )] + pub cmint: Account<'info, CMint>, + + // ... infrastructure accounts +} +``` + +### Mint with PDA Authority + +When the authority is a PDA, provide `authority_seeds`: + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateMintWithPdaAuthority<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Mint signer PDA + #[account(seeds = [b"mint"], bump)] + pub mint_signer: AccountInfo<'info>, + + /// CHECK: Authority PDA (not a signer) + #[account(seeds = [b"authority"], bump)] + pub authority: AccountInfo<'info>, + + #[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[b"mint", &[ctx.bumps.mint_signer]], + authority_seeds = &[b"authority", &[ctx.bumps.authority]] + )] + pub cmint: Account<'info, CMint>, + + // ... infrastructure accounts +} +``` + +### Mint with Freeze Authority + +```rust +#[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[b"mint", &[bump]], + freeze_authority = freeze_auth +)] +pub cmint: Account<'info, CMint>, + +/// Optional freeze authority +pub freeze_auth: Signer<'info>, +``` + +### Custom Decompression Settings + +```rust +#[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[b"mint", &[bump]], + rent_payment = 4, // 4 epochs of rent + write_top_up = 1000 // Extra lamports for writes +)] +pub cmint: Account<'info, CMint>, +``` + +### Combined with #[rentfree] PDAs + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateMintAndPda<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + /// CHECK: Mint signer + #[account(seeds = [b"mint"], bump)] + pub mint_signer: AccountInfo<'info>, + + pub authority: Signer<'info>, + + #[light_mint( + mint_signer = mint_signer, + authority = authority, + decimals = 9, + mint_seeds = &[b"mint", &[ctx.bumps.mint_signer]] + )] + pub cmint: Account<'info, CMint>, + + #[account( + init, + payer = fee_payer, + space = 8 + TokenAccount::INIT_SPACE, + seeds = [b"token", params.owner.as_ref()], + bump + )] + #[rentfree] + pub token_account: Account<'info, TokenAccount>, + + // ... infrastructure accounts +} +``` + +When both `#[light_mint]` and `#[rentfree]` are present, the macro: +1. Processes PDAs first, writing them to the CPI context +2. Invokes mint_action with CPI context to batch the mint creation +3. Uses `assigned_account_index` to order the mint relative to PDAs + +## Infrastructure Accounts + +The macro requires certain infrastructure accounts, auto-detected by naming convention: + +| Account Type | Accepted Names | +|--------------|----------------| +| Fee Payer | `fee_payer`, `payer`, `creator` | +| CToken Config | `ctoken_compressible_config`, `ctoken_config`, `light_token_config_account` | +| CToken Rent Sponsor | `ctoken_rent_sponsor`, `light_token_rent_sponsor` | +| CToken Program | `ctoken_program`, `light_token_program` | +| CToken CPI Authority | `ctoken_cpi_authority`, `light_token_program_cpi_authority`, `compress_token_program_cpi_authority` | + +## Validation + +The macro validates at compile time: +- `mint_signer`, `authority`, `decimals`, and `mint_seeds` are required +- `#[instruction(...)]` attribute must be present on the struct +- If `authority_seeds` is not provided, the generated code verifies `authority` is a transaction signer at runtime + +## Related Documentation + +- **`../rentfree.md`** - Full RentFree derive macro documentation +- **`../rentfree_program/`** - Program-level `#[rentfree_program]` macro +- **`../account/`** - Trait derives for data structs diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index bffe842e21..34684e3107 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -2151,20 +2151,16 @@ impl TestIndexer { { let compressed_accounts = hashes; if compressed_accounts.is_some() - && ![1usize, 2usize, 3usize, 4usize, 8usize] - .contains(&compressed_accounts.as_ref().unwrap().len()) + && compressed_accounts.as_ref().unwrap().len() > 8 { return Err(IndexerError::CustomError(format!( - "compressed_accounts must be of length 1, 2, 3, 4 or 8 != {}", + "compressed_accounts must be of length <= 8, got {}", compressed_accounts.unwrap().len() ))); } - if new_addresses.is_some() - && ![1usize, 2usize, 3usize, 4usize, 8usize] - .contains(&new_addresses.as_ref().unwrap().len()) - { + if new_addresses.is_some() && new_addresses.as_ref().unwrap().len() > 8 { return Err(IndexerError::CustomError(format!( - "new_addresses must be of length 1, 2, 3, 4 or 8 != {}", + "new_addresses must be of length <= 8, got {}", new_addresses.unwrap().len() ))); } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/d7_infra_names.rs b/sdk-tests/csdk-anchor-full-derived-test/src/d7_infra_names.rs new file mode 100644 index 0000000000..a3b207fd50 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/d7_infra_names.rs @@ -0,0 +1,3 @@ +//! Re-export d7_infra_names from instructions module for top-level access. + +pub use crate::instructions::d7_infra_names::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs index 6c76bb2e1c..274bd23148 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/all.rs @@ -64,5 +64,11 @@ pub struct D5AllMarkers<'info> { #[account(mut, address = CTOKEN_RENT_SPONSOR)] 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>, } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs index 3519da15b2..34c35968f7 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_token.rs @@ -47,5 +47,11 @@ pub struct D5RentfreeToken<'info> { #[account(mut, address = CTOKEN_RENT_SPONSOR)] 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>, } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs index dfdf8b3666..9c6038e80d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/all.rs @@ -64,5 +64,11 @@ pub struct D7AllNames<'info> { #[account(mut, address = CTOKEN_RENT_SPONSOR)] 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>, } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs index 14fff8b279..b4c8a6dac1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d7_infra_names/ctoken_config.rs @@ -45,5 +45,11 @@ pub struct D7CtokenConfig<'info> { #[account(mut, address = CTOKEN_RENT_SPONSOR)] 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>, } 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 52817df988..c5434cb731 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -8,6 +8,7 @@ use light_sdk_types::CpiSigner; pub mod amm_test; pub mod d5_markers; pub mod d6_account_types; +pub mod d7_infra_names; pub mod d8_builder_paths; pub mod d9_seeds; pub mod errors; @@ -18,6 +19,7 @@ pub mod state; pub use amm_test::*; pub use d5_markers::*; pub use d6_account_types::*; +pub use d7_infra_names::*; pub use d8_builder_paths::*; pub use d9_seeds::*; pub use instruction_accounts::*; @@ -61,15 +63,22 @@ pub mod csdk_anchor_full_derived_test { use super::{ amm_test::{Deposit, InitializeParams, InitializePool, Withdraw}, - d5_markers::{D5RentfreeBare, D5RentfreeBareParams}, + d5_markers::{ + D5AllMarkers, D5AllMarkersParams, D5RentfreeBare, D5RentfreeBareParams, + D5RentfreeToken, D5RentfreeTokenParams, + }, d6_account_types::{D6Account, D6AccountParams, D6Boxed, D6BoxedParams}, + d7_infra_names::{ + D7AllNames, D7AllNamesParams, D7Creator, D7CreatorParams, D7CtokenConfig, + D7CtokenConfigParams, D7Payer, D7PayerParams, + }, d8_builder_paths::{ D8All, D8AllParams, D8MultiRentfree, D8MultiRentfreeParams, D8PdaOnly, D8PdaOnlyParams, }, d9_seeds::{ - D9Constant, D9ConstantParams, D9CtxAccount, D9CtxAccountParams, D9Literal, - D9LiteralParams, D9Mixed, D9MixedParams, D9Param, D9ParamBytes, D9ParamBytesParams, - D9ParamParams, + D9All, D9AllParams, D9Constant, D9ConstantParams, D9CtxAccount, D9CtxAccountParams, + D9FunctionCall, D9FunctionCallParams, D9Literal, D9LiteralParams, D9Mixed, + D9MixedParams, D9Param, D9ParamBytes, D9ParamBytesParams, D9ParamParams, }, instruction_accounts::CreatePdasAndMintAuto, FullAutoWithMintParams, LIGHT_CPI_SIGNER, @@ -300,4 +309,195 @@ pub mod csdk_anchor_full_derived_test { ctx.accounts.d9_mixed_record.owner = params.owner; Ok(()) } + + // ========================================================================= + // D7 Infrastructure Names: Field naming convention tests + // ========================================================================= + + /// D7: "payer" field name variant (instead of fee_payer) + pub fn d7_payer<'info>( + ctx: Context<'_, '_, '_, 'info, D7Payer<'info>>, + params: D7PayerParams, + ) -> Result<()> { + ctx.accounts.d7_payer_record.owner = params.owner; + Ok(()) + } + + /// D7: "creator" field name variant (instead of fee_payer) + pub fn d7_creator<'info>( + ctx: Context<'_, '_, '_, 'info, D7Creator<'info>>, + params: D7CreatorParams, + ) -> Result<()> { + ctx.accounts.d7_creator_record.owner = params.owner; + Ok(()) + } + + /// D7: "ctoken_config" naming variant for token accounts + pub fn d7_ctoken_config<'info>( + ctx: Context<'_, '_, '_, 'info, D7CtokenConfig<'info>>, + _params: D7CtokenConfigParams, + ) -> Result<()> { + use light_token_sdk::token::CreateTokenAccountCpi; + + let mint_key = ctx.accounts.mint.key(); + // Derive the vault bump at runtime + let (_, vault_bump) = Pubkey::find_program_address( + &[crate::d7_infra_names::D7_CTOKEN_VAULT_SEED, mint_key.as_ref()], + &crate::ID, + ); + + CreateTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.d7_ctoken_vault.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + owner: ctx.accounts.d7_ctoken_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, + ) + .invoke_signed(&[ + crate::d7_infra_names::D7_CTOKEN_VAULT_SEED, + mint_key.as_ref(), + &[vault_bump], + ])?; + Ok(()) + } + + /// D7: All naming variants combined (payer + ctoken config/sponsor) + pub fn d7_all_names<'info>( + ctx: Context<'_, '_, '_, 'info, D7AllNames<'info>>, + params: D7AllNamesParams, + ) -> Result<()> { + use light_token_sdk::token::CreateTokenAccountCpi; + + // Set up the PDA record + ctx.accounts.d7_all_record.owner = params.owner; + + // Create token vault + let mint_key = ctx.accounts.mint.key(); + // Derive the vault bump at runtime + let (_, vault_bump) = Pubkey::find_program_address( + &[crate::d7_infra_names::D7_ALL_VAULT_SEED, mint_key.as_ref()], + &crate::ID, + ); + + CreateTokenAccountCpi { + payer: ctx.accounts.payer.to_account_info(), + account: ctx.accounts.d7_all_vault.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + owner: ctx.accounts.d7_all_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, + ) + .invoke_signed(&[ + crate::d7_infra_names::D7_ALL_VAULT_SEED, + mint_key.as_ref(), + &[vault_bump], + ])?; + Ok(()) + } + + // ========================================================================= + // D9 Additional Seeds Tests + // ========================================================================= + + /// D9: Function call seed expression + pub fn d9_function_call<'info>( + ctx: Context<'_, '_, '_, 'info, D9FunctionCall<'info>>, + params: D9FunctionCallParams, + ) -> Result<()> { + ctx.accounts.d9_func_record.owner = params.key_a; + Ok(()) + } + + /// D9: All seed expression types (6 PDAs) + pub fn d9_all<'info>( + ctx: Context<'_, '_, '_, 'info, D9All<'info>>, + params: D9AllParams, + ) -> Result<()> { + ctx.accounts.d9_all_lit.owner = params.owner; + ctx.accounts.d9_all_const.owner = params.owner; + ctx.accounts.d9_all_ctx.owner = params.owner; + ctx.accounts.d9_all_param.owner = params.owner; + ctx.accounts.d9_all_bytes.owner = params.owner; + ctx.accounts.d9_all_func.owner = params.owner; + Ok(()) + } + + // ========================================================================= + // D5 Additional Markers Tests + // ========================================================================= + + /// D5: #[rentfree_token] attribute test + pub fn d5_rentfree_token<'info>( + ctx: Context<'_, '_, '_, 'info, D5RentfreeToken<'info>>, + params: D5RentfreeTokenParams, + ) -> Result<()> { + use light_token_sdk::token::CreateTokenAccountCpi; + + let mint_key = ctx.accounts.mint.key(); + CreateTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.d5_token_vault.to_account_info(), + mint: ctx.accounts.mint.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, + ) + .invoke_signed(&[ + crate::d5_markers::D5_VAULT_SEED, + mint_key.as_ref(), + &[params.vault_bump], + ])?; + Ok(()) + } + + /// D5: All markers combined (#[rentfree] + #[rentfree_token]) + pub fn d5_all_markers<'info>( + ctx: Context<'_, '_, '_, 'info, D5AllMarkers<'info>>, + params: D5AllMarkersParams, + ) -> Result<()> { + use light_token_sdk::token::CreateTokenAccountCpi; + + // Set up the PDA record + ctx.accounts.d5_all_record.owner = params.owner; + + // Create token vault + let mint_key = ctx.accounts.mint.key(); + // Derive the vault bump at runtime + let (_, vault_bump) = Pubkey::find_program_address( + &[crate::d5_markers::D5_ALL_VAULT_SEED, mint_key.as_ref()], + &crate::ID, + ); + + CreateTokenAccountCpi { + payer: ctx.accounts.fee_payer.to_account_info(), + account: ctx.accounts.d5_all_vault.to_account_info(), + mint: ctx.accounts.mint.to_account_info(), + owner: ctx.accounts.d5_all_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, + ) + .invoke_signed(&[ + crate::d5_markers::D5_ALL_VAULT_SEED, + mint_key.as_ref(), + &[vault_bump], + ])?; + Ok(()) + } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs index a3df565536..43ebbfe23c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs @@ -3,6 +3,8 @@ //! These tests verify that the macro-generated code works correctly at runtime //! by testing the full lifecycle: create account -> verify on-chain -> compress -> decompress. +mod shared; + use anchor_lang::{InstructionData, ToAccountMetas}; use light_compressible_client::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, @@ -91,6 +93,20 @@ impl TestContext { assert_eq!(acc.address.unwrap(), addr); assert!(!acc.data.as_ref().unwrap().data.is_empty()); } + + /// Setup a mint for token-based tests. + /// Returns (mint_pubkey, compression_address, ata_pubkeys, mint_seed_keypair) + #[allow(dead_code)] + async fn setup_mint(&mut self) -> (Pubkey, [u8; 32], Vec, Keypair) { + shared::setup_create_mint( + &mut self.rpc, + &self.payer, + self.payer.pubkey(), // mint_authority + 9, // decimals + vec![], // no recipients initially + ) + .await + } } // ============================================================================= @@ -100,16 +116,13 @@ impl TestContext { /// Tests D6Account: Direct Account<'info, T> type #[tokio::test] async fn test_d6_account() { - use csdk_anchor_full_derived_test::d6_account_types::{D6AccountParams}; + use csdk_anchor_full_derived_test::d6_account_types::D6AccountParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); // Derive PDA - let (pda, _) = Pubkey::find_program_address( - &[b"d6_account", owner.as_ref()], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[b"d6_account", owner.as_ref()], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -157,16 +170,13 @@ async fn test_d6_account() { /// Tests D6Boxed: Box> type #[tokio::test] async fn test_d6_boxed() { - use csdk_anchor_full_derived_test::d6_account_types::{D6BoxedParams}; + use csdk_anchor_full_derived_test::d6_account_types::D6BoxedParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); // Derive PDA - let (pda, _) = Pubkey::find_program_address( - &[b"d6_boxed", owner.as_ref()], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[b"d6_boxed", owner.as_ref()], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -218,16 +228,13 @@ async fn test_d6_boxed() { /// Tests D8PdaOnly: Only #[rentfree] fields (no token accounts) #[tokio::test] async fn test_d8_pda_only() { - use csdk_anchor_full_derived_test::d8_builder_paths::{D8PdaOnlyParams}; + use csdk_anchor_full_derived_test::d8_builder_paths::D8PdaOnlyParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); // Derive PDA - let (pda, _) = Pubkey::find_program_address( - &[b"d8_pda_only", owner.as_ref()], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[b"d8_pda_only", owner.as_ref()], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -275,7 +282,7 @@ async fn test_d8_pda_only() { /// Tests D8MultiRentfree: Multiple #[rentfree] fields of same type #[tokio::test] async fn test_d8_multi_rentfree() { - use csdk_anchor_full_derived_test::d8_builder_paths::{D8MultiRentfreeParams}; + use csdk_anchor_full_derived_test::d8_builder_paths::D8MultiRentfreeParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); @@ -345,20 +352,16 @@ async fn test_d8_multi_rentfree() { /// Tests D8All: Multiple #[rentfree] fields of different types #[tokio::test] async fn test_d8_all() { - use csdk_anchor_full_derived_test::d8_builder_paths::{D8AllParams}; + use csdk_anchor_full_derived_test::d8_builder_paths::D8AllParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); // Derive PDAs - let (pda_single, _) = Pubkey::find_program_address( - &[b"d8_all_single", owner.as_ref()], - &ctx.program_id, - ); - let (pda_multi, _) = Pubkey::find_program_address( - &[b"d8_all_multi", owner.as_ref()], - &ctx.program_id, - ); + let (pda_single, _) = + Pubkey::find_program_address(&[b"d8_all_single", owner.as_ref()], &ctx.program_id); + let (pda_multi, _) = + Pubkey::find_program_address(&[b"d8_all_multi", owner.as_ref()], &ctx.program_id); // Get proof for both PDAs let proof_result = get_create_accounts_proof( @@ -415,15 +418,12 @@ async fn test_d8_all() { /// Tests D9Literal: Literal seed expression #[tokio::test] async fn test_d9_literal() { - use csdk_anchor_full_derived_test::d9_seeds::{D9LiteralParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9LiteralParams; let mut ctx = TestContext::new().await; // Derive PDA (literal seeds only) - let (pda, _) = Pubkey::find_program_address( - &[b"d9_literal_record"], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[b"d9_literal_record"], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -470,16 +470,13 @@ async fn test_d9_literal() { /// Tests D9Constant: Constant seed expression #[tokio::test] async fn test_d9_constant() { - use csdk_anchor_full_derived_test::d9_seeds::{D9ConstantParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9ConstantParams; use csdk_anchor_full_derived_test::D9_CONSTANT_SEED; let mut ctx = TestContext::new().await; // Derive PDA using constant - let (pda, _) = Pubkey::find_program_address( - &[D9_CONSTANT_SEED], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[D9_CONSTANT_SEED], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -526,16 +523,14 @@ async fn test_d9_constant() { /// Tests D9CtxAccount: Context account seed expression #[tokio::test] async fn test_d9_ctx_account() { - use csdk_anchor_full_derived_test::d9_seeds::{D9CtxAccountParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9CtxAccountParams; let mut ctx = TestContext::new().await; let authority = Keypair::new(); // Derive PDA using authority key - let (pda, _) = Pubkey::find_program_address( - &[b"d9_ctx", authority.pubkey().as_ref()], - &ctx.program_id, - ); + let (pda, _) = + Pubkey::find_program_address(&[b"d9_ctx", authority.pubkey().as_ref()], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -583,16 +578,13 @@ async fn test_d9_ctx_account() { /// Tests D9Param: Param seed expression (Pubkey) #[tokio::test] async fn test_d9_param() { - use csdk_anchor_full_derived_test::d9_seeds::{D9ParamParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9ParamParams; let mut ctx = TestContext::new().await; let owner = Keypair::new().pubkey(); // Derive PDA using param - let (pda, _) = Pubkey::find_program_address( - &[b"d9_param", owner.as_ref()], - &ctx.program_id, - ); + let (pda, _) = Pubkey::find_program_address(&[b"d9_param", owner.as_ref()], &ctx.program_id); // Get proof let proof_result = get_create_accounts_proof( @@ -640,7 +632,7 @@ async fn test_d9_param() { /// Tests D9ParamBytes: Param bytes seed expression (u64) #[tokio::test] async fn test_d9_param_bytes() { - use csdk_anchor_full_derived_test::d9_seeds::{D9ParamBytesParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9ParamBytesParams; let mut ctx = TestContext::new().await; let id = 12345u64; @@ -697,7 +689,7 @@ async fn test_d9_param_bytes() { /// Tests D9Mixed: Mixed seed expression types #[tokio::test] async fn test_d9_mixed() { - use csdk_anchor_full_derived_test::d9_seeds::{D9MixedParams}; + use csdk_anchor_full_derived_test::d9_seeds::D9MixedParams; let mut ctx = TestContext::new().await; let authority = Keypair::new(); @@ -753,6 +745,270 @@ async fn test_d9_mixed() { ctx.assert_onchain_exists(&pda).await; } +// ============================================================================= +// D7 Infrastructure Names Tests +// ============================================================================= + +/// Tests D7Payer: "payer" field name variant +#[tokio::test] +async fn test_d7_payer() { + use csdk_anchor_full_derived_test::d7_infra_names::D7PayerParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[b"d7_payer", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D7Payer { + payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d7_payer_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D7Payer { + params: D7PayerParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D7Payer instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D7Creator: "creator" field name variant +#[tokio::test] +async fn test_d7_creator() { + use csdk_anchor_full_derived_test::d7_infra_names::D7CreatorParams; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Derive PDA + let (pda, _) = Pubkey::find_program_address(&[b"d7_creator", owner.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D7Creator { + creator: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d7_creator_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D7Creator { + params: D7CreatorParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D7Creator instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +// ============================================================================= +// D9 Additional Seeds Tests +// ============================================================================= + +/// Tests D9FunctionCall: Function call seed expression +#[tokio::test] +async fn test_d9_function_call() { + use csdk_anchor_full_derived_test::d9_seeds::D9FunctionCallParams; + + let mut ctx = TestContext::new().await; + let key_a = Keypair::new().pubkey(); + let key_b = Keypair::new().pubkey(); + + // Derive PDA using max_key (same as in instruction) + let max_key = csdk_anchor_full_derived_test::max_key(&key_a, &key_b); + let (pda, _) = Pubkey::find_program_address(&[b"d9_func", max_key.as_ref()], &ctx.program_id); + + // Get proof + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(pda)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9FunctionCall { + fee_payer: ctx.payer.pubkey(), + compression_config: ctx.config_pda, + d9_func_record: pda, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9FunctionCall { + params: D9FunctionCallParams { + create_accounts_proof: proof_result.create_accounts_proof, + key_a, + key_b, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9FunctionCall instruction should succeed"); + + ctx.assert_onchain_exists(&pda).await; +} + +/// Tests D9All: All 6 seed expression types +#[tokio::test] +async fn test_d9_all() { + use csdk_anchor_full_derived_test::d9_seeds::D9AllParams; + use csdk_anchor_full_derived_test::D9_ALL_SEED; + + let mut ctx = TestContext::new().await; + let authority = Keypair::new(); + let owner = Keypair::new().pubkey(); + let id = 42u64; + let key_a = Keypair::new().pubkey(); + let key_b = Keypair::new().pubkey(); + + // Derive all 6 PDAs + let (pda_lit, _) = Pubkey::find_program_address(&[b"d9_all_lit"], &ctx.program_id); + let (pda_const, _) = Pubkey::find_program_address(&[D9_ALL_SEED], &ctx.program_id); + let (pda_ctx, _) = Pubkey::find_program_address( + &[b"d9_all_ctx", authority.pubkey().as_ref()], + &ctx.program_id, + ); + let (pda_param, _) = + Pubkey::find_program_address(&[b"d9_all_param", owner.as_ref()], &ctx.program_id); + let (pda_bytes, _) = Pubkey::find_program_address( + &[b"d9_all_bytes", id.to_le_bytes().as_ref()], + &ctx.program_id, + ); + let max_key = csdk_anchor_full_derived_test::max_key(&key_a, &key_b); + let (pda_func, _) = + Pubkey::find_program_address(&[b"d9_all_func", max_key.as_ref()], &ctx.program_id); + + // Get proof for all 6 PDAs + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![ + CreateAccountsProofInput::pda(pda_lit), + CreateAccountsProofInput::pda(pda_const), + CreateAccountsProofInput::pda(pda_ctx), + CreateAccountsProofInput::pda(pda_param), + CreateAccountsProofInput::pda(pda_bytes), + CreateAccountsProofInput::pda(pda_func), + ], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D9All { + fee_payer: ctx.payer.pubkey(), + authority: authority.pubkey(), + compression_config: ctx.config_pda, + d9_all_lit: pda_lit, + d9_all_const: pda_const, + d9_all_ctx: pda_ctx, + d9_all_param: pda_param, + d9_all_bytes: pda_bytes, + d9_all_func: pda_func, + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D9All { + params: D9AllParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + id, + key_a, + key_b, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D9All instruction should succeed"); + + // Verify all 6 accounts exist + ctx.assert_onchain_exists(&pda_lit).await; + ctx.assert_onchain_exists(&pda_const).await; + ctx.assert_onchain_exists(&pda_ctx).await; + ctx.assert_onchain_exists(&pda_param).await; + ctx.assert_onchain_exists(&pda_bytes).await; + ctx.assert_onchain_exists(&pda_func).await; +} + // ============================================================================= // Full Lifecycle Test (compression + decompression) // ============================================================================= @@ -871,3 +1127,286 @@ async fn test_d8_pda_only_full_lifecycle() { // PHASE 4: Verify account is back on-chain ctx.assert_onchain_exists(&pda).await; } + +// ============================================================================= +// D5 Markers Token Tests (require mint setup) +// ============================================================================= + +/// Tests D5RentfreeToken: #[rentfree_token] attribute +/// NOTE: This test is skipped because token-only instructions (no #[rentfree] PDAs) +/// still require a CreateAccountsProof but get_create_accounts_proof fails with empty inputs. +#[tokio::test] +async fn test_d5_rentfree_token() { + use csdk_anchor_full_derived_test::d5_markers::{ + D5RentfreeTokenParams, D5_VAULT_AUTH_SEED, D5_VAULT_SEED, + }; + use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + + let mut ctx = TestContext::new().await; + + // Setup mint + let (mint, _compression_addr, _atas, _mint_seed) = ctx.setup_mint().await; + + // Derive PDAs + let (vault_authority, _) = Pubkey::find_program_address(&[D5_VAULT_AUTH_SEED], &ctx.program_id); + let (vault, vault_bump) = + Pubkey::find_program_address(&[D5_VAULT_SEED, mint.as_ref()], &ctx.program_id); + + // Get proof (no PDA accounts for token-only instruction) + let proof_result = get_create_accounts_proof(&ctx.rpc, &ctx.program_id, vec![]) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D5RentfreeToken { + fee_payer: ctx.payer.pubkey(), + mint, + vault_authority, + d5_token_vault: vault, + 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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D5RentfreeToken { + params: D5RentfreeTokenParams { + create_accounts_proof: proof_result.create_accounts_proof, + vault_bump, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D5RentfreeToken instruction should succeed"); + + // Verify token vault exists + ctx.assert_onchain_exists(&vault).await; +} + +/// Tests D5AllMarkers: #[rentfree] + #[rentfree_token] combined +#[tokio::test] +async fn test_d5_all_markers() { + use csdk_anchor_full_derived_test::d5_markers::{ + D5AllMarkersParams, D5_ALL_AUTH_SEED, D5_ALL_VAULT_SEED, + }; + use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Setup mint + let (mint, _compression_addr, _atas, _mint_seed) = ctx.setup_mint().await; + + // Derive PDAs + let (d5_all_authority, _) = Pubkey::find_program_address(&[D5_ALL_AUTH_SEED], &ctx.program_id); + let (d5_all_record, _) = + Pubkey::find_program_address(&[b"d5_all_record", owner.as_ref()], &ctx.program_id); + let (d5_all_vault, _) = + Pubkey::find_program_address(&[D5_ALL_VAULT_SEED, mint.as_ref()], &ctx.program_id); + + // Get proof for PDA record + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(d5_all_record)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D5AllMarkers { + fee_payer: ctx.payer.pubkey(), + mint, + compression_config: ctx.config_pda, + d5_all_authority, + d5_all_record, + d5_all_vault, + 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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D5AllMarkers { + params: D5AllMarkersParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D5AllMarkers instruction should succeed"); + + // Verify both PDA record and token vault exist + ctx.assert_onchain_exists(&d5_all_record).await; + ctx.assert_onchain_exists(&d5_all_vault).await; +} + +// ============================================================================= +// D7 Infrastructure Names Token Tests (require mint setup) +// ============================================================================= + +/// Tests D7CtokenConfig: ctoken_compressible_config/ctoken_rent_sponsor naming +/// Token-only instruction (no #[rentfree] PDAs) - verifies infrastructure field naming. +#[tokio::test] +async fn test_d7_ctoken_config() { + use csdk_anchor_full_derived_test::d7_infra_names::{ + D7CtokenConfigParams, D7_CTOKEN_AUTH_SEED, D7_CTOKEN_VAULT_SEED, + }; + use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + + let mut ctx = TestContext::new().await; + + // Setup mint + let (mint, _compression_addr, _atas, _mint_seed) = ctx.setup_mint().await; + + // Derive PDAs + let (d7_ctoken_authority, _) = + Pubkey::find_program_address(&[D7_CTOKEN_AUTH_SEED], &ctx.program_id); + let (d7_ctoken_vault, _) = + Pubkey::find_program_address(&[D7_CTOKEN_VAULT_SEED, mint.as_ref()], &ctx.program_id); + + // Get proof (no PDA accounts for token-only instruction) + let proof_result = get_create_accounts_proof(&ctx.rpc, &ctx.program_id, vec![]) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D7CtokenConfig { + fee_payer: ctx.payer.pubkey(), + mint, + d7_ctoken_authority, + d7_ctoken_vault, + 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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D7CtokenConfig { + _params: D7CtokenConfigParams { + create_accounts_proof: proof_result.create_accounts_proof, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D7CtokenConfig instruction should succeed"); + + // Verify token vault exists + ctx.assert_onchain_exists(&d7_ctoken_vault).await; +} + +/// Tests D7AllNames: payer + ctoken_config/rent_sponsor naming combined +#[tokio::test] +async fn test_d7_all_names() { + use csdk_anchor_full_derived_test::d7_infra_names::{ + D7AllNamesParams, D7_ALL_AUTH_SEED, D7_ALL_VAULT_SEED, + }; + use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; + use light_token_sdk::token::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR as CTOKEN_RENT_SPONSOR}; + + let mut ctx = TestContext::new().await; + let owner = Keypair::new().pubkey(); + + // Setup mint + let (mint, _compression_addr, _atas, _mint_seed) = ctx.setup_mint().await; + + // Derive PDAs + let (d7_all_authority, _) = Pubkey::find_program_address(&[D7_ALL_AUTH_SEED], &ctx.program_id); + let (d7_all_record, _) = + Pubkey::find_program_address(&[b"d7_all_record", owner.as_ref()], &ctx.program_id); + let (d7_all_vault, _) = + Pubkey::find_program_address(&[D7_ALL_VAULT_SEED, mint.as_ref()], &ctx.program_id); + + // Get proof for PDA record + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![CreateAccountsProofInput::pda(d7_all_record)], + ) + .await + .unwrap(); + + // Build instruction + let accounts = csdk_anchor_full_derived_test::accounts::D7AllNames { + payer: ctx.payer.pubkey(), + mint, + compression_config: ctx.config_pda, + d7_all_authority, + d7_all_record, + d7_all_vault, + 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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::D7AllNames { + params: D7AllNamesParams { + create_accounts_proof: proof_result.create_accounts_proof, + owner, + }, + }; + + let instruction = Instruction { + program_id: ctx.program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction(&[instruction], &ctx.payer.pubkey(), &[&ctx.payer]) + .await + .expect("D7AllNames instruction should succeed"); + + // Verify both PDA record and token vault exist + ctx.assert_onchain_exists(&d7_all_record).await; + ctx.assert_onchain_exists(&d7_all_vault).await; +} From aaccb717639eacae3f7efd1d523ab1ce2c3d68e3 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 02:44:30 +0000 Subject: [PATCH 06/11] format, use correct anchor spl branch --- Cargo.lock | 4 +-- sdk-libs/macros/src/lib.rs | 4 ++- .../macros/src/rentfree/accounts/parse.rs | 2 +- .../macros/src/rentfree/program/decompress.rs | 19 +++++++---- .../program-test/src/indexer/test_indexer.rs | 4 +-- .../csdk-anchor-full-derived-test/Cargo.toml | 2 +- .../src/instructions/d6_account_types/all.rs | 6 ++-- .../src/instructions/d8_builder_paths/all.rs | 6 ++-- .../csdk-anchor-full-derived-test/src/lib.rs | 13 ++++--- .../tests/amm_test.rs | 34 ++++++++++--------- .../tests/macro_test.rs | 11 +++--- 11 files changed, 60 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1e4ccaf0c6..46a10fedc5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -413,7 +413,7 @@ dependencies = [ [[package]] name = "anchor-spl" version = "0.31.1" -source = "git+https://github.com/lightprotocol/anchor?rev=4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3#4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3" +source = "git+https://github.com/lightprotocol/anchor?rev=da005d7f#da005d7f1f977d5220eaa65da26cdae2df0fe25e" dependencies = [ "anchor-lang", "mpl-token-metadata", @@ -1638,7 +1638,7 @@ name = "csdk-anchor-full-derived-test" version = "0.1.0" dependencies = [ "anchor-lang", - "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3)", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=da005d7f)", "bincode", "borsh 0.10.4", "light-client", diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index d7fa31bfd3..0d42596617 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -124,7 +124,9 @@ pub fn light_hasher_sha(input: TokenStream) -> TokenStream { #[proc_macro_derive(HasCompressionInfo)] pub fn has_compression_info(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - into_token_stream(rentfree::account::traits::derive_has_compression_info(input)) + into_token_stream(rentfree::account::traits::derive_has_compression_info( + input, + )) } /// Legacy CompressAs trait implementation (use Compressible instead). diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index 66e5b6afce..df0dd01142 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -9,9 +9,9 @@ use syn::{ // Import LightMintField and parsing from light_mint module use super::light_mint::{parse_light_mint_attr, LightMintField}; -use crate::rentfree::shared_utils::MetaExpr; // Import shared types pub(super) use crate::rentfree::account::seed_extraction::extract_account_inner_type; +use crate::rentfree::shared_utils::MetaExpr; // ============================================================================ // Infrastructure Field Classification diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index 32b8256524..13b2b74197 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -210,7 +210,8 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( let binding_name = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = transform_expr_for_ctx_seeds(expr, &ctx_field_names, state_field_names); + let mapped_expr = + transform_expr_for_ctx_seeds(expr, &ctx_field_names, state_field_names); bindings.push(quote! { let #binding_name = #mapped_expr; }); @@ -240,7 +241,10 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } /// Check if a seed expression is a params-only seed (data.field where field doesn't exist on state) -fn is_params_only_seed(expr: &syn::Expr, state_field_names: &std::collections::HashSet) -> bool { +fn is_params_only_seed( + expr: &syn::Expr, + state_field_names: &std::collections::HashSet, +) -> bool { use crate::rentfree::shared_utils::is_base_path; match expr { @@ -255,9 +259,7 @@ fn is_params_only_seed(expr: &syn::Expr, state_field_names: &std::collections::H syn::Expr::MethodCall(method_call) => { is_params_only_seed(&method_call.receiver, state_field_names) } - syn::Expr::Reference(ref_expr) => { - is_params_only_seed(&ref_expr.expr, state_field_names) - } + syn::Expr::Reference(ref_expr) => is_params_only_seed(&ref_expr.expr, state_field_names), _ => false, } } @@ -325,8 +327,11 @@ pub fn generate_pda_seed_provider_impls( } }; - let seed_derivation = - generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, ctx_fields, &ctx_info.state_field_names)?; + let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds( + spec, + ctx_fields, + &ctx_info.state_field_names, + )?; // Generate impl for inner_type, but use variant-based struct name results.push(quote! { diff --git a/sdk-libs/program-test/src/indexer/test_indexer.rs b/sdk-libs/program-test/src/indexer/test_indexer.rs index 34684e3107..8cbab69efc 100644 --- a/sdk-libs/program-test/src/indexer/test_indexer.rs +++ b/sdk-libs/program-test/src/indexer/test_indexer.rs @@ -2150,9 +2150,7 @@ impl TestIndexer { { let compressed_accounts = hashes; - if compressed_accounts.is_some() - && compressed_accounts.as_ref().unwrap().len() > 8 - { + if compressed_accounts.is_some() && compressed_accounts.as_ref().unwrap().len() > 8 { return Err(IndexerError::CustomError(format!( "compressed_accounts must be of length <= 8, got {}", compressed_accounts.unwrap().len() diff --git a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml index 0d734434b4..dfaf92b4de 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml @@ -33,7 +33,7 @@ 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 = "4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3", features = ["memo", "metadata", "idl-build"] } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "da005d7f", 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"] } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs index 2386d9bd7a..ea2be23ff1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d6_account_types/all.rs @@ -6,8 +6,10 @@ use anchor_lang::prelude::*; use light_compressible::CreateAccountsProof; use light_sdk_macros::RentFree; -use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; -use crate::state::d2_compress_as::multiple::MultipleCompressAsRecord; +use crate::state::{ + d1_field_types::single_pubkey::SinglePubkeyRecord, + d2_compress_as::multiple::MultipleCompressAsRecord, +}; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct D6AllParams { diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs index a217b49807..1bbdf87791 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d8_builder_paths/all.rs @@ -6,8 +6,10 @@ use anchor_lang::prelude::*; use light_compressible::CreateAccountsProof; use light_sdk_macros::RentFree; -use crate::state::d1_field_types::single_pubkey::SinglePubkeyRecord; -use crate::state::d2_compress_as::multiple::MultipleCompressAsRecord; +use crate::state::{ + d1_field_types::single_pubkey::SinglePubkeyRecord, + d2_compress_as::multiple::MultipleCompressAsRecord, +}; #[derive(AnchorSerialize, AnchorDeserialize, Clone)] pub struct D8AllParams { 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 c5434cb731..b95afc6998 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -23,10 +23,12 @@ pub use d7_infra_names::*; pub use d8_builder_paths::*; pub use d9_seeds::*; pub use instruction_accounts::*; -pub use instructions::d7_infra_names::{ - D7_ALL_AUTH_SEED, D7_ALL_VAULT_SEED, D7_CTOKEN_AUTH_SEED, D7_CTOKEN_VAULT_SEED, +pub use instructions::{ + d7_infra_names::{ + D7_ALL_AUTH_SEED, D7_ALL_VAULT_SEED, D7_CTOKEN_AUTH_SEED, D7_CTOKEN_VAULT_SEED, + }, + d9_seeds::{D9_ALL_SEED, D9_CONSTANT_SEED}, }; -pub use instructions::d9_seeds::{D9_ALL_SEED, D9_CONSTANT_SEED}; pub use state::{ d1_field_types::single_pubkey::{PackedSinglePubkeyRecord, SinglePubkeyRecord}, d2_compress_as::multiple::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}, @@ -342,7 +344,10 @@ pub mod csdk_anchor_full_derived_test { let mint_key = ctx.accounts.mint.key(); // Derive the vault bump at runtime let (_, vault_bump) = Pubkey::find_program_address( - &[crate::d7_infra_names::D7_CTOKEN_VAULT_SEED, mint_key.as_ref()], + &[ + crate::d7_infra_names::D7_CTOKEN_VAULT_SEED, + mint_key.as_ref(), + ], &crate::ID, ); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs index 788dcf906a..a257496817 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -10,13 +10,14 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; -use csdk_anchor_full_derived_test::amm_test::{ - InitializeParams, AUTH_SEED, OBSERVATION_SEED, POOL_LP_MINT_SIGNER_SEED, POOL_SEED, - POOL_VAULT_SEED, -}; -use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ - ObservationStateSeeds, PoolStateSeeds, TokenAccountVariant, +use csdk_anchor_full_derived_test::{ + amm_test::{ + InitializeParams, AUTH_SEED, OBSERVATION_SEED, POOL_LP_MINT_SIGNER_SEED, POOL_SEED, + POOL_VAULT_SEED, + }, + csdk_anchor_full_derived_test::{ObservationStateSeeds, PoolStateSeeds, TokenAccountVariant}, }; +use light_compressed_account::instruction_data::compressed_proof::ValidityProof; use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ create_load_accounts_instructions, get_create_accounts_proof, AccountInterface, @@ -28,13 +29,11 @@ use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, Indexer, ProgramTestConfig, Rpc, }; -use light_token_interface::state::Token; -use light_compressed_account::instruction_data::compressed_proof::ValidityProof; -use light_token_interface::instructions::mint_action::MintInstructionData; +use light_token_interface::{instructions::mint_action::MintInstructionData, state::Token}; use light_token_sdk::token::{ find_mint_address, get_associated_token_address_and_bump, CreateAssociatedTokenAccount, - Decompress, DecompressMint, MintWithContext, COMPRESSIBLE_CONFIG_V1, - LIGHT_TOKEN_CPI_AUTHORITY, LIGHT_TOKEN_PROGRAM_ID, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR, + Decompress, DecompressMint, MintWithContext, COMPRESSIBLE_CONFIG_V1, LIGHT_TOKEN_CPI_AUTHORITY, + LIGHT_TOKEN_PROGRAM_ID, RENT_SPONSOR as LIGHT_TOKEN_RENT_SPONSOR, }; use solana_instruction::Instruction; use solana_keypair::Keypair; @@ -789,11 +788,14 @@ async fn test_amm_full_lifecycle() { println!(" Decompressing creator LP token ATA..."); // First create the ATA (idempotent) - let create_ata_ix = - CreateAssociatedTokenAccount::new(ctx.payer.pubkey(), ctx.creator.pubkey(), pdas.lp_mint) - .idempotent() - .instruction() - .expect("CreateAssociatedTokenAccount instruction should succeed"); + let create_ata_ix = CreateAssociatedTokenAccount::new( + ctx.payer.pubkey(), + ctx.creator.pubkey(), + pdas.lp_mint, + ) + .idempotent() + .instruction() + .expect("CreateAssociatedTokenAccount instruction should succeed"); ctx.rpc .create_and_send_transaction(&[create_ata_ix], &ctx.payer.pubkey(), &[&ctx.payer]) diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs index 43ebbfe23c..ffaea749de 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs @@ -470,8 +470,7 @@ async fn test_d9_literal() { /// Tests D9Constant: Constant seed expression #[tokio::test] async fn test_d9_constant() { - use csdk_anchor_full_derived_test::d9_seeds::D9ConstantParams; - use csdk_anchor_full_derived_test::D9_CONSTANT_SEED; + use csdk_anchor_full_derived_test::{d9_seeds::D9ConstantParams, D9_CONSTANT_SEED}; let mut ctx = TestContext::new().await; @@ -918,8 +917,7 @@ async fn test_d9_function_call() { /// Tests D9All: All 6 seed expression types #[tokio::test] async fn test_d9_all() { - use csdk_anchor_full_derived_test::d9_seeds::D9AllParams; - use csdk_anchor_full_derived_test::D9_ALL_SEED; + use csdk_anchor_full_derived_test::{d9_seeds::D9AllParams, D9_ALL_SEED}; let mut ctx = TestContext::new().await; let authority = Keypair::new(); @@ -1016,8 +1014,9 @@ async fn test_d9_all() { /// Tests the full lifecycle with compression and decompression #[tokio::test] async fn test_d8_pda_only_full_lifecycle() { - use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D8PdaOnlyRecordSeeds; - use csdk_anchor_full_derived_test::d8_builder_paths::D8PdaOnlyParams; + use csdk_anchor_full_derived_test::{ + csdk_anchor_full_derived_test::D8PdaOnlyRecordSeeds, d8_builder_paths::D8PdaOnlyParams, + }; use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ create_load_accounts_instructions, AccountInterface, AccountInterfaceExt, From c26d287437cbd4abf90332fc5b5f4127aa72d145 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 04:44:22 +0000 Subject: [PATCH 07/11] added full integration tests for all instructions, fixed seed params struct generation, added full account struct tests --- .../macros/docs/account/light_compressible.md | 4 +- sdk-libs/macros/docs/rentfree.md | 7 +- .../rentfree/account/decompress_context.rs | 49 +- .../src/rentfree/account/seed_extraction.rs | 96 +++ .../macros/src/rentfree/program/decompress.rs | 125 +++- .../src/rentfree/program/instructions.rs | 70 ++- .../src/rentfree/program/variant_enum.rs | 101 +-- sdk-libs/sdk/src/compressible/close.rs | 4 +- .../csdk-anchor-full-derived-test/src/lib.rs | 28 +- .../tests/account_macros.rs | 72 +++ .../tests/account_macros/CLAUDE.md | 211 +++++++ .../amm_observation_state_test.rs | 548 ++++++++++++++++ .../account_macros/amm_pool_state_test.rs | 568 +++++++++++++++++ .../account_macros/core_game_session_test.rs | 489 +++++++++++++++ .../core_placeholder_record_test.rs | 401 ++++++++++++ .../account_macros/core_user_record_test.rs | 401 ++++++++++++ .../account_macros/d1_all_field_types_test.rs | 583 ++++++++++++++++++ .../tests/account_macros/d1_array_test.rs | 264 ++++++++ .../account_macros/d1_multi_pubkey_test.rs | 441 +++++++++++++ .../tests/account_macros/d1_no_pubkey_test.rs | 173 ++++++ .../tests/account_macros/d1_non_copy_test.rs | 223 +++++++ .../d1_option_primitive_test.rs | 241 ++++++++ .../account_macros/d1_option_pubkey_test.rs | 427 +++++++++++++ .../account_macros/d1_single_pubkey_test.rs | 322 ++++++++++ .../account_macros/d2_all_compress_as_test.rs | 541 ++++++++++++++++ .../d2_multiple_compress_as_test.rs | 453 ++++++++++++++ .../account_macros/d2_no_compress_as_test.rs | 372 +++++++++++ .../d2_option_none_compress_as_test.rs | 451 ++++++++++++++ .../d2_single_compress_as_test.rs | 365 +++++++++++ .../account_macros/d4_all_composition_test.rs | 509 +++++++++++++++ .../tests/account_macros/d4_info_last_test.rs | 319 ++++++++++ .../tests/account_macros/d4_large_test.rs | 220 +++++++ .../tests/account_macros/d4_minimal_test.rs | 118 ++++ .../tests/account_macros/shared.rs | 403 ++++++++++++ .../{macro_test.rs => integration_tests.rs} | 370 ++++++++++- 35 files changed, 9877 insertions(+), 92 deletions(-) create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/CLAUDE.md create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs rename sdk-tests/csdk-anchor-full-derived-test/tests/{macro_test.rs => integration_tests.rs} (78%) diff --git a/sdk-libs/macros/docs/account/light_compressible.md b/sdk-libs/macros/docs/account/light_compressible.md index 34a9f026c7..e47af4f03e 100644 --- a/sdk-libs/macros/docs/account/light_compressible.md +++ b/sdk-libs/macros/docs/account/light_compressible.md @@ -224,11 +224,11 @@ impl light_sdk::compressible::Unpack for PackedUserRecord { ... } ## 7. Hashing Behavior -The `LightHasherSha` component uses SHA256 to hash the entire struct: +The `LightHasherSha` component uses SHA256 to hash the entire struct via borsh serialization: - **No `#[hash]` attributes needed** - SHA256 serializes and hashes all fields - **Type 3 ShaFlat hashing** - Efficient flat serialization for hashing -- The `compression_info` field is included in the serialized form but typically set to `None` +- **`compression_info` IS included in the hash** - The hash is computed over the entire borsh-serialized struct, including `compression_info`. This means records with `Some(CompressionInfo)` will hash differently than records with `None`. In practice, `compression_info` should be set to `None` before hashing to ensure consistent hashes for the same account data. --- diff --git a/sdk-libs/macros/docs/rentfree.md b/sdk-libs/macros/docs/rentfree.md index 24ae6cf515..90aa45788e 100644 --- a/sdk-libs/macros/docs/rentfree.md +++ b/sdk-libs/macros/docs/rentfree.md @@ -410,10 +410,12 @@ pub struct CachedData { ### 3.3 Pack/Unpack (CompressiblePack) -Generates `Pack` and `Unpack` traits with a `Packed{StructName}` struct where Pubkey fields are compressed to u8 indices. +Generates `Pack` and `Unpack` traits with a `Packed{StructName}` struct where direct Pubkey fields are compressed to u8 indices. **Source**: `sdk-libs/macros/src/rentfree/traits/pack_unpack.rs` +**Limitation**: Only direct `Pubkey` fields are converted to `u8` indices. `Option` fields are **NOT** converted - they remain as `Option` in the packed struct. This is because `Option` can be `None`, which doesn't map cleanly to an index. + **Input**: ```rust #[derive(CompressiblePack)] @@ -499,7 +501,8 @@ pub struct UserRecord { **Notes**: - `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) -- SHA256 hashes the entire struct, so no `#[hash]` attributes needed +- SHA256 hashes the entire struct via borsh serialization, so no `#[hash]` attributes needed +- **Important**: `compression_info` IS included in the hash. Set it to `None` before hashing for consistent results. --- diff --git a/sdk-libs/macros/src/rentfree/account/decompress_context.rs b/sdk-libs/macros/src/rentfree/account/decompress_context.rs index c5116f876f..177bbbfb55 100644 --- a/sdk-libs/macros/src/rentfree/account/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/account/decompress_context.rs @@ -31,11 +31,16 @@ pub fn generate_decompress_context_trait_impl( // Use variant_name for CtxSeeds struct (matches what decompress.rs generates) let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", variant_name); let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_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 pattern to extract params-only fields from packed variant + let params_field_patterns: Vec<_> = params_only_fields.iter().map(|(field, _, _)| { + quote! { #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); @@ -55,11 +60,27 @@ pub fn generate_decompress_context_trait_impl( }).collect(); quote! { let ctx_seeds = #ctx_seeds_struct_name { #(#field_inits),* }; } }; - if ctx_fields.is_empty() { + // Generate SeedParams update with params-only field values + // Note: variant_seed_params is declared OUTSIDE the match to avoid borrow checker issues + // (the reference passed to handle_packed_pda_variant would outlive the match arm scope) + // params-only fields are stored directly in packed variant (not by reference), + // so we use the value directly without dereferencing + let seed_params_update = if params_only_fields.is_empty() { + // No update needed - use the default value declared before match + quote! {} + } else { + let field_inits: Vec<_> = params_only_fields.iter().map(|(field, _, _)| { + quote! { #field: std::option::Option::Some(#field) } + }).collect(); + quote! { variant_seed_params = SeedParams { #(#field_inits,)* ..Default::default() }; } + }; + // When no ctx_fields or params_only_fields, use simple pattern + if ctx_fields.is_empty() && params_only_fields.is_empty() { quote! { RentFreeAccountVariant::#packed_variant_name { data: packed, .. } => { #ctx_seeds_construction - match light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( + #seed_params_update + light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( &*self.rent_sponsor, cpi_accounts, address_space, @@ -71,11 +92,8 @@ pub fn generate_decompress_context_trait_impl( &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), - } + std::option::Option::Some(&variant_seed_params), + )?; } RentFreeAccountVariant::#variant_name { .. } => { unreachable!("Unpacked variants should not be present during decompression"); @@ -83,10 +101,11 @@ pub fn generate_decompress_context_trait_impl( } } else { quote! { - RentFreeAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* .. } => { + RentFreeAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* #(#params_field_patterns,)* .. } => { #(#resolve_ctx_seeds)* #ctx_seeds_construction - match light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( + #seed_params_update + light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( &*self.rent_sponsor, cpi_accounts, address_space, @@ -98,11 +117,8 @@ pub fn generate_decompress_context_trait_impl( &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), - } + std::option::Option::Some(&variant_seed_params), + )?; } RentFreeAccountVariant::#variant_name { .. } => { unreachable!("Unpacked variants should not be present during decompression"); @@ -119,7 +135,7 @@ pub fn generate_decompress_context_trait_impl( type CompressedData = RentFreeAccountData; type PackedTokenData = light_token_sdk::compat::PackedCTokenData<#packed_token_variant_ident>; type CompressedMeta = light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress; - type SeedParams = (); + type SeedParams = SeedParams; fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { &*self.fee_payer @@ -174,6 +190,9 @@ pub fn generate_decompress_context_trait_impl( for (i, compressed_data) in compressed_accounts.into_iter().enumerate() { solana_msg::msg!("collect_pda_and_token: processing account {}", i); let meta = compressed_data.meta; + // Declare variant_seed_params OUTSIDE the match to avoid borrow checker issues + // (reference passed to handle_packed_pda_variant with ? would outlive match arm scope) + let mut variant_seed_params = SeedParams::default(); match compressed_data.data { #(#pda_match_arms)* RentFreeAccountVariant::PackedCTokenData(mut data) => { diff --git a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs index 104b055a50..9463a7b268 100644 --- a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs @@ -621,3 +621,99 @@ pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> } fields } + +/// Get params-only seed fields (data.* fields that don't exist on state struct). +/// Returns (field_name, field_type, has_conversion). +/// - has_conversion is true for fields using to_le_bytes(), to_be_bytes(), etc. +/// - field_type is u64 for fields with byte conversion, Pubkey otherwise. +pub fn get_params_only_seed_fields( + seeds: &[ClassifiedSeed], + state_field_names: &std::collections::HashSet, +) -> Vec<(Ident, syn::Type, bool)> { + let mut fields = Vec::new(); + for seed in seeds { + if let ClassifiedSeed::DataField { + field_name, + conversion, + } = seed + { + let field_str = field_name.to_string(); + // Only include fields that are NOT on the state struct + if !state_field_names.contains(&field_str) { + if !fields.iter().any(|(f, _, _): &(Ident, _, _)| f == field_name) { + let has_conversion = conversion.is_some(); + let field_type: syn::Type = if has_conversion { + syn::parse_quote!(u64) + } else { + syn::parse_quote!(solana_pubkey::Pubkey) + }; + fields.push((field_name.clone(), field_type, has_conversion)); + } + } + } + } + fields +} + +/// Get params-only seed fields from a TokenSeedSpec. +/// This is a convenience wrapper that works with the SeedElement type. +pub fn get_params_only_seed_fields_from_spec( + spec: &crate::rentfree::program::instructions::TokenSeedSpec, + state_field_names: &std::collections::HashSet, +) -> Vec<(Ident, syn::Type, bool)> { + use crate::rentfree::program::instructions::SeedElement; + + let mut fields = Vec::new(); + for seed in &spec.seeds { + if let SeedElement::Expression(expr) = seed { + if let Some((field_name, has_conversion)) = extract_data_field_from_expr(expr) { + let field_str = field_name.to_string(); + // Only include fields that are NOT on the state struct + if !state_field_names.contains(&field_str) { + if !fields.iter().any(|(f, _, _): &(Ident, _, _)| f == &field_name) { + let field_type: syn::Type = if has_conversion { + syn::parse_quote!(u64) + } else { + syn::parse_quote!(solana_pubkey::Pubkey) + }; + fields.push((field_name, field_type, has_conversion)); + } + } + } + } + } + fields +} + +/// Extract data field name and conversion info from an expression. +/// Returns (field_name, has_conversion) if the expression is a data.* field. +fn extract_data_field_from_expr(expr: &syn::Expr) -> Option<(Ident, bool)> { + use crate::rentfree::shared_utils::is_base_path; + + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + if is_base_path(&field_expr.base, "data") { + return Some((field_name.clone(), false)); + } + } + None + } + syn::Expr::MethodCall(method_call) => { + // Handle data.field.to_le_bytes().as_ref() etc. + let has_bytes_conversion = method_call.method == "to_le_bytes" + || method_call.method == "to_be_bytes"; + if has_bytes_conversion { + return extract_data_field_from_expr(&method_call.receiver) + .map(|(name, _)| (name, true)); + } + // For .as_ref(), recurse without marking conversion + if method_call.method == "as_ref" || method_call.method == "as_bytes" { + return extract_data_field_from_expr(&method_call.receiver); + } + None + } + syn::Expr::Reference(ref_expr) => extract_data_field_from_expr(&ref_expr.expr), + _ => None, + } +} diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index 13b2b74197..28dddd0031 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -60,7 +60,7 @@ pub fn generate_process_decompress_accounts_idempotent() -> Result system_accounts_offset, LIGHT_CPI_SIGNER, &crate::ID, - std::option::Option::None::<&()>, + None, ) .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) } @@ -157,12 +157,23 @@ pub fn generate_decompress_accounts_struct(variant: InstructionVariant) -> Resul /// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. /// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) /// Only maps data.field -> self.field if the field exists on the state struct. +/// For params-only fields, uses seed_params.field instead of skipping. #[inline(never)] fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( spec: &TokenSeedSpec, ctx_seed_fields: &[syn::Ident], state_field_names: &std::collections::HashSet, + params_only_fields: &[(syn::Ident, syn::Type, bool)], ) -> Result { + // Build a lookup for params-only field names + let params_only_names: std::collections::HashSet = params_only_fields + .iter() + .map(|(name, _, _)| name.to_string()) + .collect(); + let params_only_has_conversion: std::collections::HashMap = params_only_fields + .iter() + .map(|(name, _, has_conv)| (name.to_string(), *has_conv)) + .collect(); let mut bindings: Vec = Vec::new(); let mut seed_refs = Vec::new(); @@ -199,13 +210,40 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } // Check if this is a data.field expression where the field doesn't exist on state - // If so, skip it (the seed cannot be derived from state during decompression) - if is_params_only_seed(expr, state_field_names) { - // Skip params-only seeds - they cannot be derived during decompression - // The PDA address is stored in compression_info.address, so we don't need - // to re-derive it. However, we still need all seeds for the PDA verification. - // For now, we leave a placeholder that will need to be handled differently. - continue; + // If so, use seed_params.field instead of skipping + if let Some(field_name) = get_params_only_field_name(expr, state_field_names) { + if params_only_names.contains(&field_name) { + let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site()); + let binding_name = + syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); + + // Check if this field has a conversion (to_le_bytes, to_be_bytes) + let has_conversion = params_only_has_conversion + .get(&field_name) + .copied() + .unwrap_or(false); + + if has_conversion { + // u64 field with to_le_bytes conversion + // Must bind bytes to a variable to avoid temporary value dropped while borrowed + let bytes_binding_name = + syn::Ident::new(&format!("{}_bytes", binding_name), proc_macro2::Span::call_site()); + bindings.push(quote! { + let #binding_name = seed_params.#field_ident + .ok_or(solana_program_error::ProgramError::InvalidAccountData)?; + let #bytes_binding_name = #binding_name.to_le_bytes(); + }); + seed_refs.push(quote! { #bytes_binding_name.as_ref() }); + } else { + // Pubkey field + bindings.push(quote! { + let #binding_name = seed_params.#field_ident + .ok_or(solana_program_error::ProgramError::InvalidAccountData)?; + }); + seed_refs.push(quote! { #binding_name.as_ref() }); + } + continue; + } } let binding_name = @@ -241,26 +279,41 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( } /// Check if a seed expression is a params-only seed (data.field where field doesn't exist on state) +#[allow(dead_code)] fn is_params_only_seed( expr: &syn::Expr, state_field_names: &std::collections::HashSet, ) -> bool { + get_params_only_field_name(expr, state_field_names).is_some() +} + +/// Get the field name from a params-only seed expression. +/// Returns Some(field_name) if the expression is a data.field where field doesn't exist on state. +fn get_params_only_field_name( + expr: &syn::Expr, + state_field_names: &std::collections::HashSet, +) -> Option { use crate::rentfree::shared_utils::is_base_path; match expr { syn::Expr::Field(field_expr) => { if let syn::Member::Named(field_name) = &field_expr.member { if is_base_path(&field_expr.base, "data") { - return !state_field_names.contains(&field_name.to_string()); + let name = field_name.to_string(); + if !state_field_names.contains(&name) { + return Some(name); + } } } - false + None } syn::Expr::MethodCall(method_call) => { - is_params_only_seed(&method_call.receiver, state_field_names) + get_params_only_field_name(&method_call.receiver, state_field_names) + } + syn::Expr::Reference(ref_expr) => { + get_params_only_field_name(&ref_expr.expr, state_field_names) } - syn::Expr::Reference(ref_expr) => is_params_only_seed(&ref_expr.expr, state_field_names), - _ => false, + _ => None, } } @@ -327,27 +380,49 @@ pub fn generate_pda_seed_provider_impls( } }; + let params_only_fields = &ctx_info.params_only_seed_fields; let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds( spec, ctx_fields, &ctx_info.state_field_names, + params_only_fields, )?; // Generate impl for inner_type, but use variant-based struct name - results.push(quote! { - #ctx_seeds_struct - - impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #inner_type { - 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 + // Use SeedParams if there are params-only fields, otherwise use () + let has_params_only = !params_only_fields.is_empty(); + let seed_params_impl = if has_params_only { + quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + seed_params: &SeedParams, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } + } + } + } else { + quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, SeedParams> for #inner_type { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &SeedParams, + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } } } - }); + }; + results.push(seed_params_impl); } Ok(results) diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 4e612361b1..c8a78521f2 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -97,11 +97,15 @@ fn codegen( .map(|fields| fields.into_iter().collect()) .unwrap_or_default(); + // Extract params-only seed fields (data.* fields that don't exist on state) + let params_only_seed_fields = crate::rentfree::account::seed_extraction::get_params_only_seed_fields_from_spec(spec, &state_field_names); + PdaCtxSeedInfo::with_state_fields( spec.variant.clone(), inner_type, ctx_fields, state_field_names, + params_only_seed_fields, ) }) .collect() @@ -111,9 +115,54 @@ fn codegen( let enum_and_traits = super::variant_enum::compressed_account_variant_with_ctx_seeds(&pda_ctx_seeds)?; - let seed_params_struct = quote! { - #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] - pub struct SeedParams; + // Collect all unique params-only seed fields across all variants for SeedParams struct + // Use BTreeMap for deterministic ordering + let mut all_params_only_fields: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + for ctx_info in &pda_ctx_seeds { + for (field_name, field_type, _) in &ctx_info.params_only_seed_fields { + let field_str = field_name.to_string(); + all_params_only_fields + .entry(field_str) + .or_insert_with(|| field_type.clone()); + } + } + + let seed_params_struct = if all_params_only_fields.is_empty() { + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] + pub struct SeedParams; + } + } else { + // Collect into Vec for consistent ordering between field declarations and Default impl + let sorted_fields: Vec<_> = all_params_only_fields.iter().collect(); + let seed_param_fields: Vec<_> = sorted_fields + .iter() + .map(|(name, ty)| { + let field_ident = format_ident!("{}", name); + quote! { pub #field_ident: Option<#ty> } + }) + .collect(); + let seed_param_defaults: Vec<_> = sorted_fields + .iter() + .map(|(name, _)| { + let field_ident = format_ident!("{}", name); + quote! { #field_ident: None } + }) + .collect(); + quote! { + #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug)] + pub struct SeedParams { + #(#seed_param_fields,)* + } + impl Default for SeedParams { + fn default() -> Self { + Self { + #(#seed_param_defaults,)* + } + } + } + } }; let instruction_data_types: std::collections::HashMap = instruction_data @@ -135,6 +184,7 @@ fn codegen( let seeds_struct_name = format_ident!("{}Seeds", variant_name); let constructor_name = format_ident!("{}", to_snake_case(&variant_name.to_string())); let ctx_fields = &ctx_info.ctx_seed_fields; + let params_only_fields = &ctx_info.params_only_seed_fields; let ctx_field_decls: Vec<_> = ctx_fields.iter().map(|field| { quote! { pub #field: solana_pubkey::Pubkey } }).collect(); @@ -158,6 +208,10 @@ fn codegen( } }) }).collect(); + + // Extract params-only field names from ctx_info for variant construction + let params_only_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + quote! { #[derive(Clone, Debug)] pub struct #seeds_struct_name { @@ -176,9 +230,11 @@ fn codegen( #(#data_verifications)* // Use variant_name for the enum variant + // Include ctx fields and params-only fields from seeds std::result::Result::Ok(Self::#variant_name { data, #(#ctx_fields: seeds.#ctx_fields,)* + #(#params_only_field_names: seeds.#params_only_field_names,)* }) } } @@ -335,9 +391,11 @@ fn codegen( &instruction_data, )?; - // Insert SeedParams struct - let seed_params_item: Item = syn::parse2(seed_params_struct)?; - content.1.push(seed_params_item); + // Insert SeedParams struct and impl + let seed_params_file: syn::File = syn::parse2(seed_params_struct)?; + for item in seed_params_file.items { + content.1.push(item); + } // Insert XxxSeeds structs and RentFreeAccountVariant constructors for seeds_tokens in seeds_structs_and_constructors.into_iter() { diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index 1b4283995d..f986d649c1 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -18,6 +18,9 @@ pub struct PdaCtxSeedInfo { pub ctx_seed_fields: Vec, /// Field names that exist on the state struct (for filtering data.* seeds) pub state_field_names: std::collections::HashSet, + /// Params-only seed fields (name, type, has_conversion) - seeds from params.* that don't exist on state + /// The bool indicates whether a conversion method like to_le_bytes() is applied + pub params_only_seed_fields: Vec<(Ident, Type, bool)>, } impl PdaCtxSeedInfo { @@ -26,12 +29,14 @@ impl PdaCtxSeedInfo { inner_type: Type, ctx_seed_fields: Vec, state_field_names: std::collections::HashSet, + params_only_seed_fields: Vec<(Ident, Type, bool)>, ) -> Self { Self { variant_name, inner_type, ctx_seed_fields, state_field_names, + params_only_seed_fields, } } } @@ -48,7 +53,7 @@ pub fn compressed_account_variant_with_ctx_seeds( )); } - // Phase 2: Generate struct variants with ctx.* seed fields + // Phase 2: Generate struct variants with ctx.* seed fields and params-only seed fields // Uses variant_name for enum variant naming, inner_type for data field types let account_variants = pda_ctx_seeds.iter().map(|info| { let variant_name = &info.variant_name; @@ -59,22 +64,30 @@ pub fn compressed_account_variant_with_ctx_seeds( let packed_inner_type = make_packed_type(&info.inner_type).expect("inner_type should be a valid type path"); let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; - // Unpacked variant: Pubkey fields for ctx.* seeds + // Unpacked variant: Pubkey fields for ctx.* seeds + params-only seed values // 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 } }); + let unpacked_params_fields = params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: #ty } + }); - // Packed variant: u8 index fields for ctx.* seeds + // Packed variant: u8 index fields for ctx.* seeds + params-only seed values (same type) let packed_ctx_fields = ctx_fields.iter().map(|field| { let idx_field = format_ident!("{}_idx", field); quote! { #idx_field: u8 } }); + // Params-only fields keep the same type in packed variant (not indices) + let packed_params_fields = params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: #ty } + }); quote! { - #variant_name { data: #inner_type, #(#unpacked_ctx_fields,)* }, - #packed_variant_name { data: #packed_inner_type, #(#packed_ctx_fields,)* }, + #variant_name { data: #inner_type, #(#unpacked_ctx_fields,)* #(#unpacked_params_fields,)* }, + #packed_variant_name { data: #packed_inner_type, #(#packed_ctx_fields,)* #(#packed_params_fields,)* }, } }); @@ -93,13 +106,17 @@ pub fn compressed_account_variant_with_ctx_seeds( let first_variant = &first.variant_name; let first_type = qualify_type_with_crate(&first.inner_type); let first_ctx_fields = &first.ctx_seed_fields; + let first_params_only_fields = &first.params_only_seed_fields; let first_default_ctx_fields = first_ctx_fields.iter().map(|field| { quote! { #field: Pubkey::default() } }); + let first_default_params_fields = first_params_only_fields.iter().map(|(field, ty, _)| { + quote! { #field: <#ty as Default>::default() } + }); let default_impl = quote! { impl Default for RentFreeAccountVariant { fn default() -> Self { - Self::#first_variant { data: #first_type::default(), #(#first_default_ctx_fields,)* } + Self::#first_variant { data: #first_type::default(), #(#first_default_ctx_fields,)* #(#first_default_params_fields,)* } } } }; @@ -231,15 +248,28 @@ pub fn compressed_account_variant_with_ctx_seeds( } }; - // Phase 2: Pack/Unpack with ctx seed fields + // Phase 2: Pack/Unpack with ctx seed fields and params-only seed fields let pack_match_arms: Vec<_> = pda_ctx_seeds.iter().map(|info| { let variant_name = &info.variant_name; let inner_type = qualify_type_with_crate(&info.inner_type); let packed_variant_name = format_ident!("Packed{}", variant_name); let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; - if ctx_fields.is_empty() { - // No ctx seeds - simple pack + // Collect ctx field names and their idx equivalents + let ctx_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(); + + // Collect params-only field names (these are copied directly, not indexed) + let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + + // If no ctx seeds and no params-only fields - simple pack + if ctx_fields.is_empty() && params_only_fields.is_empty() { quote! { RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), RentFreeAccountVariant::#variant_name { data, .. } => RentFreeAccountVariant::#packed_variant_name { @@ -247,22 +277,15 @@ pub fn compressed_account_variant_with_ctx_seeds( }, } } 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(); - + // Has ctx seeds and/or params-only fields - pack data, ctx seed pubkeys, and copy params-only values quote! { RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), - RentFreeAccountVariant::#variant_name { data, #(#field_names,)* .. } => { + RentFreeAccountVariant::#variant_name { data, #(#ctx_field_names,)* #(#params_field_names,)* .. } => { #(#pack_ctx_seeds)* RentFreeAccountVariant::#packed_variant_name { data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts), #(#idx_field_names,)* + #(#params_field_names: *#params_field_names,)* } }, } @@ -294,9 +317,26 @@ pub fn compressed_account_variant_with_ctx_seeds( let packed_inner_type = make_packed_type(inner_type) .expect("inner_type should be a valid type path"); let ctx_fields = &info.ctx_seed_fields; + let params_only_fields = &info.params_only_seed_fields; - if ctx_fields.is_empty() { - // No ctx seeds - simple unpack + // Collect ctx field names and their idx equivalents + let idx_field_names: Vec<_> = ctx_fields.iter().map(|f| format_ident!("{}_idx", f)).collect(); + let ctx_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(); + + // Collect params-only field names (these are copied directly, not resolved from indices) + let params_field_names: Vec<_> = params_only_fields.iter().map(|(f, _, _)| f).collect(); + + // If no ctx seeds and no params-only fields - simple unpack + if ctx_fields.is_empty() && params_only_fields.is_empty() { quote! { RentFreeAccountVariant::#packed_variant_name { data, .. } => Ok(RentFreeAccountVariant::#variant_name { data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, @@ -304,25 +344,14 @@ pub fn compressed_account_variant_with_ctx_seeds( RentFreeAccountVariant::#variant_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(); - + // Has ctx seeds and/or params-only fields - unpack data, resolve ctx seed pubkeys, and copy params-only values quote! { - RentFreeAccountVariant::#packed_variant_name { data, #(#idx_field_names,)* .. } => { + RentFreeAccountVariant::#packed_variant_name { data, #(#idx_field_names,)* #(#params_field_names,)* .. } => { #(#unpack_ctx_seeds)* Ok(RentFreeAccountVariant::#variant_name { data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, - #(#field_names,)* + #(#ctx_field_names,)* + #(#params_field_names: *#params_field_names,)* }) }, RentFreeAccountVariant::#variant_name { .. } => unreachable!(), diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs index a240d3aae3..f50404d0c8 100644 --- a/sdk-libs/sdk/src/compressible/close.rs +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -11,7 +11,7 @@ pub fn close<'info>( if info.key == sol_destination.key { info.assign(&system_program_id); - info.resize(0) + info.realloc(0, false) .map_err(|_| LightSdkError::ConstraintViolation)?; return Ok(()); } @@ -38,7 +38,7 @@ pub fn close<'info>( } info.assign(&system_program_id); - info.resize(0) + info.realloc(0, false) .map_err(|_| LightSdkError::ConstraintViolation)?; Ok(()) 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 b95afc6998..297b14ab83 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -30,9 +30,31 @@ pub use instructions::{ d9_seeds::{D9_ALL_SEED, D9_CONSTANT_SEED}, }; pub use state::{ - d1_field_types::single_pubkey::{PackedSinglePubkeyRecord, SinglePubkeyRecord}, - d2_compress_as::multiple::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}, - GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord, + d1_field_types::{ + all::{AllFieldTypesRecord, PackedAllFieldTypesRecord}, + arrays::ArrayRecord, + multi_pubkey::{MultiPubkeyRecord, PackedMultiPubkeyRecord}, + no_pubkey::NoPubkeyRecord, + non_copy::NonCopyRecord, + option_primitive::OptionPrimitiveRecord, + option_pubkey::{OptionPubkeyRecord, PackedOptionPubkeyRecord}, + single_pubkey::{PackedSinglePubkeyRecord, SinglePubkeyRecord}, + }, + d2_compress_as::{ + absent::{NoCompressAsRecord, PackedNoCompressAsRecord}, + all::{AllCompressAsRecord, PackedAllCompressAsRecord}, + multiple::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}, + option_none::{OptionNoneCompressAsRecord, PackedOptionNoneCompressAsRecord}, + single::{PackedSingleCompressAsRecord, SingleCompressAsRecord}, + }, + d4_composition::{ + all::{AllCompositionRecord, PackedAllCompositionRecord}, + info_last::{InfoLastRecord, PackedInfoLastRecord}, + large::LargeRecord, + minimal::MinimalRecord, + }, + GameSession, PackedGameSession, PackedPlaceholderRecord, PackedUserRecord, PlaceholderRecord, + UserRecord, }; #[inline] pub fn max_key(left: &Pubkey, right: &Pubkey) -> [u8; 32] { diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros.rs new file mode 100644 index 0000000000..cc572631ff --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros.rs @@ -0,0 +1,72 @@ +//! Unit tests for RentFreeAccount-derived traits +//! +//! Tests individual traits derived by the `RentFreeAccount` macro on account data structs. + +#[path = "account_macros/shared.rs"] +pub mod shared; + +#[path = "account_macros/d1_single_pubkey_test.rs"] +pub mod d1_single_pubkey_test; + +#[path = "account_macros/d1_multi_pubkey_test.rs"] +pub mod d1_multi_pubkey_test; + +#[path = "account_macros/d1_no_pubkey_test.rs"] +pub mod d1_no_pubkey_test; + +#[path = "account_macros/d1_option_primitive_test.rs"] +pub mod d1_option_primitive_test; + +#[path = "account_macros/d1_option_pubkey_test.rs"] +pub mod d1_option_pubkey_test; + +#[path = "account_macros/d1_non_copy_test.rs"] +pub mod d1_non_copy_test; + +#[path = "account_macros/d1_array_test.rs"] +pub mod d1_array_test; + +#[path = "account_macros/d1_all_field_types_test.rs"] +pub mod d1_all_field_types_test; + +#[path = "account_macros/d2_single_compress_as_test.rs"] +pub mod d2_single_compress_as_test; + +#[path = "account_macros/d2_multiple_compress_as_test.rs"] +pub mod d2_multiple_compress_as_test; + +#[path = "account_macros/d2_no_compress_as_test.rs"] +pub mod d2_no_compress_as_test; + +#[path = "account_macros/d2_option_none_compress_as_test.rs"] +pub mod d2_option_none_compress_as_test; + +#[path = "account_macros/d2_all_compress_as_test.rs"] +pub mod d2_all_compress_as_test; + +#[path = "account_macros/d4_minimal_test.rs"] +pub mod d4_minimal_test; + +#[path = "account_macros/d4_info_last_test.rs"] +pub mod d4_info_last_test; + +#[path = "account_macros/d4_large_test.rs"] +pub mod d4_large_test; + +#[path = "account_macros/d4_all_composition_test.rs"] +pub mod d4_all_composition_test; + +#[path = "account_macros/amm_pool_state_test.rs"] +pub mod amm_pool_state_test; + +#[path = "account_macros/amm_observation_state_test.rs"] +pub mod amm_observation_state_test; + +#[path = "account_macros/core_user_record_test.rs"] +pub mod core_user_record_test; + +#[path = "account_macros/core_game_session_test.rs"] +pub mod core_game_session_test; + +#[path = "account_macros/core_placeholder_record_test.rs"] +pub mod core_placeholder_record_test; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/CLAUDE.md b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/CLAUDE.md new file mode 100644 index 0000000000..6674dfe507 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/CLAUDE.md @@ -0,0 +1,211 @@ +# Account Macros Test Directory + +This directory contains unit tests for trait implementations derived by the `#[derive(RentFreeAccount)]` and `#[derive(LightCompressible)]` macros on account data structs. + +## Test Coverage Requirement + +**Every account struct** with `#[derive(RentFreeAccount)]` or `#[derive(LightCompressible)]` **must have its own dedicated test file** in this directory. + +## Directory Structure + +``` +account_macros/ +├── CLAUDE.md # This documentation +├── shared.rs # Generic test helpers and CompressibleTestFactory trait +├── d1_single_pubkey_test.rs # Tests for SinglePubkeyRecord +├── d1_multi_pubkey_test.rs # Tests for MultiPubkeyRecord (TODO) +├── d1_no_pubkey_test.rs # Tests for NoPubkeyRecord (TODO) +└── ... # One test file per account struct +``` + +## File Naming Convention + +Test files follow the pattern: `{dimension}_{struct_descriptor}_test.rs` + +- **Dimension prefix** matches the source module (e.g., `d1_` for `d1_field_types/`) +- **Struct descriptor** is a snake_case description of the struct being tested +- **Suffix** is always `_test.rs` + +Examples: +| Account Struct | Source Module | Test File | +|----------------|---------------|-----------| +| `SinglePubkeyRecord` | `d1_field_types/single_pubkey.rs` | `d1_single_pubkey_test.rs` | +| `MultiPubkeyRecord` | `d1_field_types/multi_pubkey.rs` | `d1_multi_pubkey_test.rs` | +| `NoPubkeyRecord` | `d1_field_types/no_pubkey.rs` | `d1_no_pubkey_test.rs` | +| `CompressAsAbsentRecord` | `d2_compress_as/absent.rs` | `d2_compress_as_absent_test.rs` | + +## Required Test File Structure + +Each test file must contain three sections: + +### 1. Factory Implementation (Required) + +Implement `CompressibleTestFactory` for your struct: + +```rust +use super::shared::CompressibleTestFactory; + +impl CompressibleTestFactory for YourRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + // ... initialize all other fields with valid test values + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + // ... initialize all other fields with valid test values + } + } +} +``` + +### 2. Generic Tests via Macro (Required) + +Invoke the macro to generate 17 generic trait tests: + +```rust +use crate::generate_trait_tests; + +generate_trait_tests!(YourRecord); +``` + +This generates tests for: +- **LightDiscriminator** (4 tests): 8-byte length, non-zero, method matches constant, slice matches array +- **HasCompressionInfo** (6 tests): reference access, mutation, opt access, set_none, panic on None +- **CompressAs** (2 tests): sets compression_info to None, returns Cow::Owned +- **Size** (2 tests): positive value, deterministic +- **CompressedInitSpace** (1 test): includes discriminator +- **DataHasher** (3 tests): 32-byte output, deterministic, compression_info affects hash + +### 3. Struct-Specific Tests (Required) + +Tests that cannot be generic because they depend on the struct's specific fields: + +#### CompressAs Field Preservation Tests +```rust +#[test] +fn test_compress_as_preserves_other_fields() { + // Verify each field is preserved after compress_as() +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + // Verify compress_as() works when compression_info starts as None +} +``` + +#### DataHasher Field Sensitivity Tests +```rust +#[test] +fn test_hash_differs_for_different_{field_name}() { + // One test per non-compression_info field + // Verify changing that field changes the hash +} +``` + +#### Pack/Unpack Tests (if struct has direct Pubkey fields) + +**IMPORTANT**: Only direct `Pubkey` fields are converted to `u8` indices. `Option` fields are **NOT** converted - they remain as `Option` in the packed struct. + +```rust +#[test] +fn test_packed_struct_has_u8_{pubkey_field}() { + // Verify PackedX struct has u8 index for each direct Pubkey field + // Note: Option fields stay as Option +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + // Verify Pubkey -> u8 index conversion +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + // Same Pubkey packed twice gets same index +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + // Different Pubkeys get different indices +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + // Packed struct always has compression_info = None +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + // Verify pubkeys are stored in PackedAccounts +} + +#[test] +fn test_pack_index_assignment_order() { + // Verify sequential index assignment +} +``` + +## Checklist for Creating a New Test File + +When adding tests for a new account struct `MyNewRecord`: + +- [ ] Create test file: `{dimension}_{descriptor}_test.rs` +- [ ] Add imports: + ```rust + use super::shared::CompressibleTestFactory; + use crate::generate_trait_tests; + use csdk_anchor_full_derived_test::{PackedMyNewRecord, MyNewRecord}; + use light_hasher::{DataHasher, Sha256}; + use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, + }; + use solana_pubkey::Pubkey; + ``` +- [ ] Implement `CompressibleTestFactory` for `MyNewRecord` +- [ ] Add `generate_trait_tests!(MyNewRecord);` +- [ ] Add `test_compress_as_preserves_other_fields` +- [ ] Add `test_compress_as_when_compression_info_already_none` +- [ ] Add `test_hash_differs_for_different_{field}` for each non-compression_info field +- [ ] If struct has Pubkey fields, add all Pack/Unpack tests +- [ ] Register test file in `/tests/account_macros.rs`: + ```rust + #[path = "account_macros/{your_test_file}.rs"] + pub mod {your_module_name}; + ``` + +## Generic vs Struct-Specific Tests + +| Test Category | Generic (shared.rs) | Struct-Specific | +|---------------|---------------------|-----------------| +| LightDiscriminator | All 4 tests | None | +| HasCompressionInfo | All 6 tests | None | +| CompressAs | Basic 2 tests | Field preservation | +| Size | All 2 tests | None | +| CompressedInitSpace | All 1 test | None | +| DataHasher | Basic 3 tests | Field sensitivity | +| Pack/Unpack | None | All (struct-dependent) | + +## Running Tests + +```bash +# Run all account macro tests +cargo test -p csdk-anchor-full-derived-test --test account_macros + +# Run tests for a specific struct +cargo test -p csdk-anchor-full-derived-test --test account_macros d1_single_pubkey + +# Run a specific test +cargo test -p csdk-anchor-full-derived-test --test account_macros test_pack_converts_pubkey_to_index +``` + +## Test Dependencies + +Tests depend on: +- `light_hasher` - For `DataHasher`, `Sha256` +- `light_sdk` - For `CompressAs`, `CompressionInfo`, `Pack`, `PackedAccounts`, `Size`, etc. +- `solana_pubkey` - For `Pubkey` +- Account structs and Packed variants from `csdk_anchor_full_derived_test` diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs new file mode 100644 index 0000000000..0f59fc79c2 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs @@ -0,0 +1,548 @@ +//! AMM ObservationState Tests: ObservationState trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `ObservationState`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedObservationState +//! +//! ObservationState has 1 Pubkey field (pool_id) and a nested array of Observation structs, +//! testing Pack/Unpack behavior with array fields and nested data structures. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedObservationState, ObservationState, Observation}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for ObservationState { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + initialized: false, + observation_index: 0, + pool_id: Pubkey::new_unique(), + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id: Pubkey::new_unique(), + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(ObservationState); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_pool_id() { + let pool_id = Pubkey::new_unique(); + + let observation_state = ObservationState { + compression_info: Some(CompressionInfo::default()), + initialized: true, + observation_index: 5, + pool_id, + observations: [ + Observation { + block_timestamp: 1000, + cumulative_token_0_price_x32: 100, + cumulative_token_1_price_x32: 200, + }, + Observation { + block_timestamp: 2000, + cumulative_token_0_price_x32: 300, + cumulative_token_1_price_x32: 400, + }, + ], + padding: [0u64; 4], + }; + + let compressed = observation_state.compress_as(); + let inner = compressed.into_owned(); + + assert_eq!(inner.pool_id, pool_id); + assert_eq!(inner.initialized, true); + assert_eq!(inner.observation_index, 5); +} + +#[test] +fn test_compress_as_preserves_observation_data() { + let observation_state = ObservationState { + compression_info: Some(CompressionInfo::default()), + initialized: true, + observation_index: 1, + pool_id: Pubkey::new_unique(), + observations: [ + Observation { + block_timestamp: 1111, + cumulative_token_0_price_x32: 5000, + cumulative_token_1_price_x32: 6000, + }, + Observation { + block_timestamp: 2222, + cumulative_token_0_price_x32: 7000, + cumulative_token_1_price_x32: 8000, + }, + ], + padding: [10, 20, 30, 40], + }; + + let compressed = observation_state.compress_as(); + let inner = compressed.into_owned(); + + assert_eq!(inner.observations[0].block_timestamp, 1111); + assert_eq!(inner.observations[0].cumulative_token_0_price_x32, 5000); + assert_eq!(inner.observations[0].cumulative_token_1_price_x32, 6000); + assert_eq!(inner.observations[1].block_timestamp, 2222); + assert_eq!(inner.observations[1].cumulative_token_0_price_x32, 7000); + assert_eq!(inner.observations[1].cumulative_token_1_price_x32, 8000); + assert_eq!(inner.padding, [10, 20, 30, 40]); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_pool_id() { + let mut observation1 = ObservationState::without_compression_info(); + let mut observation2 = ObservationState::without_compression_info(); + + observation1.pool_id = Pubkey::new_unique(); + observation2.pool_id = Pubkey::new_unique(); + + let hash1 = observation1 + .hash::() + .expect("hash should succeed"); + let hash2 = observation2 + .hash::() + .expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different pool_id should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_initialized() { + let mut observation1 = ObservationState::without_compression_info(); + let mut observation2 = ObservationState::without_compression_info(); + + observation1.initialized = true; + observation2.initialized = false; + + let hash1 = observation1 + .hash::() + .expect("hash should succeed"); + let hash2 = observation2 + .hash::() + .expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different initialized should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_observation_index() { + let mut observation1 = ObservationState::without_compression_info(); + let mut observation2 = ObservationState::without_compression_info(); + + observation1.observation_index = 1; + observation2.observation_index = 2; + + let hash1 = observation1 + .hash::() + .expect("hash should succeed"); + let hash2 = observation2 + .hash::() + .expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different observation_index should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_observation_data() { + let mut observation1 = ObservationState::without_compression_info(); + let mut observation2 = ObservationState::without_compression_info(); + + observation1.observations[0].block_timestamp = 1000; + observation2.observations[0].block_timestamp = 2000; + + let hash1 = observation1 + .hash::() + .expect("hash should succeed"); + let hash2 = observation2 + .hash::() + .expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different observation data should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_pool_id_index() { + // ObservationState has 1 Pubkey field (pool_id), so PackedObservationState should have 1 u8 field + let packed = PackedObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id: 0, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + assert_eq!(packed.pool_id, 0u8); +} + +#[test] +fn test_pack_converts_pool_id_to_index() { + let pool_id = Pubkey::new_unique(); + + let observation_state = ObservationState { + compression_info: None, + initialized: true, + observation_index: 0, + pool_id, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = observation_state.pack(&mut packed_accounts); + + // The pool_id should have been added to packed_accounts and assigned index 0 + assert_eq!(packed.pool_id, 0u8); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], pool_id); +} + +#[test] +fn test_pack_with_pre_existing_pubkeys() { + let pool_id = Pubkey::new_unique(); + + let observation_state = ObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + // Pre-insert another pubkey + packed_accounts.insert_or_get(Pubkey::new_unique()); + + let packed = observation_state.pack(&mut packed_accounts); + + // The pool_id should have been added and assigned index 1 (since index 0 is taken) + assert_eq!(packed.pool_id, 1u8); +} + +#[test] +fn test_pack_preserves_all_fields() { + let pool_id = Pubkey::new_unique(); + + let observation_state = ObservationState { + compression_info: None, + initialized: true, + observation_index: 42, + pool_id, + observations: [ + Observation { + block_timestamp: 1000, + cumulative_token_0_price_x32: 5000, + cumulative_token_1_price_x32: 6000, + }, + Observation { + block_timestamp: 2000, + cumulative_token_0_price_x32: 7000, + cumulative_token_1_price_x32: 8000, + }, + ], + padding: [111, 222, 333, 444], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = observation_state.pack(&mut packed_accounts); + + assert_eq!(packed.initialized, true); + assert_eq!(packed.observation_index, 42); + assert_eq!(packed.observations[0].block_timestamp, 1000); + assert_eq!(packed.observations[0].cumulative_token_0_price_x32, 5000); + assert_eq!(packed.observations[0].cumulative_token_1_price_x32, 6000); + assert_eq!(packed.observations[1].block_timestamp, 2000); + assert_eq!(packed.observations[1].cumulative_token_0_price_x32, 7000); + assert_eq!(packed.observations[1].cumulative_token_1_price_x32, 8000); + assert_eq!(packed.padding, [111, 222, 333, 444]); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let observation_with_info = ObservationState { + compression_info: Some(CompressionInfo::default()), + initialized: false, + observation_index: 0, + pool_id: Pubkey::new_unique(), + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = observation_with_info.pack(&mut packed_accounts); + + assert!( + packed.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_different_pool_ids_get_different_indices() { + let pool_id1 = Pubkey::new_unique(); + let pool_id2 = Pubkey::new_unique(); + + let observation1 = ObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id: pool_id1, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let observation2 = ObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id: pool_id2, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = observation1.pack(&mut packed_accounts); + let packed2 = observation2.pack(&mut packed_accounts); + + // Different pool IDs should get different indices + assert_ne!( + packed1.pool_id, packed2.pool_id, + "different pool_ids should produce different indices" + ); +} + +#[test] +fn test_pack_reuses_same_pool_id_index() { + let pool_id = Pubkey::new_unique(); + + let observation1 = ObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id, + observations: [ + Observation { + block_timestamp: 1000, + cumulative_token_0_price_x32: 100, + cumulative_token_1_price_x32: 200, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let observation2 = ObservationState { + compression_info: None, + initialized: true, + observation_index: 1, + pool_id, + observations: [ + Observation { + block_timestamp: 2000, + cumulative_token_0_price_x32: 300, + cumulative_token_1_price_x32: 400, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = observation1.pack(&mut packed_accounts); + let packed2 = observation2.pack(&mut packed_accounts); + + // Same pool_id should get same index + assert_eq!( + packed1.pool_id, packed2.pool_id, + "same pool_id should produce same index" + ); +} + +#[test] +fn test_pack_stores_pool_id_in_packed_accounts() { + let pool_id = Pubkey::new_unique(); + + let observation_state = ObservationState { + compression_info: None, + initialized: false, + observation_index: 0, + pool_id, + observations: [ + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + Observation { + block_timestamp: 0, + cumulative_token_0_price_x32: 0, + cumulative_token_1_price_x32: 0, + }, + ], + padding: [0u64; 4], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = observation_state.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1, "should have 1 pubkey stored"); + assert_eq!( + stored_pubkeys[packed.pool_id as usize], pool_id, + "stored pubkey should match" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs new file mode 100644 index 0000000000..85603f5690 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs @@ -0,0 +1,568 @@ +//! AMM PoolState Tests: PoolState trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `PoolState`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedPoolState +//! +//! PoolState has 10 Pubkey fields and multiple numeric fields, testing +//! comprehensive Pack/Unpack behavior with multiple pubkey indices. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedPoolState, PoolState}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for PoolState { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(PoolState); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_numeric_fields() { + let pool = PoolState { + compression_info: Some(CompressionInfo::default()), + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 42, + status: 1, + lp_mint_decimals: 8, + mint_0_decimals: 6, + mint_1_decimals: 9, + lp_supply: 1000000, + protocol_fees_token_0: 500, + protocol_fees_token_1: 600, + fund_fees_token_0: 100, + fund_fees_token_1: 200, + open_time: 1234567890, + recent_epoch: 500, + padding: [0u64; 1], + }; + + let compressed = pool.compress_as(); + let inner = compressed.into_owned(); + + assert_eq!(inner.auth_bump, 42); + assert_eq!(inner.status, 1); + assert_eq!(inner.lp_mint_decimals, 8); + assert_eq!(inner.mint_0_decimals, 6); + assert_eq!(inner.mint_1_decimals, 9); + assert_eq!(inner.lp_supply, 1000000); + assert_eq!(inner.protocol_fees_token_0, 500); + assert_eq!(inner.protocol_fees_token_1, 600); + assert_eq!(inner.fund_fees_token_0, 100); + assert_eq!(inner.fund_fees_token_1, 200); + assert_eq!(inner.open_time, 1234567890); + assert_eq!(inner.recent_epoch, 500); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_amm_config() { + let mut pool1 = PoolState::without_compression_info(); + let mut pool2 = PoolState::without_compression_info(); + + pool1.amm_config = Pubkey::new_unique(); + pool2.amm_config = Pubkey::new_unique(); + + let hash1 = pool1.hash::().expect("hash should succeed"); + let hash2 = pool2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different amm_config should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_lp_supply() { + let mut pool1 = PoolState::without_compression_info(); + let mut pool2 = PoolState::without_compression_info(); + + pool1.lp_supply = 1000000; + pool2.lp_supply = 2000000; + + let hash1 = pool1.hash::().expect("hash should succeed"); + let hash2 = pool2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different lp_supply should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_auth_bump() { + let mut pool1 = PoolState::without_compression_info(); + let mut pool2 = PoolState::without_compression_info(); + + pool1.auth_bump = 100; + pool2.auth_bump = 200; + + let hash1 = pool1.hash::().expect("hash should succeed"); + let hash2 = pool2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different auth_bump should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_open_time() { + let mut pool1 = PoolState::without_compression_info(); + let mut pool2 = PoolState::without_compression_info(); + + pool1.open_time = 1000; + pool2.open_time = 2000; + + let hash1 = pool1.hash::().expect("hash should succeed"); + let hash2 = pool2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different open_time should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_pubkey_indices() { + // PoolState has 10 Pubkey fields, so PackedPoolState should have 10 u8 fields + let packed = PackedPoolState { + compression_info: None, + amm_config: 0, + pool_creator: 1, + token_0_vault: 2, + token_1_vault: 3, + lp_mint: 4, + token_0_mint: 5, + token_1_mint: 6, + token_0_program: 7, + token_1_program: 8, + observation_key: 9, + auth_bump: 42, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 100, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + assert_eq!(packed.amm_config, 0u8); + assert_eq!(packed.pool_creator, 1u8); + assert_eq!(packed.observation_key, 9u8); + assert_eq!(packed.auth_bump, 42u8); +} + +#[test] +fn test_pack_converts_all_10_pubkeys_to_indices() { + let pubkeys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let pool = PoolState { + compression_info: None, + amm_config: pubkeys[0], + pool_creator: pubkeys[1], + token_0_vault: pubkeys[2], + token_1_vault: pubkeys[3], + lp_mint: pubkeys[4], + token_0_mint: pubkeys[5], + token_1_mint: pubkeys[6], + token_0_program: pubkeys[7], + token_1_program: pubkeys[8], + observation_key: pubkeys[9], + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = pool.pack(&mut packed_accounts); + + // All 10 pubkeys should have been added and assigned indices 0-9 + assert_eq!(packed.amm_config, 0u8); + assert_eq!(packed.pool_creator, 1u8); + assert_eq!(packed.token_0_vault, 2u8); + assert_eq!(packed.token_1_vault, 3u8); + assert_eq!(packed.lp_mint, 4u8); + assert_eq!(packed.token_0_mint, 5u8); + assert_eq!(packed.token_1_mint, 6u8); + assert_eq!(packed.token_0_program, 7u8); + assert_eq!(packed.token_1_program, 8u8); + assert_eq!(packed.observation_key, 9u8); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 10); + for (i, pubkey) in pubkeys.iter().enumerate() { + assert_eq!(stored_pubkeys[i], *pubkey); + } +} + +#[test] +fn test_pack_reuses_same_pubkey_indices() { + // If the same pubkey is used in multiple fields, it should get the same index + let shared_pubkey = Pubkey::new_unique(); + + let pool = PoolState { + compression_info: None, + amm_config: shared_pubkey, + pool_creator: shared_pubkey, + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = pool.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed.amm_config, packed.pool_creator, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_preserves_numeric_fields() { + let pool = PoolState { + compression_info: None, + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 127, + status: 2, + lp_mint_decimals: 8, + mint_0_decimals: 6, + mint_1_decimals: 9, + lp_supply: 9999999, + protocol_fees_token_0: 444, + protocol_fees_token_1: 555, + fund_fees_token_0: 111, + fund_fees_token_1: 222, + open_time: 1700000000, + recent_epoch: 999, + padding: [42u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = pool.pack(&mut packed_accounts); + + assert_eq!(packed.auth_bump, 127); + assert_eq!(packed.status, 2); + assert_eq!(packed.lp_mint_decimals, 8); + assert_eq!(packed.mint_0_decimals, 6); + assert_eq!(packed.mint_1_decimals, 9); + assert_eq!(packed.lp_supply, 9999999); + assert_eq!(packed.protocol_fees_token_0, 444); + assert_eq!(packed.protocol_fees_token_1, 555); + assert_eq!(packed.fund_fees_token_0, 111); + assert_eq!(packed.fund_fees_token_1, 222); + assert_eq!(packed.open_time, 1700000000); + assert_eq!(packed.recent_epoch, 999); + assert_eq!(packed.padding[0], 42); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let pool_with_info = PoolState { + compression_info: Some(CompressionInfo::default()), + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = pool_with_info.pack(&mut packed_accounts); + + assert!( + packed.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let pool1 = PoolState { + compression_info: None, + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let pool2 = PoolState { + compression_info: None, + amm_config: Pubkey::new_unique(), + pool_creator: Pubkey::new_unique(), + token_0_vault: Pubkey::new_unique(), + token_1_vault: Pubkey::new_unique(), + lp_mint: Pubkey::new_unique(), + token_0_mint: Pubkey::new_unique(), + token_1_mint: Pubkey::new_unique(), + token_0_program: Pubkey::new_unique(), + token_1_program: Pubkey::new_unique(), + observation_key: Pubkey::new_unique(), + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = pool1.pack(&mut packed_accounts); + let packed2 = pool2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.amm_config, packed2.amm_config, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_stores_all_pubkeys_in_packed_accounts() { + let pubkeys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + + let pool = PoolState { + compression_info: None, + amm_config: pubkeys[0], + pool_creator: pubkeys[1], + token_0_vault: pubkeys[2], + token_1_vault: pubkeys[3], + lp_mint: pubkeys[4], + token_0_mint: pubkeys[5], + token_1_mint: pubkeys[6], + token_0_program: pubkeys[7], + token_1_program: pubkeys[8], + observation_key: pubkeys[9], + auth_bump: 0, + status: 0, + lp_mint_decimals: 9, + mint_0_decimals: 9, + mint_1_decimals: 6, + lp_supply: 0, + protocol_fees_token_0: 0, + protocol_fees_token_1: 0, + fund_fees_token_0: 0, + fund_fees_token_1: 0, + open_time: 0, + recent_epoch: 0, + padding: [0u64; 1], + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = pool.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 10, "should have 10 pubkeys stored"); + + // Verify each pubkey is stored at its index + for (i, expected_pubkey) in pubkeys.iter().enumerate() { + assert_eq!( + stored_pubkeys[i], *expected_pubkey, + "pubkey at index {} should match", + i + ); + } +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs new file mode 100644 index 0000000000..4f17335401 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs @@ -0,0 +1,489 @@ +//! Core Tests: GameSession trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `GameSession`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedGameSession +//! +//! GameSession has #[compress_as(start_time = 0, end_time = None, score = 0)] +//! which overrides field values during compression. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{GameSession, PackedGameSession}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for GameSession { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player: Pubkey::new_unique(), + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + session_id: 1, + player: Pubkey::new_unique(), + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(GameSession); + +// ============================================================================= +// Struct-Specific CompressAs Tests with Overrides +// ============================================================================= + +#[test] +fn test_compress_as_overrides_start_time() { + let player = Pubkey::new_unique(); + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.start_time, 0, + "compress_as should override start_time to 0" + ); +} + +#[test] +fn test_compress_as_overrides_end_time() { + let player = Pubkey::new_unique(); + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.end_time, None, + "compress_as should override end_time to None" + ); +} + +#[test] +fn test_compress_as_overrides_score() { + let player = Pubkey::new_unique(); + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.score, 0, + "compress_as should override score to 0" + ); +} + +#[test] +fn test_compress_as_preserves_session_id() { + let player = Pubkey::new_unique(); + let session_id = 999u64; + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.session_id, session_id, + "compress_as should preserve session_id" + ); +} + +#[test] +fn test_compress_as_preserves_player() { + let player = Pubkey::new_unique(); + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.player, player, + "compress_as should preserve player" + ); +} + +#[test] +fn test_compress_as_preserves_game_type() { + let player = Pubkey::new_unique(); + let game_type = "custom game".to_string(); + + let record = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player, + game_type: game_type.clone(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let compressed = record.compress_as(); + assert_eq!( + compressed.game_type, game_type, + "compress_as should preserve game_type" + ); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_session_id() { + let player = Pubkey::new_unique(); + + let record1 = GameSession { + compression_info: None, + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 2, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different session_id should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_player() { + let record1 = GameSession { + compression_info: None, + session_id: 1, + player: Pubkey::new_unique(), + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 1, + player: Pubkey::new_unique(), + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different player should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_game_type() { + let player = Pubkey::new_unique(); + + let record1 = GameSession { + compression_info: None, + session_id: 1, + player, + game_type: "game1".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 1, + player, + game_type: "game2".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different game_type should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_player() { + // Verify PackedGameSession has the expected structure + // The Packed struct uses the same field name but changes type to u8 + let packed = PackedGameSession { + compression_info: None, + session_id: 1, + player: 0, + game_type: "test".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + assert_eq!(packed.player, 0u8); + assert_eq!(packed.session_id, 1u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let player = Pubkey::new_unique(); + let record = GameSession { + compression_info: None, + session_id: 1, + player, + game_type: "test game".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The player should have been added to packed_accounts + // and packed.player should be the index (0 for first pubkey) + assert_eq!(packed.player, 0u8); + assert_eq!(packed.session_id, 1); + + let mut packed_accounts = PackedAccounts::default(); + packed_accounts.insert_or_get(Pubkey::new_unique()); + let packed = record.pack(&mut packed_accounts); + + // The player should have been added to packed_accounts + // and packed.player should be the index (1 for second pubkey) + assert_eq!(packed.player, 1u8); + assert_eq!(packed.session_id, 1); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let player = Pubkey::new_unique(); + + let record1 = GameSession { + compression_info: None, + session_id: 1, + player, + game_type: "game1".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 2, + player, + game_type: "game2".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed1.player, packed2.player, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = GameSession { + compression_info: None, + session_id: 1, + player: Pubkey::new_unique(), + game_type: "game1".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 2, + player: Pubkey::new_unique(), + game_type: "game2".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.player, packed2.player, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = GameSession { + compression_info: Some(CompressionInfo::default()), + session_id: 1, + player: Pubkey::new_unique(), + game_type: "test".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record_without_info = GameSession { + compression_info: None, + session_id: 2, + player: Pubkey::new_unique(), + game_type: "test".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + // Both packed structs should have compression_info = None + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let player1 = Pubkey::new_unique(); + let player2 = Pubkey::new_unique(); + + let record1 = GameSession { + compression_info: None, + session_id: 1, + player: player1, + game_type: "game1".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let record2 = GameSession { + compression_info: None, + session_id: 2, + player: player2, + game_type: "game2".to_string(), + start_time: 100, + end_time: Some(200), + score: 50, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Verify pubkeys are stored and retrievable + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.player as usize], player1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.player as usize], player2, + "second pubkey should match" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs new file mode 100644 index 0000000000..290f61a948 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs @@ -0,0 +1,401 @@ +//! Core Tests: PlaceholderRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `PlaceholderRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedPlaceholderRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedPlaceholderRecord, PlaceholderRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for PlaceholderRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(PlaceholderRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let name = "test placeholder".to_string(); + let placeholder_id = 42u64; + let counter = 999u32; + + let record = PlaceholderRecord { + compression_info: Some(CompressionInfo::default()), + owner, + name: name.clone(), + placeholder_id, + counter, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.name, name); + assert_eq!(compressed.placeholder_id, placeholder_id); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let name = "test placeholder".to_string(); + let placeholder_id = 5u64; + let counter = 123u32; + + let record = PlaceholderRecord { + compression_info: None, + owner, + name: name.clone(), + placeholder_id, + counter, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.name, name); + assert_eq!(compressed.placeholder_id, placeholder_id); + assert_eq!(compressed.counter, counter); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = PlaceholderRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_name() { + let owner = Pubkey::new_unique(); + + let record1 = PlaceholderRecord { + compression_info: None, + owner, + name: "placeholder1".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner, + name: "placeholder2".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different name should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_placeholder_id() { + let owner = Pubkey::new_unique(); + + let record1 = PlaceholderRecord { + compression_info: None, + owner, + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner, + name: "test placeholder".to_string(), + placeholder_id: 2, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different placeholder_id should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = PlaceholderRecord { + compression_info: None, + owner, + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 0, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner, + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 1, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + // Verify PackedPlaceholderRecord has the expected structure + // The Packed struct uses the same field name but changes type to u8 + let packed = PackedPlaceholderRecord { + compression_info: None, + owner: 0, + name: "test".to_string(), + placeholder_id: 1, + counter: 42, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.placeholder_id, 1u64); + assert_eq!(packed.counter, 42u32); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = PlaceholderRecord { + compression_info: None, + owner, + name: "test placeholder".to_string(), + placeholder_id: 1, + counter: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (0 for first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.placeholder_id, 1); + assert_eq!(packed.counter, 100); + + let mut packed_accounts = PackedAccounts::default(); + packed_accounts.insert_or_get(Pubkey::new_unique()); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (1 for second pubkey) + assert_eq!(packed.owner, 1u8); + assert_eq!(packed.placeholder_id, 1); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = PlaceholderRecord { + compression_info: None, + owner, + name: "placeholder1".to_string(), + placeholder_id: 1, + counter: 1, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner, + name: "placeholder2".to_string(), + placeholder_id: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = PlaceholderRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "placeholder1".to_string(), + placeholder_id: 1, + counter: 1, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "placeholder2".to_string(), + placeholder_id: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = PlaceholderRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + name: "test".to_string(), + placeholder_id: 1, + counter: 100, + }; + + let record_without_info = PlaceholderRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test".to_string(), + placeholder_id: 2, + counter: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + // Both packed structs should have compression_info = None + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = PlaceholderRecord { + compression_info: None, + owner: owner1, + name: "placeholder1".to_string(), + placeholder_id: 1, + counter: 1, + }; + + let record2 = PlaceholderRecord { + compression_info: None, + owner: owner2, + name: "placeholder2".to_string(), + placeholder_id: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Verify pubkeys are stored and retrievable + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs new file mode 100644 index 0000000000..54c482c9c0 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs @@ -0,0 +1,401 @@ +//! Core Tests: UserRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `UserRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedUserRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedUserRecord, UserRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for UserRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + name: "test user".to_string(), + score: 0, + category_id: 1, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test user".to_string(), + score: 0, + category_id: 1, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(UserRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let name = "test user".to_string(); + let score = 999u64; + let category_id = 42u64; + + let record = UserRecord { + compression_info: Some(CompressionInfo::default()), + owner, + name: name.clone(), + score, + category_id, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.name, name); + assert_eq!(compressed.score, score); + assert_eq!(compressed.category_id, category_id); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let name = "test user".to_string(); + let score = 123u64; + let category_id = 5u64; + + let record = UserRecord { + compression_info: None, + owner, + name: name.clone(), + score, + category_id, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.name, name); + assert_eq!(compressed.score, score); + assert_eq!(compressed.category_id, category_id); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = UserRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test user".to_string(), + score: 100, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test user".to_string(), + score: 100, + category_id: 1, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_name() { + let owner = Pubkey::new_unique(); + + let record1 = UserRecord { + compression_info: None, + owner, + name: "user1".to_string(), + score: 100, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner, + name: "user2".to_string(), + score: 100, + category_id: 1, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different name should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_score() { + let owner = Pubkey::new_unique(); + + let record1 = UserRecord { + compression_info: None, + owner, + name: "test user".to_string(), + score: 100, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner, + name: "test user".to_string(), + score: 200, + category_id: 1, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different score should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_category_id() { + let owner = Pubkey::new_unique(); + + let record1 = UserRecord { + compression_info: None, + owner, + name: "test user".to_string(), + score: 100, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner, + name: "test user".to_string(), + score: 100, + category_id: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different category_id should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + // Verify PackedUserRecord has the expected structure + // The Packed struct uses the same field name but changes type to u8 + let packed = PackedUserRecord { + compression_info: None, + owner: 0, + name: "test".to_string(), + score: 42, + category_id: 1, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.score, 42u64); + assert_eq!(packed.category_id, 1u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = UserRecord { + compression_info: None, + owner, + name: "test user".to_string(), + score: 100, + category_id: 1, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (0 for first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.score, 100); + assert_eq!(packed.category_id, 1); + + let mut packed_accounts = PackedAccounts::default(); + packed_accounts.insert_or_get(Pubkey::new_unique()); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (1 for second pubkey) + assert_eq!(packed.owner, 1u8); + assert_eq!(packed.score, 100); + assert_eq!(packed.category_id, 1); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = UserRecord { + compression_info: None, + owner, + name: "user1".to_string(), + score: 1, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner, + name: "user2".to_string(), + score: 2, + category_id: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = UserRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "user1".to_string(), + score: 1, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "user2".to_string(), + score: 2, + category_id: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = UserRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + name: "test".to_string(), + score: 100, + category_id: 1, + }; + + let record_without_info = UserRecord { + compression_info: None, + owner: Pubkey::new_unique(), + name: "test".to_string(), + score: 200, + category_id: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + // Both packed structs should have compression_info = None + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = UserRecord { + compression_info: None, + owner: owner1, + name: "user1".to_string(), + score: 1, + category_id: 1, + }; + + let record2 = UserRecord { + compression_info: None, + owner: owner2, + name: "user2".to_string(), + score: 2, + category_id: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Verify pubkeys are stored and retrievable + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs new file mode 100644 index 0000000000..d187dddf76 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs @@ -0,0 +1,583 @@ +//! D1 Tests: AllFieldTypesRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `AllFieldTypesRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedAllFieldTypesRecord +//! +//! Comprehensive test exercising all field type code paths: +//! - Multiple Pubkeys (owner, delegate, authority) -> u8 indices +//! - Option (close_authority) -> remains Option (NOT converted to u8) +//! - String (name) -> clone() path +//! - Arrays (hash) -> direct copy +//! - Option (end_time, enabled) -> unchanged +//! - Regular primitives (counter, flag) -> direct copy + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedAllFieldTypesRecord, AllFieldTypesRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for AllFieldTypesRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test name".to_string(), + hash: [0u8; 32], + end_time: Some(1000), + enabled: Some(true), + counter: 0, + flag: false, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test name".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 0, + flag: false, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(AllFieldTypesRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_all_field_types() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let close_authority = Some(Pubkey::new_unique()); + let name = "Alice".to_string(); + let mut hash = [0u8; 32]; + hash[0] = 42; + let end_time = Some(5000u64); + let enabled = Some(false); + let counter = 999u64; + let flag = true; + + let record = AllFieldTypesRecord { + compression_info: Some(CompressionInfo::default()), + owner, + delegate, + authority, + close_authority, + name: name.clone(), + hash, + end_time, + enabled, + counter, + flag, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.authority, authority); + assert_eq!(compressed.close_authority, close_authority); + assert_eq!(compressed.name, name); + assert_eq!(compressed.hash, hash); + assert_eq!(compressed.end_time, end_time); + assert_eq!(compressed.enabled, enabled); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let name = "Bob".to_string(); + let counter = 123u64; + + let record = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: name.clone(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter, + flag: false, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve all fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.name, name); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_pubkey_field() { + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate, + authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate, + authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!(hash1, hash2, "different owner should produce different hash"); +} + +#[test] +fn test_hash_differs_for_different_option_pubkey_field() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different close_authority (Some vs None) should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_string_field() { + let owner = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "Alice".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "Bob".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!(hash1, hash2, "different name should produce different hash"); +} + +#[test] +fn test_hash_differs_for_different_array_field() { + let owner = Pubkey::new_unique(); + let mut hash1_array = [0u8; 32]; + hash1_array[0] = 1; + + let mut hash2_array = [0u8; 32]; + hash2_array[0] = 2; + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: hash1_array, + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: hash2_array, + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!(hash1, hash2, "different hash array should produce different hash"); +} + +#[test] +fn test_hash_differs_for_different_option_primitive() { + let owner = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: Some(1000), + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: Some(2000), + enabled: None, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different end_time should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_primitive() { + let owner = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 200, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!(hash1, hash2, "different counter should produce different hash"); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_all_types_converted() { + // Verify PackedAllFieldTypesRecord has the correct field types + // Note: Option is NOT converted to Option - it stays as Option + let close_authority = Pubkey::new_unique(); + let packed = PackedAllFieldTypesRecord { + compression_info: None, + owner: 0, + delegate: 1, + authority: 2, + close_authority: Some(close_authority), + name: "test".to_string(), + hash: [0u8; 32], + end_time: Some(1000), + enabled: Some(true), + counter: 42, + flag: false, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + assert_eq!(packed.close_authority, Some(close_authority)); + assert_eq!(packed.name, "test".to_string()); + assert_eq!(packed.counter, 42u64); + assert_eq!(packed.flag, false); +} + +#[test] +fn test_pack_converts_all_pubkey_types() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let close_authority = Pubkey::new_unique(); + let name = "test".to_string(); + + let record = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: Some(close_authority), + name: name.clone(), + hash: [0u8; 32], + end_time: Some(1000), + enabled: Some(true), + counter: 100, + flag: true, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Direct Pubkey fields are converted to u8 indices + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + // Option is NOT converted to Option - it stays as Option + assert_eq!(packed.close_authority, Some(close_authority)); + assert_eq!(packed.name, name); + assert_eq!(packed.counter, 100); + assert_eq!(packed.flag, true); + + // Only direct Pubkey fields are stored in packed_accounts (not Option) + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 3); + assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[1], delegate); + assert_eq!(stored_pubkeys[2], authority); +} + +#[test] +fn test_pack_with_option_pubkey_none() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 100, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Only three pubkeys should have been added + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + assert_eq!(packed.close_authority, None, "Option::None should remain None"); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 3); +} + +#[test] +fn test_pack_reuses_pubkey_indices() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: "test1".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 1, + flag: false, + }; + + let record2 = AllFieldTypesRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: "test2".to_string(), + hash: [0u8; 32], + end_time: None, + enabled: None, + counter: 2, + flag: true, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkeys should get same indices + assert_eq!(packed1.owner, packed2.owner); + assert_eq!(packed1.delegate, packed2.delegate); + assert_eq!(packed1.authority, packed2.authority); + + // Should still only have 3 pubkeys total + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 3); +} + +#[test] +fn test_pack_preserves_non_pubkey_fields() { + let name = "AllFieldsTest".to_string(); + let mut hash = [0u8; 32]; + hash[0] = 99; + let end_time = Some(9999u64); + let enabled = Some(true); + let counter = 12345u64; + let flag = true; + + let record = AllFieldTypesRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: name.clone(), + hash, + end_time, + enabled, + counter, + flag, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // All non-Pubkey fields should be preserved + assert_eq!(packed.name, name); + assert_eq!(packed.hash, hash); + assert_eq!(packed.end_time, end_time); + assert_eq!(packed.enabled, enabled); + assert_eq!(packed.counter, counter); + assert_eq!(packed.flag, flag); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs new file mode 100644 index 0000000000..dc95cd2fa9 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs @@ -0,0 +1,264 @@ +//! D1 Tests: ArrayRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `ArrayRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack (identity implementation with array fields) +//! +//! Note: Since ArrayRecord has no Pubkey fields, the Pack trait generates an identity +//! implementation where Packed = Self. Array fields are directly copied in pack/unpack. +//! Therefore, no Pack/Unpack tests are needed. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::ArrayRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo}, +}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for ArrayRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + hash: [0u8; 32], + short_data: [0u8; 8], + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + hash: [0u8; 32], + short_data: [0u8; 8], + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(ArrayRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let mut hash = [0u8; 32]; + hash[0] = 1; + hash[31] = 255; + + let mut short_data = [0u8; 8]; + short_data[0] = 42; + short_data[7] = 99; + + let counter = 999u64; + + let record = ArrayRecord { + compression_info: Some(CompressionInfo::default()), + hash, + short_data, + counter, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.hash, hash); + assert_eq!(compressed.short_data, short_data); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let mut hash = [0u8; 32]; + hash[15] = 128; + + let mut short_data = [0u8; 8]; + short_data[3] = 77; + + let counter = 123u64; + + let record = ArrayRecord { + compression_info: None, + hash, + short_data, + counter, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.hash, hash); + assert_eq!(compressed.short_data, short_data); + assert_eq!(compressed.counter, counter); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let hash = [5u8; 32]; + let short_data = [10u8; 8]; + + let record1 = ArrayRecord { + compression_info: None, + hash, + short_data, + counter: 1, + }; + + let record2 = ArrayRecord { + compression_info: None, + hash, + short_data, + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_hash_array() { + let mut hash1_array = [0u8; 32]; + hash1_array[0] = 1; + + let mut hash2_array = [0u8; 32]; + hash2_array[0] = 2; + + let short_data = [10u8; 8]; + + let record1 = ArrayRecord { + compression_info: None, + hash: hash1_array, + short_data, + counter: 100, + }; + + let record2 = ArrayRecord { + compression_info: None, + hash: hash2_array, + short_data, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different hash array should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_short_data_array() { + let hash = [5u8; 32]; + + let mut short_data1 = [0u8; 8]; + short_data1[0] = 1; + + let mut short_data2 = [0u8; 8]; + short_data2[0] = 2; + + let record1 = ArrayRecord { + compression_info: None, + hash, + short_data: short_data1, + counter: 100, + }; + + let record2 = ArrayRecord { + compression_info: None, + hash, + short_data: short_data2, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different short_data array should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_array_position() { + let short_data = [10u8; 8]; + + let mut hash1_array = [0u8; 32]; + hash1_array[0] = 5; + + let mut hash2_array = [0u8; 32]; + hash2_array[31] = 5; // same value, different position + + let record1 = ArrayRecord { + compression_info: None, + hash: hash1_array, + short_data, + counter: 100, + }; + + let record2 = ArrayRecord { + compression_info: None, + hash: hash2_array, + short_data, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different array positions should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_zero_vs_nonzero_array() { + let zero_hash = [0u8; 32]; + let nonzero_hash = [1u8; 32]; + let short_data = [10u8; 8]; + + let record1 = ArrayRecord { + compression_info: None, + hash: zero_hash, + short_data, + counter: 100, + }; + + let record2 = ArrayRecord { + compression_info: None, + hash: nonzero_hash, + short_data, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "zero vs non-zero array should produce different hash" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs new file mode 100644 index 0000000000..f94f9cfc3c --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs @@ -0,0 +1,441 @@ +//! D1 Tests: MultiPubkeyRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `MultiPubkeyRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedMultiPubkeyRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedMultiPubkeyRecord, MultiPubkeyRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for MultiPubkeyRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(MultiPubkeyRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let amount = 999u64; + + let record = MultiPubkeyRecord { + compression_info: Some(CompressionInfo::default()), + owner, + delegate, + authority, + amount, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.authority, authority); + assert_eq!(compressed.amount, amount); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let amount = 123u64; + + let record = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.authority, authority); + assert_eq!(compressed.amount, amount); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_amount() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount: 1, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different amount should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate, + authority, + amount: 100, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate, + authority, + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_delegate() { + let owner = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority, + amount: 100, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate: Pubkey::new_unique(), + authority, + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different delegate should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_authority() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority: Pubkey::new_unique(), + amount: 100, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority: Pubkey::new_unique(), + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different authority should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_indices() { + // Verify PackedMultiPubkeyRecord has three u8 index fields + let packed = PackedMultiPubkeyRecord { + compression_info: None, + owner: 0, + delegate: 1, + authority: 2, + amount: 42, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + assert_eq!(packed.amount, 42u64); +} + +#[test] +fn test_pack_converts_all_pubkeys_to_indices() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // All three Pubkeys should have been added and packed should have their indices + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + assert_eq!(packed.amount, 100); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 3); + assert_eq!(stored_pubkeys[0], owner); + assert_eq!(stored_pubkeys[1], delegate); + assert_eq!(stored_pubkeys[2], authority); +} + +#[test] +fn test_pack_reuses_pubkey_indices() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount: 1, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner, + delegate, + authority, + amount: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkeys should get same indices + assert_eq!(packed1.owner, packed2.owner); + assert_eq!(packed1.delegate, packed2.delegate); + assert_eq!(packed1.authority, packed2.authority); + + // Should still only have 3 pubkeys total + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 3); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = MultiPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 1, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.owner, packed2.owner, + "different owner pubkeys should produce different indices" + ); + assert_ne!( + packed1.delegate, packed2.delegate, + "different delegate pubkeys should produce different indices" + ); + assert_ne!( + packed1.authority, packed2.authority, + "different authority pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = MultiPubkeyRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 100, + }; + + let record_without_info = MultiPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + amount: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + // Both packed structs should have compression_info = None + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_all_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let delegate1 = Pubkey::new_unique(); + let authority1 = Pubkey::new_unique(); + + let owner2 = Pubkey::new_unique(); + let delegate2 = Pubkey::new_unique(); + let authority2 = Pubkey::new_unique(); + + let record1 = MultiPubkeyRecord { + compression_info: None, + owner: owner1, + delegate: delegate1, + authority: authority1, + amount: 1, + }; + + let record2 = MultiPubkeyRecord { + compression_info: None, + owner: owner2, + delegate: delegate2, + authority: authority2, + amount: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Verify pubkeys are stored and retrievable + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 6, "should have 6 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first record owner should match" + ); + assert_eq!( + stored_pubkeys[packed1.delegate as usize], delegate1, + "first record delegate should match" + ); + assert_eq!( + stored_pubkeys[packed1.authority as usize], authority1, + "first record authority should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second record owner should match" + ); + assert_eq!( + stored_pubkeys[packed2.delegate as usize], delegate2, + "second record delegate should match" + ); + assert_eq!( + stored_pubkeys[packed2.authority as usize], authority2, + "second record authority should match" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs new file mode 100644 index 0000000000..009d022259 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs @@ -0,0 +1,173 @@ +//! D1 Tests: NoPubkeyRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `NoPubkeyRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack (identity implementation: PackedNoPubkeyRecord = NoPubkeyRecord) +//! +//! Note: Since NoPubkeyRecord has no Pubkey fields, the Pack trait generates an identity +//! implementation where Packed = Self. Therefore, no Pack/Unpack tests are needed - the +//! struct is packed as-is without transformation. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::NoPubkeyRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo}, +}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for NoPubkeyRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + counter: 0, + flag: false, + value: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + counter: 0, + flag: false, + value: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(NoPubkeyRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let counter = 999u64; + let flag = true; + let value = 42u32; + + let record = NoPubkeyRecord { + compression_info: Some(CompressionInfo::default()), + counter, + flag, + value, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); + assert_eq!(compressed.value, value); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let counter = 123u64; + let flag = false; + let value = 789u32; + + let record = NoPubkeyRecord { + compression_info: None, + counter, + flag, + value, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); + assert_eq!(compressed.value, value); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let record1 = NoPubkeyRecord { + compression_info: None, + counter: 1, + flag: true, + value: 100, + }; + + let record2 = NoPubkeyRecord { + compression_info: None, + counter: 2, + flag: true, + value: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_flag() { + let record1 = NoPubkeyRecord { + compression_info: None, + counter: 100, + flag: true, + value: 50, + }; + + let record2 = NoPubkeyRecord { + compression_info: None, + counter: 100, + flag: false, + value: 50, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different flag should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_value() { + let record1 = NoPubkeyRecord { + compression_info: None, + counter: 100, + flag: true, + value: 1, + }; + + let record2 = NoPubkeyRecord { + compression_info: None, + counter: 100, + flag: true, + value: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different value should produce different hash" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs new file mode 100644 index 0000000000..28e6d3f550 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs @@ -0,0 +1,223 @@ +//! D1 Tests: NonCopyRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `NonCopyRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack (identity implementation with clone() path) +//! +//! Note: Since NonCopyRecord has no Pubkey fields, the Pack trait generates an identity +//! implementation where Packed = Self. String fields use the clone() code path in pack/unpack. +//! Therefore, no Pack/Unpack tests are needed. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::NonCopyRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo}, +}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for NonCopyRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + name: "test name".to_string(), + description: "test description".to_string(), + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + name: "test name".to_string(), + description: "test description".to_string(), + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(NonCopyRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let name = "Alice".to_string(); + let description = "A test user".to_string(); + let counter = 999u64; + + let record = NonCopyRecord { + compression_info: Some(CompressionInfo::default()), + name: name.clone(), + description: description.clone(), + counter, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.name, name); + assert_eq!(compressed.description, description); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let name = "Bob".to_string(); + let description = "Another test user".to_string(); + let counter = 123u64; + + let record = NonCopyRecord { + compression_info: None, + name: name.clone(), + description: description.clone(), + counter, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.name, name); + assert_eq!(compressed.description, description); + assert_eq!(compressed.counter, counter); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let record1 = NonCopyRecord { + compression_info: None, + name: "test".to_string(), + description: "description".to_string(), + counter: 1, + }; + + let record2 = NonCopyRecord { + compression_info: None, + name: "test".to_string(), + description: "description".to_string(), + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_name() { + let record1 = NonCopyRecord { + compression_info: None, + name: "Alice".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let record2 = NonCopyRecord { + compression_info: None, + name: "Bob".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different name should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_description() { + let record1 = NonCopyRecord { + compression_info: None, + name: "test".to_string(), + description: "first description".to_string(), + counter: 100, + }; + + let record2 = NonCopyRecord { + compression_info: None, + name: "test".to_string(), + description: "second description".to_string(), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different description should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_string_length() { + let record1 = NonCopyRecord { + compression_info: None, + name: "a".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let record2 = NonCopyRecord { + compression_info: None, + name: "aa".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different string length should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_empty_vs_non_empty_string() { + let record1 = NonCopyRecord { + compression_info: None, + name: "".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let record2 = NonCopyRecord { + compression_info: None, + name: "name".to_string(), + description: "description".to_string(), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "empty vs non-empty string should produce different hash" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs new file mode 100644 index 0000000000..35db5c59e2 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs @@ -0,0 +1,241 @@ +//! D1 Tests: OptionPrimitiveRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `OptionPrimitiveRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack (identity implementation: PackedOptionPrimitiveRecord = OptionPrimitiveRecord) +//! +//! Note: Since OptionPrimitiveRecord has no Pubkey fields, the Pack trait generates an identity +//! implementation where Packed = Self. Option types remain unchanged in the packed +//! struct (not converted to Option). Therefore, no Pack/Unpack tests are needed. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::OptionPrimitiveRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo}, +}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for OptionPrimitiveRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + counter: 0, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + counter: 0, + end_time: None, + enabled: None, + score: None, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(OptionPrimitiveRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let counter = 999u64; + let end_time = Some(2000u64); + let enabled = Some(false); + let score = Some(100u32); + + let record = OptionPrimitiveRecord { + compression_info: Some(CompressionInfo::default()), + counter, + end_time, + enabled, + score, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.end_time, end_time); + assert_eq!(compressed.enabled, enabled); + assert_eq!(compressed.score, score); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let counter = 123u64; + let end_time = None; + let enabled = Some(true); + let score = None; + + let record = OptionPrimitiveRecord { + compression_info: None, + counter, + end_time, + enabled, + score, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.end_time, end_time); + assert_eq!(compressed.enabled, enabled); + assert_eq!(compressed.score, score); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let record1 = OptionPrimitiveRecord { + compression_info: None, + counter: 1, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let record2 = OptionPrimitiveRecord { + compression_info: None, + counter: 2, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_end_time() { + let record1 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let record2 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(2000), + enabled: Some(true), + score: Some(50), + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different end_time should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_enabled() { + let record1 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let record2 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(false), + score: Some(50), + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different enabled should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_score() { + let record1 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let record2 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(true), + score: Some(100), + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different score should produce different hash" + ); +} + +#[test] +fn test_hash_differs_when_option_is_none_vs_some() { + let record1 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: None, + enabled: Some(true), + score: Some(50), + }; + + let record2 = OptionPrimitiveRecord { + compression_info: None, + counter: 100, + end_time: Some(1000), + enabled: Some(true), + score: Some(50), + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "Option None vs Some should produce different hash" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs new file mode 100644 index 0000000000..dc78daca4e --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs @@ -0,0 +1,427 @@ +//! D1 Tests: OptionPubkeyRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `OptionPubkeyRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedOptionPubkeyRecord +//! +//! IMPORTANT: Option fields are NOT converted to Option in the packed struct. +//! Only direct Pubkey fields (like `owner: Pubkey`) are converted to u8 indices. +//! Option fields remain as Option in the packed struct. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::OptionPubkeyRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for OptionPubkeyRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Some(Pubkey::new_unique()), + close_authority: Some(Pubkey::new_unique()), + amount: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: None, + close_authority: None, + amount: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(OptionPubkeyRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let delegate = Some(Pubkey::new_unique()); + let close_authority = Some(Pubkey::new_unique()); + let amount = 999u64; + + let record = OptionPubkeyRecord { + compression_info: Some(CompressionInfo::default()), + owner, + delegate, + close_authority, + amount, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.close_authority, close_authority); + assert_eq!(compressed.amount, amount); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let delegate = Some(Pubkey::new_unique()); + let close_authority = None; + let amount = 123u64; + + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate, + close_authority, + amount, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.close_authority, close_authority); + assert_eq!(compressed.amount, amount); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_amount() { + let owner = Pubkey::new_unique(); + + let record1 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(Pubkey::new_unique()), + close_authority: None, + amount: 1, + }; + + let record2 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(Pubkey::new_unique()), + close_authority: None, + amount: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different amount should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = OptionPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: None, + close_authority: Some(Pubkey::new_unique()), + amount: 100, + }; + + let record2 = OptionPubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: None, + close_authority: Some(Pubkey::new_unique()), + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_delegate() { + let owner = Pubkey::new_unique(); + + let record1 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(Pubkey::new_unique()), + close_authority: None, + amount: 100, + }; + + let record2 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(Pubkey::new_unique()), + close_authority: None, + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different delegate should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_close_authority() { + let owner = Pubkey::new_unique(); + + let record1 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: Some(Pubkey::new_unique()), + amount: 100, + }; + + let record2 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: Some(Pubkey::new_unique()), + amount: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different close_authority should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_pack_converts_pubkey_fields_to_indices() { + // Verify that pack() converts Pubkey fields to u8 indices + // This test checks the Pack trait implementation + let owner = Pubkey::new_unique(); + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: None, + amount: 42, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The packed struct should have owner as u8 index (0 since it's first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, None); + assert_eq!(packed.close_authority, None); + assert_eq!(packed.amount, 42u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: None, + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added and packed.owner should be the index (0 for first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, None); + assert_eq!(packed.close_authority, None); + assert_eq!(packed.amount, 100); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], owner); +} + +#[test] +fn test_pack_preserves_option_pubkey_as_option_pubkey() { + // Option fields are NOT converted to Option + // They remain as Option in the packed struct + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(delegate), + close_authority: None, + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Direct Pubkey field is converted to u8 index + assert_eq!(packed.owner, 0u8); + // Option stays as Option - NOT converted to Option + assert_eq!(packed.delegate, Some(delegate)); + assert_eq!(packed.close_authority, None); + + // Only the direct Pubkey field (owner) is stored in packed_accounts + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], owner); +} + +#[test] +fn test_pack_option_pubkey_none_stays_none() { + // Option::None remains None in packed struct + let owner = Pubkey::new_unique(); + let close_authority = Pubkey::new_unique(); + + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: Some(close_authority), + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Direct Pubkey field is converted to u8 index + assert_eq!(packed.owner, 0u8); + // Option fields stay as Option - NOT converted to Option + assert_eq!(packed.delegate, None, "Option::None stays None"); + assert_eq!(packed.close_authority, Some(close_authority), "Option::Some stays Some"); + + // Only the direct Pubkey field (owner) is stored in packed_accounts + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], owner); +} + +#[test] +fn test_pack_all_option_pubkeys_some() { + // Tests that Option fields with Some values are preserved as-is + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let close_authority = Pubkey::new_unique(); + + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(delegate), + close_authority: Some(close_authority), + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Direct Pubkey field is converted to u8 index + assert_eq!(packed.owner, 0u8); + // Option fields stay as Option + assert_eq!(packed.delegate, Some(delegate)); + assert_eq!(packed.close_authority, Some(close_authority)); + + // Only the direct Pubkey field (owner) is stored in packed_accounts + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], owner); +} + +#[test] +fn test_pack_all_option_pubkeys_none() { + let owner = Pubkey::new_unique(); + + let record = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: None, + close_authority: None, + amount: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Only owner should have been added + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, None); + assert_eq!(packed.close_authority, None); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); + assert_eq!(stored_pubkeys[0], owner); +} + +#[test] +fn test_pack_reuses_same_pubkey_index_for_direct_fields() { + // Tests that the same Pubkey in the direct (non-Option) field gets the same index + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + + let record1 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(delegate), + close_authority: None, + amount: 1, + }; + + let record2 = OptionPubkeyRecord { + compression_info: None, + owner, + delegate: Some(delegate), + close_authority: None, + amount: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same direct Pubkey field should get same index + assert_eq!( + packed1.owner, packed2.owner, + "same owner should produce same index" + ); + // Option fields stay as Option (not converted to indices) + assert_eq!(packed1.delegate, packed2.delegate); + + // Only one pubkey stored (owner) since it's the only direct Pubkey field + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 1); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs new file mode 100644 index 0000000000..1371287936 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs @@ -0,0 +1,322 @@ +//! D1 Tests: SinglePubkeyRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `SinglePubkeyRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedSinglePubkeyRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedSinglePubkeyRecord, SinglePubkeyRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for SinglePubkeyRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(SinglePubkeyRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let counter = 999u64; + + let record = SinglePubkeyRecord { + compression_info: Some(CompressionInfo::default()), + owner, + counter, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let counter = 123u64; + + let record = SinglePubkeyRecord { + compression_info: None, + owner, + counter, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = SinglePubkeyRecord { + compression_info: None, + owner, + counter: 1, + }; + + let record2 = SinglePubkeyRecord { + compression_info: None, + owner, + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = SinglePubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 100, + }; + + let record2 = SinglePubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + // Verify PackedSinglePubkeyRecord has the expected structure + // The Packed struct uses the same field name but changes type to u8 + let packed = PackedSinglePubkeyRecord { + compression_info: None, + owner: 0, + counter: 42, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 42u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = SinglePubkeyRecord { + compression_info: None, + owner, + counter: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (0 for first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 100); + + let mut packed_accounts = PackedAccounts::default(); + packed_accounts.insert_or_get(Pubkey::new_unique()); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (0 for second pubkey) + assert_eq!(packed.owner, 1u8); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = SinglePubkeyRecord { + compression_info: None, + owner, + counter: 1, + }; + + let record2 = SinglePubkeyRecord { + compression_info: None, + owner, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = SinglePubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 1, + }; + + let record2 = SinglePubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + // Per the CompressiblePack design (see docs/rentfree.md lines 443-458), + // Pack always sets compression_info to None in the packed struct. + // This is intentional - compression_info is metadata for on-chain accounts, + // not needed in the compressed representation. + let record_with_info = SinglePubkeyRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + counter: 100, + }; + + let record_without_info = SinglePubkeyRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + // Both packed structs should have compression_info = None + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = SinglePubkeyRecord { + compression_info: None, + owner: owner1, + counter: 1, + }; + + let record2 = SinglePubkeyRecord { + compression_info: None, + owner: owner2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Verify pubkeys are stored and retrievable + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + // Pack records with unique pubkeys in sequence + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = SinglePubkeyRecord { + compression_info: None, + owner: *owner, + counter: 0, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + // Verify indices are assigned sequentially: 0, 1, 2, 3, 4 + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs new file mode 100644 index 0000000000..105445729b --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs @@ -0,0 +1,541 @@ +//! D2 Tests: AllCompressAsRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `AllCompressAsRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedAllCompressAsRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{AllCompressAsRecord, PackedAllCompressAsRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for AllCompressAsRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + time: 999, + score: 999, + cached: 999, + end: Some(999), + counter: 0, + flag: false, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + time: 999, + score: 999, + cached: 999, + end: Some(999), + counter: 0, + flag: false, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(AllCompressAsRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_overrides_numeric_fields() { + let owner = Pubkey::new_unique(); + let counter = 100u64; + let flag = true; + + let record = AllCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + time: 888, // Original value + score: 777, // Original value + cached: 666, // Original value + end: Some(999), + counter, + flag, + }; + + let compressed = record.compress_as(); + + // Per #[compress_as(time = 0, score = 0, cached = 0)]: + assert_eq!(compressed.time, 0, "time should be 0 after compress_as"); + assert_eq!(compressed.score, 0, "score should be 0 after compress_as"); + assert_eq!(compressed.cached, 0, "cached should be 0 after compress_as"); + + // Other fields should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +#[test] +fn test_compress_as_overrides_option_to_none() { + let owner = Pubkey::new_unique(); + let counter = 100u64; + + let record = AllCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + time: 100, + score: 100, + cached: 100, + end: Some(999), // Original value + counter, + flag: false, + }; + + let compressed = record.compress_as(); + + // Per #[compress_as(end = None)]: + assert_eq!(compressed.end, None, "end should be None after compress_as"); + + // Other fields should be correct + assert_eq!(compressed.time, 0); + assert_eq!(compressed.score, 0); + assert_eq!(compressed.cached, 0); +} + +#[test] +fn test_compress_as_preserves_non_overridden_fields() { + let owner = Pubkey::new_unique(); + let counter = 555u64; + let flag = true; + + let record = AllCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + time: 100, + score: 200, + cached: 300, + end: Some(400), + counter, + flag, + }; + + let compressed = record.compress_as(); + + // counter and flag have no compress_as override, should be preserved + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); + assert_eq!(compressed.owner, owner); +} + +#[test] +fn test_compress_as_all_overrides_together() { + let owner = Pubkey::new_unique(); + let counter = 777u64; + let flag = false; + + let record = AllCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + time: u64::MAX, + score: u64::MAX, + cached: u64::MAX, + end: Some(u64::MAX), + counter, + flag, + }; + + let compressed = record.compress_as(); + + // All overridden fields should be at their override values + assert_eq!(compressed.time, 0); + assert_eq!(compressed.score, 0); + assert_eq!(compressed.cached, 0); + assert_eq!(compressed.end, None); + + // Non-overridden fields should be preserved + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = AllCompressAsRecord { + compression_info: None, + owner, + time: 0, + score: 0, + cached: 0, + end: None, + counter: 1, + flag: false, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner, + time: 0, + score: 0, + cached: 0, + end: None, + counter: 2, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_flag() { + let owner = Pubkey::new_unique(); + + let record1 = AllCompressAsRecord { + compression_info: None, + owner, + time: 0, + score: 0, + cached: 0, + end: None, + counter: 0, + flag: true, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner, + time: 0, + score: 0, + cached: 0, + end: None, + counter: 0, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different flag should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_time() { + let owner = Pubkey::new_unique(); + + let record1 = AllCompressAsRecord { + compression_info: None, + owner, + time: 1, + score: 0, + cached: 0, + end: None, + counter: 0, + flag: false, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner, + time: 2, + score: 0, + cached: 0, + end: None, + counter: 0, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different time should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = AllCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + time: 100, + score: 100, + cached: 100, + end: Some(100), + counter: 100, + flag: false, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + time: 100, + score: 100, + cached: 100, + end: Some(100), + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + let packed = PackedAllCompressAsRecord { + compression_info: None, + owner: 0, + time: 42, + score: 43, + cached: 44, + end: None, + counter: 100, + flag: true, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.time, 42u64); + assert_eq!(packed.score, 43u64); + assert_eq!(packed.cached, 44u64); + assert_eq!(packed.end, None); + assert_eq!(packed.counter, 100u64); + assert_eq!(packed.flag, true); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = AllCompressAsRecord { + compression_info: None, + owner, + time: 50, + score: 60, + cached: 70, + end: Some(80), + counter: 100, + flag: true, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.time, 50); + assert_eq!(packed.score, 60); + assert_eq!(packed.cached, 70); + assert_eq!(packed.end, Some(80)); + assert_eq!(packed.counter, 100); + assert_eq!(packed.flag, true); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = AllCompressAsRecord { + compression_info: None, + owner, + time: 1, + score: 1, + cached: 1, + end: Some(1), + counter: 1, + flag: true, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner, + time: 2, + score: 2, + cached: 2, + end: Some(2), + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = AllCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + time: 1, + score: 1, + cached: 1, + end: Some(1), + counter: 1, + flag: true, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + time: 2, + score: 2, + cached: 2, + end: Some(2), + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = AllCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + time: 100, + score: 100, + cached: 100, + end: Some(100), + counter: 100, + flag: true, + }; + + let record_without_info = AllCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + time: 200, + score: 200, + cached: 200, + end: Some(200), + counter: 200, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = AllCompressAsRecord { + compression_info: None, + owner: owner1, + time: 1, + score: 1, + cached: 1, + end: Some(1), + counter: 1, + flag: true, + }; + + let record2 = AllCompressAsRecord { + compression_info: None, + owner: owner2, + time: 2, + score: 2, + cached: 2, + end: Some(2), + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = AllCompressAsRecord { + compression_info: None, + owner: *owner, + time: 0, + score: 0, + cached: 0, + end: None, + counter: 0, + flag: false, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs new file mode 100644 index 0000000000..8295f1aeb0 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs @@ -0,0 +1,453 @@ +//! D2 Tests: MultipleCompressAsRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `MultipleCompressAsRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedMultipleCompressAsRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for MultipleCompressAsRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + start: 999, + score: 999, + cached: 999, + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + start: 999, + score: 999, + cached: 999, + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(MultipleCompressAsRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_overrides_all_marked_fields() { + let owner = Pubkey::new_unique(); + let counter = 100u64; + + let record = MultipleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start: 888, // Original value + score: 777, // Original value + cached: 666, // Original value + counter, + }; + + let compressed = record.compress_as(); + + // Per #[compress_as(start = 0, score = 0, cached = 0)]: + assert_eq!(compressed.start, 0, "start should be 0 after compress_as"); + assert_eq!(compressed.score, 0, "score should be 0 after compress_as"); + assert_eq!(compressed.cached, 0, "cached should be 0 after compress_as"); + + // Fields without compress_as override should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_preserves_non_overridden_fields() { + let owner = Pubkey::new_unique(); + let counter = 555u64; + + let record = MultipleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start: 100, + score: 200, + cached: 300, + counter, + }; + + let compressed = record.compress_as(); + + // counter has no compress_as override, should be preserved + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.owner, owner); +} + +#[test] +fn test_compress_as_with_all_max_values() { + let owner = Pubkey::new_unique(); + + let record = MultipleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start: u64::MAX, + score: u64::MAX, + cached: u64::MAX, + counter: u64::MAX, + }; + + let compressed = record.compress_as(); + + // Overridden fields should still be 0 + assert_eq!(compressed.start, 0); + assert_eq!(compressed.score, 0); + assert_eq!(compressed.cached, 0); + // Non-overridden fields should be preserved + assert_eq!(compressed.counter, u64::MAX); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 0, + score: 0, + cached: 0, + counter: 1, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 0, + score: 0, + cached: 0, + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_start() { + let owner = Pubkey::new_unique(); + + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 1, + score: 0, + cached: 0, + counter: 0, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 2, + score: 0, + cached: 0, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different start should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_score() { + let owner = Pubkey::new_unique(); + + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 0, + score: 1, + cached: 0, + counter: 0, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 0, + score: 2, + cached: 0, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different score should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start: 100, + score: 100, + cached: 100, + counter: 100, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start: 100, + score: 100, + cached: 100, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + let packed = PackedMultipleCompressAsRecord { + compression_info: None, + owner: 0, + start: 42, + score: 43, + cached: 44, + counter: 100, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.start, 42u64); + assert_eq!(packed.score, 43u64); + assert_eq!(packed.cached, 44u64); + assert_eq!(packed.counter, 100u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 50, + score: 60, + cached: 70, + counter: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.start, 50); + assert_eq!(packed.score, 60); + assert_eq!(packed.cached, 70); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 1, + score: 1, + cached: 1, + counter: 1, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner, + start: 2, + score: 2, + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start: 1, + score: 1, + cached: 1, + counter: 1, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start: 2, + score: 2, + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = MultipleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + start: 100, + score: 100, + cached: 100, + counter: 100, + }; + + let record_without_info = MultipleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start: 200, + score: 200, + cached: 200, + counter: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = MultipleCompressAsRecord { + compression_info: None, + owner: owner1, + start: 1, + score: 1, + cached: 1, + counter: 1, + }; + + let record2 = MultipleCompressAsRecord { + compression_info: None, + owner: owner2, + start: 2, + score: 2, + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = MultipleCompressAsRecord { + compression_info: None, + owner: *owner, + start: 0, + score: 0, + cached: 0, + counter: 0, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs new file mode 100644 index 0000000000..3d6d52b7cb --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs @@ -0,0 +1,372 @@ +//! D2 Tests: NoCompressAsRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `NoCompressAsRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedNoCompressAsRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{NoCompressAsRecord, PackedNoCompressAsRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for NoCompressAsRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + counter: 0, + flag: false, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 0, + flag: false, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(NoCompressAsRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_all_fields() { + let owner = Pubkey::new_unique(); + let counter = 123u64; + let flag = true; + + let record = NoCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + counter, + flag, + }; + + let compressed = record.compress_as(); + + // No compress_as attribute, all fields should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +#[test] +fn test_compress_as_with_multiple_flag_values() { + let owner = Pubkey::new_unique(); + let counter = 555u64; + + for flag_val in &[true, false] { + let record = NoCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + counter, + flag: *flag_val, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.flag, *flag_val, "flag should be preserved"); + assert_eq!(compressed.counter, counter, "counter should be preserved"); + } +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let owner = Pubkey::new_unique(); + let counter = 789u64; + let flag = true; + + let record = NoCompressAsRecord { + compression_info: None, + owner, + counter, + flag, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve all fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 1, + flag: false, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 2, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_flag() { + let owner = Pubkey::new_unique(); + + let record1 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 100, + flag: true, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different flag should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = NoCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 100, + flag: false, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 100, + flag: false, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + let packed = PackedNoCompressAsRecord { + compression_info: None, + owner: 0, + counter: 42, + flag: true, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 42u64); + assert_eq!(packed.flag, true); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = NoCompressAsRecord { + compression_info: None, + owner, + counter: 100, + flag: true, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 100); + assert_eq!(packed.flag, true); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 1, + flag: true, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner, + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = NoCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 1, + flag: true, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = NoCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + counter: 100, + flag: true, + }; + + let record_without_info = NoCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + counter: 200, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = NoCompressAsRecord { + compression_info: None, + owner: owner1, + counter: 1, + flag: true, + }; + + let record2 = NoCompressAsRecord { + compression_info: None, + owner: owner2, + counter: 2, + flag: false, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = NoCompressAsRecord { + compression_info: None, + owner: *owner, + counter: 0, + flag: false, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs new file mode 100644 index 0000000000..480a2834a1 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs @@ -0,0 +1,451 @@ +//! D2 Tests: OptionNoneCompressAsRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `OptionNoneCompressAsRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedOptionNoneCompressAsRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{ + OptionNoneCompressAsRecord, PackedOptionNoneCompressAsRecord, +}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for OptionNoneCompressAsRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + start_time: 0, + end_time: Some(999), + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 0, + end_time: Some(999), + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(OptionNoneCompressAsRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_overrides_end_time_to_none() { + let owner = Pubkey::new_unique(); + let start_time = 100u64; + let counter = 50u64; + + let record = OptionNoneCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start_time, + end_time: Some(999), // Original value + counter, + }; + + let compressed = record.compress_as(); + + // Per #[compress_as(end_time = None)], end_time should be None in compressed form + assert_eq!(compressed.end_time, None, "end_time should be None after compress_as"); + // Other fields should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.start_time, start_time); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_with_end_time_already_none() { + let owner = Pubkey::new_unique(); + let start_time = 200u64; + let counter = 75u64; + + let record = OptionNoneCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start_time, + end_time: None, // Already None + counter, + }; + + let compressed = record.compress_as(); + + // Should remain None + assert_eq!(compressed.end_time, None); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.start_time, start_time); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_preserves_start_time_and_counter() { + let owner = Pubkey::new_unique(); + let start_time = 555u64; + let counter = 777u64; + + let record = OptionNoneCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start_time, + end_time: Some(u64::MAX), + counter, + }; + + let compressed = record.compress_as(); + + // start_time and counter have no compress_as override, should be preserved + assert_eq!(compressed.start_time, start_time); + assert_eq!(compressed.counter, counter); + // end_time should be None + assert_eq!(compressed.end_time, None); +} + +#[test] +fn test_compress_as_with_various_end_time_values() { + let owner = Pubkey::new_unique(); + + for end_val in &[Some(0u64), Some(100), Some(999), Some(u64::MAX), None] { + let record = OptionNoneCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + start_time: 0, + end_time: *end_val, + counter: 0, + }; + + let compressed = record.compress_as(); + // All should compress end_time to None + assert_eq!(compressed.end_time, None, "end_time should always be None after compress_as"); + } +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 0, + end_time: None, + counter: 1, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 0, + end_time: None, + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_start_time() { + let owner = Pubkey::new_unique(); + + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 1, + end_time: None, + counter: 0, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 2, + end_time: None, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different start_time should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_end_time() { + let owner = Pubkey::new_unique(); + + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 0, + end_time: Some(1), + counter: 0, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 0, + end_time: Some(2), + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different end_time should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 100, + end_time: Some(100), + counter: 100, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 100, + end_time: Some(100), + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + let packed = PackedOptionNoneCompressAsRecord { + compression_info: None, + owner: 0, + start_time: 42, + end_time: None, + counter: 100, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.start_time, 42u64); + assert_eq!(packed.end_time, None); + assert_eq!(packed.counter, 100u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 50, + end_time: Some(100), + counter: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.start_time, 50); + assert_eq!(packed.end_time, Some(100)); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 1, + end_time: Some(1), + counter: 1, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner, + start_time: 2, + end_time: Some(2), + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 1, + end_time: Some(1), + counter: 1, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 2, + end_time: Some(2), + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = OptionNoneCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + start_time: 100, + end_time: Some(100), + counter: 100, + }; + + let record_without_info = OptionNoneCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + start_time: 200, + end_time: Some(200), + counter: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = OptionNoneCompressAsRecord { + compression_info: None, + owner: owner1, + start_time: 1, + end_time: Some(1), + counter: 1, + }; + + let record2 = OptionNoneCompressAsRecord { + compression_info: None, + owner: owner2, + start_time: 2, + end_time: Some(2), + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = OptionNoneCompressAsRecord { + compression_info: None, + owner: *owner, + start_time: 0, + end_time: None, + counter: 0, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs new file mode 100644 index 0000000000..9b28833e42 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs @@ -0,0 +1,365 @@ +//! D2 Tests: SingleCompressAsRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `SingleCompressAsRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedSingleCompressAsRecord + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{PackedSingleCompressAsRecord, SingleCompressAsRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for SingleCompressAsRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + cached: 999, + counter: 0, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 999, + counter: 0, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(SingleCompressAsRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_overrides_cached_to_zero() { + let owner = Pubkey::new_unique(); + let counter = 100u64; + + let record = SingleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + cached: 999, // Original value + counter, + }; + + let compressed = record.compress_as(); + // Per #[compress_as(cached = 0)], cached should be 0 in compressed form + assert_eq!(compressed.cached, 0, "cached should be 0 after compress_as"); + // Other fields should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_preserves_counter() { + let owner = Pubkey::new_unique(); + let counter = 555u64; + + let record = SingleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + cached: 999, + counter, + }; + + let compressed = record.compress_as(); + // counter has no compress_as override, should be preserved + assert_eq!(compressed.counter, counter); +} + +#[test] +fn test_compress_as_with_multiple_cached_values() { + let owner = Pubkey::new_unique(); + + for cached_val in &[0u64, 100, 999, u64::MAX] { + let record = SingleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner, + cached: *cached_val, + counter: 0, + }; + + let compressed = record.compress_as(); + // All should compress cached to 0 + assert_eq!(compressed.cached, 0, "cached should always be 0 after compress_as"); + } +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 0, + counter: 1, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 0, + counter: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_cached() { + let owner = Pubkey::new_unique(); + + let record1 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 1, + counter: 0, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 2, + counter: 0, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different cached value should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = SingleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 100, + counter: 100, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 100, + counter: 100, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + let packed = PackedSingleCompressAsRecord { + compression_info: None, + owner: 0, + cached: 42, + counter: 100, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.cached, 42u64); + assert_eq!(packed.counter, 100u64); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 50, + counter: 100, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.cached, 50); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 1, + counter: 1, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner, + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = SingleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 1, + counter: 1, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record_with_info = SingleCompressAsRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + cached: 100, + counter: 100, + }; + + let record_without_info = SingleCompressAsRecord { + compression_info: None, + owner: Pubkey::new_unique(), + cached: 200, + counter: 200, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record_with_info.pack(&mut packed_accounts); + let packed2 = record_without_info.pack(&mut packed_accounts); + + assert!( + packed1.compression_info.is_none(), + "pack should set compression_info to None" + ); + assert!( + packed2.compression_info.is_none(), + "pack should set compression_info to None" + ); +} + +#[test] +fn test_pack_stores_pubkeys_in_packed_accounts() { + let owner1 = Pubkey::new_unique(); + let owner2 = Pubkey::new_unique(); + + let record1 = SingleCompressAsRecord { + compression_info: None, + owner: owner1, + cached: 1, + counter: 1, + }; + + let record2 = SingleCompressAsRecord { + compression_info: None, + owner: owner2, + cached: 2, + counter: 2, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 pubkeys stored"); + assert_eq!( + stored_pubkeys[packed1.owner as usize], owner1, + "first pubkey should match" + ); + assert_eq!( + stored_pubkeys[packed2.owner as usize], owner2, + "second pubkey should match" + ); +} + +#[test] +fn test_pack_index_assignment_order() { + let mut packed_accounts = PackedAccounts::default(); + + let owners: Vec = (0..5).map(|_| Pubkey::new_unique()).collect(); + let mut indices = Vec::new(); + + for owner in &owners { + let record = SingleCompressAsRecord { + compression_info: None, + owner: *owner, + cached: 0, + counter: 0, + }; + let packed = record.pack(&mut packed_accounts); + indices.push(packed.owner); + } + + assert_eq!(indices, vec![0, 1, 2, 3, 4], "indices should be sequential"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs new file mode 100644 index 0000000000..c9d22b0dd6 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs @@ -0,0 +1,509 @@ +//! D4 Tests: AllCompositionRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `AllCompositionRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedAllCompositionRecord +//! +//! AllCompositionRecord has 3 Pubkey fields + 1 Option field and uses +//! #[compress_as(cached_time = 0, end_time = None)] to override field values. +//! This tests full Pack/Unpack behavior with compress_as attribute overrides. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{AllCompositionRecord, PackedAllCompositionRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for AllCompositionRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(AllCompositionRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests with Attribute Overrides +// ============================================================================= + +#[test] +fn test_compress_as_overrides_cached_time() { + // #[compress_as(cached_time = 0, ...)] should set cached_time to 0 + let record = AllCompositionRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 999, // This should be overridden to 0 + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let compressed = record.compress_as(); + + // cached_time should be 0 due to #[compress_as(cached_time = 0)] + assert_eq!( + compressed.cached_time, 0, + "compress_as should override cached_time to 0" + ); +} + +#[test] +fn test_compress_as_overrides_end_time() { + // #[compress_as(..., end_time = None)] should set end_time to None + let record = AllCompositionRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: Some(999), // This should be overridden to None + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let compressed = record.compress_as(); + + // end_time should be None due to #[compress_as(..., end_time = None)] + assert!( + compressed.end_time.is_none(), + "compress_as should override end_time to None" + ); +} + +#[test] +fn test_compress_as_preserves_start_time() { + // start_time is NOT in #[compress_as(...)], so it should NOT be overridden + let start_time_value = 777u64; + + let record = AllCompositionRecord { + compression_info: Some(CompressionInfo::default()), + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: start_time_value, + cached_time: 200, + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let compressed = record.compress_as(); + + // start_time should be preserved because it's not in the #[compress_as(...)] + assert_eq!( + compressed.start_time, start_time_value, + "compress_as should NOT override start_time (not in compress_as attribute)" + ); +} + +#[test] +fn test_compress_as_preserves_non_override_fields() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let record = AllCompositionRecord { + compression_info: Some(CompressionInfo::default()), + owner, + delegate, + authority, + close_authority: Some(Pubkey::new_unique()), + name: "custom_name".to_string(), + hash: [5u8; 32], + start_time: 500, + cached_time: 600, + end_time: Some(700), + counter_1: 11, + counter_2: 22, + counter_3: 33, + flag_1: false, + flag_2: true, + score: Some(99), + }; + + let compressed = record.compress_as(); + + // Fields not in compress_as should be preserved + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.delegate, delegate); + assert_eq!(compressed.authority, authority); + assert_eq!(compressed.counter_1, 11); + assert_eq!(compressed.counter_2, 22); + assert_eq!(compressed.counter_3, 33); + assert_eq!(compressed.flag_1, false); + assert_eq!(compressed.flag_2, true); + assert_eq!(compressed.score, Some(99)); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = AllCompositionRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: None, + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: None, + }; + + let record2 = AllCompositionRecord { + compression_info: None, + owner: Pubkey::new_unique(), + delegate: record1.delegate, + authority: record1.authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: None, + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: None, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_counter_3() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + + let mut record1 = AllCompositionRecord { + compression_info: None, + owner, + delegate, + authority, + close_authority: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: None, + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: None, + }; + + let mut record2 = record1.clone(); + record2.counter_3 = 999; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter_3 should produce different hash" + ); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_pubkey_fields() { + // Verify PackedAllCompositionRecord has direct Pubkey fields as u8 + // Note: Option is NOT converted to Option - it stays as Option + let close_authority = Pubkey::new_unique(); + let packed = PackedAllCompositionRecord { + owner: 0, + delegate: 1, + authority: 2, + close_authority: Some(close_authority), + compression_info: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 0, // overridden by compress_as + end_time: None, // overridden by compress_as + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.delegate, 1u8); + assert_eq!(packed.authority, 2u8); + assert_eq!(packed.close_authority, Some(close_authority)); +} + +#[test] +fn test_pack_converts_all_pubkeys_to_indices() { + let owner = Pubkey::new_unique(); + let delegate = Pubkey::new_unique(); + let authority = Pubkey::new_unique(); + let close_authority = Pubkey::new_unique(); + + let record = AllCompositionRecord { + owner, + delegate, + authority, + close_authority: Some(close_authority), + compression_info: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Direct Pubkey fields are converted to u8 indices + assert_eq!(packed.owner, 0u8); // First pubkey + assert_eq!(packed.delegate, 1u8); // Second pubkey + assert_eq!(packed.authority, 2u8); // Third pubkey + // Option is NOT converted to Option - it stays as Option + assert_eq!(packed.close_authority, Some(close_authority)); +} + +#[test] +fn test_pack_does_not_apply_compress_as_overrides() { + // Note: Pack does NOT apply compress_as overrides. Those are only applied + // by the CompressAs trait's compress_as() method. If you need overrides + // applied, call compress_as() first, then pack() the result. + let close_authority = Pubkey::new_unique(); + let record = AllCompositionRecord { + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(close_authority), + compression_info: Some(CompressionInfo::default()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 999, + end_time: Some(999), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // Pack preserves field values - compress_as overrides are NOT applied + assert_eq!(packed.cached_time, 999, "pack preserves cached_time value"); + assert_eq!(packed.end_time, Some(999), "pack preserves end_time value"); + // Option stays as Option + assert_eq!(packed.close_authority, Some(close_authority)); +} + +#[test] +fn test_pack_preserves_start_time_without_override() { + let start_time_value = 555u64; + + let record = AllCompositionRecord { + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: None, + compression_info: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: start_time_value, + cached_time: 200, + end_time: None, + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert_eq!( + packed.start_time, start_time_value, + "pack should preserve start_time (not in compress_as override)" + ); +} + +#[test] +fn test_pack_reuses_duplicate_pubkeys_for_direct_fields() { + // Test that same Pubkey used in multiple direct Pubkey fields gets same index + let shared_pubkey = Pubkey::new_unique(); + + let record1 = AllCompositionRecord { + owner: shared_pubkey, + delegate: shared_pubkey, // Same as owner + authority: Pubkey::new_unique(), + close_authority: Some(shared_pubkey), // Option is NOT packed + compression_info: None, + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: None, + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record1.pack(&mut packed_accounts); + + // owner and delegate are the same pubkey, should get the same index + assert_eq!(packed.owner, packed.delegate, "same pubkey should get same index"); + + // Option is NOT converted to Option - it stays as Option + assert_eq!(packed.close_authority, Some(shared_pubkey)); + + // Only 2 unique pubkeys stored (shared_pubkey and authority) + let stored_pubkeys = packed_accounts.packed_pubkeys(); + assert_eq!(stored_pubkeys.len(), 2, "should have 2 unique pubkeys"); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let record = AllCompositionRecord { + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(Pubkey::new_unique()), + compression_info: Some(CompressionInfo::default()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 200, + end_time: Some(300), + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + assert!( + packed.compression_info.is_none(), + "pack should set compression_info to None" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs new file mode 100644 index 0000000000..6f8b9f13b3 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs @@ -0,0 +1,319 @@ +//! D4 Tests: InfoLastRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `InfoLastRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! - CompressiblePack -> Pack + Unpack + PackedInfoLastRecord +//! +//! InfoLastRecord has 1 Pubkey field (owner) and demonstrates that +//! compression_info can be placed in non-first position (ordering test). + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::{InfoLastRecord, PackedInfoLastRecord}; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + compressible::{CompressAs, CompressionInfo, Pack}, + instruction::PackedAccounts, +}; +use solana_pubkey::Pubkey; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for InfoLastRecord { + fn with_compression_info() -> Self { + Self { + owner: Pubkey::new_unique(), + counter: 0, + flag: false, + compression_info: Some(CompressionInfo::default()), + } + } + + fn without_compression_info() -> Self { + Self { + owner: Pubkey::new_unique(), + counter: 0, + flag: false, + compression_info: None, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(InfoLastRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_other_fields() { + let owner = Pubkey::new_unique(); + let counter = 999u64; + let flag = true; + + let record = InfoLastRecord { + owner, + counter, + flag, + compression_info: Some(CompressionInfo::default()), + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, counter); + assert_eq!(compressed.flag, flag); +} + +#[test] +fn test_compress_as_preserves_all_field_types() { + let owner = Pubkey::new_unique(); + + let record = InfoLastRecord { + owner, + counter: 42, + flag: true, + compression_info: Some(CompressionInfo::default()), + }; + + let compressed = record.compress_as(); + + // Verify all fields are preserved despite compression_info being last + assert_eq!(compressed.owner, owner); + assert_eq!(compressed.counter, 42); + assert_eq!(compressed.flag, true); + assert!(compressed.compression_info.is_none()); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_counter() { + let owner = Pubkey::new_unique(); + + let record1 = InfoLastRecord { + owner, + counter: 1, + flag: false, + compression_info: None, + }; + + let record2 = InfoLastRecord { + owner, + counter: 2, + flag: false, + compression_info: None, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_owner() { + let record1 = InfoLastRecord { + owner: Pubkey::new_unique(), + counter: 100, + flag: false, + compression_info: None, + }; + + let record2 = InfoLastRecord { + owner: Pubkey::new_unique(), + counter: 100, + flag: false, + compression_info: None, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_flag() { + let owner = Pubkey::new_unique(); + + let record1 = InfoLastRecord { + owner, + counter: 50, + flag: true, + compression_info: None, + }; + + let record2 = InfoLastRecord { + owner, + counter: 50, + flag: false, + compression_info: None, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!(hash1, hash2, "different flag should produce different hash"); +} + +// ============================================================================= +// Pack/Unpack Tests (struct-specific, cannot be generic) +// ============================================================================= + +#[test] +fn test_packed_struct_has_u8_owner() { + // Verify PackedInfoLastRecord has the expected structure + // The Packed struct uses the same field name but changes type to u8 + let packed = PackedInfoLastRecord { + owner: 0, + counter: 42, + flag: true, + compression_info: None, + }; + + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 42u64); + assert_eq!(packed.flag, true); +} + +#[test] +fn test_pack_converts_pubkey_to_index() { + let owner = Pubkey::new_unique(); + let record = InfoLastRecord { + owner, + counter: 100, + flag: false, + compression_info: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (0 for first pubkey) + assert_eq!(packed.owner, 0u8); + assert_eq!(packed.counter, 100); + assert_eq!(packed.flag, false); + + let mut packed_accounts = PackedAccounts::default(); + packed_accounts.insert_or_get(Pubkey::new_unique()); + let packed = record.pack(&mut packed_accounts); + + // The owner should have been added to packed_accounts + // and packed.owner should be the index (1 for second pubkey) + assert_eq!(packed.owner, 1u8); + assert_eq!(packed.counter, 100); +} + +#[test] +fn test_pack_reuses_same_pubkey_index() { + let owner = Pubkey::new_unique(); + + let record1 = InfoLastRecord { + owner, + counter: 1, + flag: true, + compression_info: None, + }; + + let record2 = InfoLastRecord { + owner, + counter: 2, + flag: false, + compression_info: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Same pubkey should get same index + assert_eq!( + packed1.owner, packed2.owner, + "same pubkey should produce same index" + ); +} + +#[test] +fn test_pack_preserves_counter_and_flag() { + let owner = Pubkey::new_unique(); + let counter = 777u64; + let flag = true; + + let record = InfoLastRecord { + owner, + counter, + flag, + compression_info: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record.pack(&mut packed_accounts); + + // counter and flag should be preserved + assert_eq!(packed.counter, counter); + assert_eq!(packed.flag, flag); +} + +#[test] +fn test_pack_sets_compression_info_to_none() { + let owner = Pubkey::new_unique(); + + let record_with_info = InfoLastRecord { + owner, + counter: 100, + flag: true, + compression_info: Some(CompressionInfo::default()), + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed = record_with_info.pack(&mut packed_accounts); + + assert!( + packed.compression_info.is_none(), + "pack should set compression_info to None (even if input has Some)" + ); +} + +#[test] +fn test_pack_different_pubkeys_get_different_indices() { + let record1 = InfoLastRecord { + owner: Pubkey::new_unique(), + counter: 1, + flag: true, + compression_info: None, + }; + + let record2 = InfoLastRecord { + owner: Pubkey::new_unique(), + counter: 2, + flag: false, + compression_info: None, + }; + + let mut packed_accounts = PackedAccounts::default(); + let packed1 = record1.pack(&mut packed_accounts); + let packed2 = record2.pack(&mut packed_accounts); + + // Different pubkeys should get different indices + assert_ne!( + packed1.owner, packed2.owner, + "different pubkeys should produce different indices" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs new file mode 100644 index 0000000000..49117be7ca --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs @@ -0,0 +1,220 @@ +//! D4 Tests: LargeRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `LargeRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! +//! LargeRecord has NO Pubkey fields and 12 u64 fields (13 total including compression_info). +//! This exercises the SHA256 hash mode for large structs. +//! Pack/Unpack traits are NOT generated because there are no Pubkey fields. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::LargeRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for LargeRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + field_01: 1, + field_02: 2, + field_03: 3, + field_04: 4, + field_05: 5, + field_06: 6, + field_07: 7, + field_08: 8, + field_09: 9, + field_10: 10, + field_11: 11, + field_12: 12, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + field_01: 1, + field_02: 2, + field_03: 3, + field_04: 4, + field_05: 5, + field_06: 6, + field_07: 7, + field_08: 8, + field_09: 9, + field_10: 10, + field_11: 11, + field_12: 12, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(LargeRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_all_fields() { + let record = LargeRecord { + compression_info: Some(CompressionInfo::default()), + field_01: 100, + field_02: 200, + field_03: 300, + field_04: 400, + field_05: 500, + field_06: 600, + field_07: 700, + field_08: 800, + field_09: 900, + field_10: 1000, + field_11: 1100, + field_12: 1200, + }; + + let compressed = record.compress_as(); + + // Verify all fields are preserved + assert_eq!(compressed.field_01, 100); + assert_eq!(compressed.field_02, 200); + assert_eq!(compressed.field_03, 300); + assert_eq!(compressed.field_04, 400); + assert_eq!(compressed.field_05, 500); + assert_eq!(compressed.field_06, 600); + assert_eq!(compressed.field_07, 700); + assert_eq!(compressed.field_08, 800); + assert_eq!(compressed.field_09, 900); + assert_eq!(compressed.field_10, 1000); + assert_eq!(compressed.field_11, 1100); + assert_eq!(compressed.field_12, 1200); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let record = LargeRecord { + compression_info: None, + field_01: 1, + field_02: 2, + field_03: 3, + field_04: 4, + field_05: 5, + field_06: 6, + field_07: 7, + field_08: 8, + field_09: 9, + field_10: 10, + field_11: 11, + field_12: 12, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve all fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.field_01, 1); + assert_eq!(compressed.field_12, 12); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests (SHA256 mode) +// ============================================================================= + +#[test] +fn test_hash_produces_32_bytes_for_large_struct() { + let record = LargeRecord::without_compression_info(); + let hash = record.hash::().expect("hash should succeed"); + assert_eq!(hash.len(), 32, "SHA256 hash should produce 32 bytes"); +} + +#[test] +fn test_hash_differs_for_different_field_01() { + let mut record1 = LargeRecord::without_compression_info(); + let mut record2 = LargeRecord::without_compression_info(); + + record1.field_01 = 100; + record2.field_01 = 200; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different field_01 should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_field_06() { + let mut record1 = LargeRecord::without_compression_info(); + let mut record2 = LargeRecord::without_compression_info(); + + record1.field_06 = 600; + record2.field_06 = 700; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different field_06 should produce different hash" + ); +} + +#[test] +fn test_hash_differs_for_different_field_12() { + let mut record1 = LargeRecord::without_compression_info(); + let mut record2 = LargeRecord::without_compression_info(); + + record1.field_12 = 1200; + record2.field_12 = 1300; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different field_12 should produce different hash" + ); +} + +#[test] +fn test_hash_same_for_same_large_struct() { + let record1 = LargeRecord::without_compression_info(); + let record2 = record1.clone(); + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_eq!(hash1, hash2, "identical large records should produce same hash"); +} + +#[test] +fn test_hash_includes_all_fields_by_changing_middle_field() { + let mut record1 = LargeRecord::without_compression_info(); + let mut record2 = LargeRecord::without_compression_info(); + + // Change a field in the middle + record1.field_06 = 600; + record2.field_06 = 999; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "changing middle field should change hash (all fields included)" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs new file mode 100644 index 0000000000..bb074b49bf --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs @@ -0,0 +1,118 @@ +//! D4 Tests: MinimalRecord trait derive tests +//! +//! Tests each trait derived by `RentFreeAccount` macro for `MinimalRecord`: +//! - LightHasherSha -> DataHasher + ToByteArray +//! - LightDiscriminator -> LIGHT_DISCRIMINATOR constant +//! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace +//! +//! MinimalRecord has NO Pubkey fields, so Pack/Unpack traits are NOT generated. + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; +use csdk_anchor_full_derived_test::MinimalRecord; +use light_hasher::{DataHasher, Sha256}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +// ============================================================================= +// Factory Implementation +// ============================================================================= + +impl CompressibleTestFactory for MinimalRecord { + fn with_compression_info() -> Self { + Self { + compression_info: Some(CompressionInfo::default()), + value: 42u64, + } + } + + fn without_compression_info() -> Self { + Self { + compression_info: None, + value: 42u64, + } + } +} + +// ============================================================================= +// Generate all generic trait tests via macro +// ============================================================================= + +generate_trait_tests!(MinimalRecord); + +// ============================================================================= +// Struct-Specific CompressAs Tests +// ============================================================================= + +#[test] +fn test_compress_as_preserves_value() { + let value = 999u64; + + let record = MinimalRecord { + compression_info: Some(CompressionInfo::default()), + value, + }; + + let compressed = record.compress_as(); + assert_eq!(compressed.value, value); +} + +#[test] +fn test_compress_as_when_compression_info_already_none() { + let value = 123u64; + + let record = MinimalRecord { + compression_info: None, + value, + }; + + let compressed = record.compress_as(); + + // Should still work and preserve fields + assert!(compressed.compression_info.is_none()); + assert_eq!(compressed.value, value); +} + +// ============================================================================= +// Struct-Specific DataHasher Tests +// ============================================================================= + +#[test] +fn test_hash_differs_for_different_value() { + let record1 = MinimalRecord { + compression_info: None, + value: 1, + }; + + let record2 = MinimalRecord { + compression_info: None, + value: 2, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "different value should produce different hash" + ); +} + +#[test] +fn test_hash_same_for_same_value() { + let value = 100u64; + + let record1 = MinimalRecord { + compression_info: None, + value, + }; + + let record2 = MinimalRecord { + compression_info: None, + value, + }; + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_eq!(hash1, hash2, "same value should produce same hash"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs new file mode 100644 index 0000000000..55721419a4 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs @@ -0,0 +1,403 @@ +//! Shared generic test helpers for RentFreeAccount-derived traits. +//! +//! These functions test trait implementations generically and can be reused +//! across different account struct types. + +use std::borrow::Cow; + +use light_hasher::{DataHasher, Sha256}; +use light_sdk::{ + account::Size, + compressible::{CompressAs, CompressedInitSpace, CompressionInfo, HasCompressionInfo}, + LightDiscriminator, +}; + +// ============================================================================= +// Test Factory Trait +// ============================================================================= + +/// Trait for creating test instances of compressible account structs. +/// +/// Implement this trait for each account struct to enable generic testing. +pub trait CompressibleTestFactory: Sized { + /// Create an instance with `compression_info = Some(CompressionInfo::default())` + fn with_compression_info() -> Self; + + /// Create an instance with `compression_info = None` + fn without_compression_info() -> Self; +} + +// ============================================================================= +// LightDiscriminator Tests (4 tests) +// ============================================================================= + +/// Verifies LIGHT_DISCRIMINATOR is exactly 8 bytes. +pub fn assert_discriminator_is_8_bytes() { + let discriminator = T::LIGHT_DISCRIMINATOR; + assert_eq!( + discriminator.len(), + 8, + "LIGHT_DISCRIMINATOR should be 8 bytes" + ); +} + +/// Verifies LIGHT_DISCRIMINATOR is not all zeros. +pub fn assert_discriminator_is_non_zero() { + let discriminator = T::LIGHT_DISCRIMINATOR; + let all_zero = discriminator.iter().all(|&b| b == 0); + assert!(!all_zero, "LIGHT_DISCRIMINATOR should not be all zeros"); +} + +/// Verifies discriminator() method returns the same value as LIGHT_DISCRIMINATOR constant. +pub fn assert_discriminator_method_matches_constant() { + let from_method = T::discriminator(); + let from_constant = T::LIGHT_DISCRIMINATOR; + assert_eq!( + from_method, from_constant, + "discriminator() should return LIGHT_DISCRIMINATOR" + ); +} + +/// Verifies LIGHT_DISCRIMINATOR_SLICE matches the LIGHT_DISCRIMINATOR array. +pub fn assert_discriminator_slice_matches_array() { + let array = T::LIGHT_DISCRIMINATOR; + let slice = T::LIGHT_DISCRIMINATOR_SLICE; + + assert_eq!( + slice, &array, + "LIGHT_DISCRIMINATOR_SLICE should match LIGHT_DISCRIMINATOR array" + ); + assert_eq!(slice.len(), 8); +} + +// ============================================================================= +// HasCompressionInfo Tests (6 tests) +// ============================================================================= + +/// Verifies compression_info() returns a valid reference when Some. +pub fn assert_compression_info_returns_reference() +{ + let record = T::with_compression_info(); + let info = record.compression_info(); + // Just verify we can access it - the default values + assert_eq!(info.config_version, 0); + assert_eq!(info.lamports_per_write, 0); +} + +/// Verifies compression_info_mut() allows modification. +pub fn assert_compression_info_mut_allows_modification< + T: HasCompressionInfo + CompressibleTestFactory, +>() { + let mut record = T::with_compression_info(); + + { + let info = record.compression_info_mut(); + info.config_version = 99; + info.lamports_per_write = 1000; + } + + assert_eq!(record.compression_info().config_version, 99); + assert_eq!(record.compression_info().lamports_per_write, 1000); +} + +/// Verifies compression_info_mut_opt() returns a mutable reference to the Option. +pub fn assert_compression_info_mut_opt_works() { + let mut record = T::with_compression_info(); + + // Should be able to access and modify the Option itself + let opt = record.compression_info_mut_opt(); + assert!(opt.is_some()); + + // Set to None via the mutable reference + *opt = None; + + // Verify it changed + let opt2 = record.compression_info_mut_opt(); + assert!(opt2.is_none()); + + // Set back to Some + *opt2 = Some(CompressionInfo::default()); + assert!(record.compression_info_mut_opt().is_some()); +} + +/// Verifies set_compression_info_none() sets the field to None. +pub fn assert_set_compression_info_none_works() { + let mut record = T::with_compression_info(); + + // Verify it starts as Some + assert!(record.compression_info_mut_opt().is_some()); + + record.set_compression_info_none(); + + // Verify it's now None + assert!(record.compression_info_mut_opt().is_none()); +} + +/// Verifies compression_info() panics when compression_info is None. +/// Call this from a test marked with `#[should_panic]`. +pub fn assert_compression_info_panics_when_none() +{ + let record = T::without_compression_info(); + // This should panic since compression_info is None + let _ = record.compression_info(); +} + +/// Verifies compression_info_mut() panics when compression_info is None. +/// Call this from a test marked with `#[should_panic]`. +pub fn assert_compression_info_mut_panics_when_none< + T: HasCompressionInfo + CompressibleTestFactory, +>() { + let mut record = T::without_compression_info(); + // This should panic since compression_info is None + let _ = record.compression_info_mut(); +} + +// ============================================================================= +// CompressAs Tests (2 tests) +// ============================================================================= + +/// Verifies compress_as() sets compression_info to None. +pub fn assert_compress_as_sets_compression_info_to_none< + T: CompressAs + HasCompressionInfo + CompressibleTestFactory + Clone, +>() { + let record = T::with_compression_info(); + let compressed = record.compress_as(); + + // Get the inner value - compress_as should return Owned when it modifies + let mut inner = compressed.into_owned(); + assert!( + inner.compression_info_mut_opt().is_none(), + "compress_as should set compression_info to None" + ); +} + +/// Verifies compress_as() returns Cow::Owned when compression_info is Some. +pub fn assert_compress_as_returns_owned_cow< + T: CompressAs + HasCompressionInfo + CompressibleTestFactory + Clone, +>() { + let record = T::with_compression_info(); + let compressed = record.compress_as(); + + assert!( + matches!(compressed, Cow::Owned(_)), + "compress_as should return Cow::Owned when compression_info is Some" + ); +} + +// ============================================================================= +// Size Tests (2 tests) +// ============================================================================= + +/// Verifies size() returns a positive value. +pub fn assert_size_returns_positive() { + let record = T::with_compression_info(); + let size = record.size(); + assert!(size > 0, "size should be positive"); +} + +/// Verifies size() returns the same value when called multiple times on the same instance. +pub fn assert_size_is_deterministic() { + let record = T::with_compression_info(); + let record_clone = record.clone(); + + let size1 = record.size(); + let size2 = record_clone.size(); + + assert_eq!(size1, size2, "size should be deterministic for same data"); +} + +// ============================================================================= +// CompressedInitSpace Tests (1 test) +// ============================================================================= + +/// Verifies COMPRESSED_INIT_SPACE is at least as large as the discriminator. +pub fn assert_compressed_init_space_includes_discriminator< + T: CompressedInitSpace + LightDiscriminator, +>() { + let compressed_space = T::COMPRESSED_INIT_SPACE; + let discriminator_len = T::LIGHT_DISCRIMINATOR.len(); + + assert!( + compressed_space >= discriminator_len, + "COMPRESSED_INIT_SPACE ({}) should be >= discriminator length ({})", + compressed_space, + discriminator_len + ); +} + +// ============================================================================= +// DataHasher Tests (3 tests) +// ============================================================================= + +/// Verifies hash() produces a 32-byte result. +pub fn assert_hash_produces_32_bytes() { + let record = T::without_compression_info(); + let hash = record.hash::().expect("hash should succeed"); + assert_eq!(hash.len(), 32, "hash should produce 32-byte result"); +} + +/// Verifies hash() is deterministic (same input = same hash). +pub fn assert_hash_is_deterministic() { + let record1 = T::without_compression_info(); + let record2 = record1.clone(); + + let hash1 = record1.hash::().expect("hash should succeed"); + let hash2 = record2.hash::().expect("hash should succeed"); + + assert_eq!(hash1, hash2, "same input should produce same hash"); +} + +/// Verifies compression_info IS included in the hash (LightHasherSha behavior). +pub fn assert_hash_includes_compression_info() { + let record_with_info = T::with_compression_info(); + let record_without_info = T::without_compression_info(); + + let hash1 = record_with_info + .hash::() + .expect("hash should succeed"); + let hash2 = record_without_info + .hash::() + .expect("hash should succeed"); + + assert_ne!( + hash1, hash2, + "compression_info SHOULD affect hash - LightHasherSha hashes entire struct" + ); +} + +// ============================================================================= +// Macro for generating all trait tests +// ============================================================================= + +/// Generates all generic trait tests for a given type. +/// +/// Usage: +/// ```ignore +/// generate_trait_tests!(SinglePubkeyRecord); +/// ``` +#[macro_export] +macro_rules! generate_trait_tests { + ($type:ty) => { + mod discriminator_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_discriminator_is_8_bytes() { + assert_discriminator_is_8_bytes::<$type>(); + } + + #[test] + fn test_discriminator_is_non_zero() { + assert_discriminator_is_non_zero::<$type>(); + } + + #[test] + fn test_discriminator_method_matches_constant() { + assert_discriminator_method_matches_constant::<$type>(); + } + + #[test] + fn test_discriminator_slice_matches_array() { + assert_discriminator_slice_matches_array::<$type>(); + } + } + + mod has_compression_info_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_compression_info_returns_reference() { + assert_compression_info_returns_reference::<$type>(); + } + + #[test] + fn test_compression_info_mut_allows_modification() { + assert_compression_info_mut_allows_modification::<$type>(); + } + + #[test] + fn test_compression_info_mut_opt_works() { + assert_compression_info_mut_opt_works::<$type>(); + } + + #[test] + fn test_set_compression_info_none_works() { + assert_set_compression_info_none_works::<$type>(); + } + + #[test] + #[should_panic] + fn test_compression_info_panics_when_none() { + assert_compression_info_panics_when_none::<$type>(); + } + + #[test] + #[should_panic] + fn test_compression_info_mut_panics_when_none() { + assert_compression_info_mut_panics_when_none::<$type>(); + } + } + + mod compress_as_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_compress_as_sets_compression_info_to_none() { + assert_compress_as_sets_compression_info_to_none::<$type>(); + } + + #[test] + fn test_compress_as_returns_owned_cow() { + assert_compress_as_returns_owned_cow::<$type>(); + } + } + + mod size_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_size_returns_positive() { + assert_size_returns_positive::<$type>(); + } + + #[test] + fn test_size_is_deterministic() { + assert_size_is_deterministic::<$type>(); + } + } + + mod compressed_init_space_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_compressed_init_space_includes_discriminator() { + assert_compressed_init_space_includes_discriminator::<$type>(); + } + } + + mod data_hasher_tests { + use super::*; + use $crate::shared::*; + + #[test] + fn test_hash_produces_32_bytes() { + assert_hash_produces_32_bytes::<$type>(); + } + + #[test] + fn test_hash_is_deterministic() { + assert_hash_is_deterministic::<$type>(); + } + + #[test] + fn test_hash_includes_compression_info() { + assert_hash_includes_compression_info::<$type>(); + } + } + }; +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs similarity index 78% rename from sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs rename to sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index ffaea749de..dce2bac012 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/macro_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -6,14 +6,19 @@ mod shared; use anchor_lang::{InstructionData, ToAccountMetas}; +use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::RentFreeAccountVariant; +use light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ - get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, + create_load_accounts_instructions, get_create_accounts_proof, AccountInterface, + AccountInterfaceExt, CreateAccountsProofInput, InitializeRentFreeConfig, + RentFreeDecompressAccount, }; use light_macros::pubkey; use light_program_test::{ program_test::{setup_mock_program_data, LightProgramTest, TestRpc}, Indexer, ProgramTestConfig, Rpc, }; +use light_sdk::compressible::IntoVariant; use solana_instruction::Instruction; use solana_keypair::Keypair; use solana_pubkey::Pubkey; @@ -94,6 +99,63 @@ impl TestContext { assert!(!acc.data.as_ref().unwrap().data.is_empty()); } + /// Runs the full compression/decompression lifecycle for a single PDA. + async fn assert_lifecycle(&mut self, pda: &Pubkey, seeds: S) + where + S: IntoVariant, + { + // Warp to trigger compression + self.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + self.assert_onchain_closed(pda).await; + + // Get account interface + let account_interface = self + .rpc + .get_account_info_interface(pda, &self.program_id) + .await + .expect("failed to get account interface"); + assert!( + account_interface.is_cold, + "Account should be cold after compression" + ); + + // Build decompression request + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&account_interface), + seeds, + ) + .expect("Seed verification failed")]; + + // Create and execute decompression + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + self.program_id, + self.payer.pubkey(), + self.config_pda, + self.payer.pubkey(), + &self.rpc, + ) + .await + .expect("create_load_accounts_instructions should succeed"); + + self.rpc + .create_and_send_transaction( + &decompress_instructions, + &self.payer.pubkey(), + &[&self.payer], + ) + .await + .expect("Decompression should succeed"); + + // Verify account is back on-chain + self.assert_onchain_exists(pda).await; + } + /// Setup a mint for token-based tests. /// Returns (mint_pubkey, compression_address, ata_pubkeys, mint_seed_keypair) #[allow(dead_code)] @@ -165,6 +227,10 @@ async fn test_d6_account() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D6AccountRecordSeeds; + ctx.assert_lifecycle(&pda, D6AccountRecordSeeds { owner }).await; } /// Tests D6Boxed: Box> type @@ -219,6 +285,10 @@ async fn test_d6_boxed() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D6BoxedRecordSeeds; + ctx.assert_lifecycle(&pda, D6BoxedRecordSeeds { owner }).await; } // ============================================================================= @@ -277,6 +347,10 @@ async fn test_d8_pda_only() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D8PdaOnlyRecordSeeds; + ctx.assert_lifecycle(&pda, D8PdaOnlyRecordSeeds { owner }).await; } /// Tests D8MultiRentfree: Multiple #[rentfree] fields of same type @@ -347,6 +421,85 @@ async fn test_d8_multi_rentfree() { // Verify both accounts exist on-chain ctx.assert_onchain_exists(&pda1).await; ctx.assert_onchain_exists(&pda2).await; + + // Full lifecycle: compression + decompression (multi-PDA, one at a time) + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + D8MultiRecord1Seeds, D8MultiRecord2Seeds, + }; + + // Warp to trigger compression + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + ctx.assert_onchain_closed(&pda1).await; + ctx.assert_onchain_closed(&pda2).await; + + // Decompress first account + let interface1 = ctx + .rpc + .get_account_info_interface(&pda1, &ctx.program_id) + .await + .unwrap(); + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&interface1), + D8MultiRecord1Seeds { owner, id1 }, + ) + .unwrap()]; + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .unwrap(); + ctx.rpc + .create_and_send_transaction( + &decompress_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer], + ) + .await + .unwrap(); + ctx.assert_onchain_exists(&pda1).await; + + // Decompress second account + let interface2 = ctx + .rpc + .get_account_info_interface(&pda2, &ctx.program_id) + .await + .unwrap(); + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&interface2), + D8MultiRecord2Seeds { owner, id2 }, + ) + .unwrap()]; + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .unwrap(); + ctx.rpc + .create_and_send_transaction( + &decompress_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer], + ) + .await + .unwrap(); + ctx.assert_onchain_exists(&pda2).await; } /// Tests D8All: Multiple #[rentfree] fields of different types @@ -409,6 +562,85 @@ async fn test_d8_all() { // Verify both accounts exist on-chain ctx.assert_onchain_exists(&pda_single).await; ctx.assert_onchain_exists(&pda_multi).await; + + // Full lifecycle: compression + decompression (multi-PDA, one at a time) + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + D8AllMultiSeeds, D8AllSingleSeeds, + }; + + // Warp to trigger compression + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + ctx.assert_onchain_closed(&pda_single).await; + ctx.assert_onchain_closed(&pda_multi).await; + + // Decompress first account (single type) + let interface_single = ctx + .rpc + .get_account_info_interface(&pda_single, &ctx.program_id) + .await + .unwrap(); + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&interface_single), + D8AllSingleSeeds { owner }, + ) + .unwrap()]; + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .unwrap(); + ctx.rpc + .create_and_send_transaction( + &decompress_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer], + ) + .await + .unwrap(); + ctx.assert_onchain_exists(&pda_single).await; + + // Decompress second account (multi type) + let interface_multi = ctx + .rpc + .get_account_info_interface(&pda_multi, &ctx.program_id) + .await + .unwrap(); + let program_owned_accounts = vec![RentFreeDecompressAccount::from_seeds( + AccountInterface::from(&interface_multi), + D8AllMultiSeeds { owner }, + ) + .unwrap()]; + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .unwrap(); + ctx.rpc + .create_and_send_transaction( + &decompress_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer], + ) + .await + .unwrap(); + ctx.assert_onchain_exists(&pda_multi).await; } // ============================================================================= @@ -465,6 +697,10 @@ async fn test_d9_literal() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9LiteralRecordSeeds; + ctx.assert_lifecycle(&pda, D9LiteralRecordSeeds {}).await; } /// Tests D9Constant: Constant seed expression @@ -517,6 +753,10 @@ async fn test_d9_constant() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9ConstantRecordSeeds; + ctx.assert_lifecycle(&pda, D9ConstantRecordSeeds {}).await; } /// Tests D9CtxAccount: Context account seed expression @@ -572,6 +812,16 @@ async fn test_d9_ctx_account() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9CtxRecordSeeds; + ctx.assert_lifecycle( + &pda, + D9CtxRecordSeeds { + authority: authority.pubkey(), + }, + ) + .await; } /// Tests D9Param: Param seed expression (Pubkey) @@ -626,6 +876,10 @@ async fn test_d9_param() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9ParamRecordSeeds; + ctx.assert_lifecycle(&pda, D9ParamRecordSeeds { owner }).await; } /// Tests D9ParamBytes: Param bytes seed expression (u64) @@ -683,6 +937,10 @@ async fn test_d9_param_bytes() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9ParamBytesRecordSeeds; + ctx.assert_lifecycle(&pda, D9ParamBytesRecordSeeds { id }).await; } /// Tests D9Mixed: Mixed seed expression types @@ -742,6 +1000,17 @@ async fn test_d9_mixed() { // Verify account exists on-chain ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9MixedRecordSeeds; + ctx.assert_lifecycle( + &pda, + D9MixedRecordSeeds { + authority: authority.pubkey(), + owner, + }, + ) + .await; } // ============================================================================= @@ -799,6 +1068,10 @@ async fn test_d7_payer() { .expect("D7Payer instruction should succeed"); ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7PayerRecordSeeds; + ctx.assert_lifecycle(&pda, D7PayerRecordSeeds { owner }).await; } /// Tests D7Creator: "creator" field name variant @@ -852,6 +1125,10 @@ async fn test_d7_creator() { .expect("D7Creator instruction should succeed"); ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7CreatorRecordSeeds; + ctx.assert_lifecycle(&pda, D7CreatorRecordSeeds { owner }).await; } // ============================================================================= @@ -912,6 +1189,10 @@ async fn test_d9_function_call() { .expect("D9FunctionCall instruction should succeed"); ctx.assert_onchain_exists(&pda).await; + + // Full lifecycle: compression + decompression + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9FuncRecordSeeds; + ctx.assert_lifecycle(&pda, D9FuncRecordSeeds { key_a, key_b }).await; } /// Tests D9All: All 6 seed expression types @@ -1005,6 +1286,77 @@ async fn test_d9_all() { ctx.assert_onchain_exists(&pda_param).await; ctx.assert_onchain_exists(&pda_bytes).await; ctx.assert_onchain_exists(&pda_func).await; + + // Full lifecycle: compression + decompression (6 PDAs, one at a time) + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::{ + D9AllBytesSeeds, D9AllConstSeeds, D9AllCtxSeeds, D9AllFuncSeeds, D9AllLitSeeds, + D9AllParamSeeds, + }; + + // Warp to trigger compression + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + ctx.assert_onchain_closed(&pda_lit).await; + ctx.assert_onchain_closed(&pda_const).await; + ctx.assert_onchain_closed(&pda_ctx).await; + ctx.assert_onchain_closed(&pda_param).await; + ctx.assert_onchain_closed(&pda_bytes).await; + ctx.assert_onchain_closed(&pda_func).await; + + // Helper to decompress a single account + async fn decompress_one>( + ctx: &mut TestContext, + pda: &Pubkey, + seeds: S, + ) { + let interface = ctx + .rpc + .get_account_info_interface(pda, &ctx.program_id) + .await + .unwrap(); + let program_owned_accounts = vec![ + RentFreeDecompressAccount::from_seeds(AccountInterface::from(&interface), seeds) + .unwrap(), + ]; + let decompress_instructions = create_load_accounts_instructions( + &program_owned_accounts, + &[], + &[], + ctx.program_id, + ctx.payer.pubkey(), + ctx.config_pda, + ctx.payer.pubkey(), + &ctx.rpc, + ) + .await + .unwrap(); + ctx.rpc + .create_and_send_transaction( + &decompress_instructions, + &ctx.payer.pubkey(), + &[&ctx.payer], + ) + .await + .unwrap(); + ctx.assert_onchain_exists(pda).await; + } + + // Decompress all 6 accounts one at a time + decompress_one(&mut ctx, &pda_lit, D9AllLitSeeds {}).await; + decompress_one(&mut ctx, &pda_const, D9AllConstSeeds {}).await; + decompress_one( + &mut ctx, + &pda_ctx, + D9AllCtxSeeds { + authority: authority.pubkey(), + }, + ) + .await; + decompress_one(&mut ctx, &pda_param, D9AllParamSeeds { owner }).await; + decompress_one(&mut ctx, &pda_bytes, D9AllBytesSeeds { id }).await; + decompress_one(&mut ctx, &pda_func, D9AllFuncSeeds { key_a, key_b }).await; } // ============================================================================= @@ -1194,6 +1546,8 @@ async fn test_d5_rentfree_token() { // Verify token vault exists ctx.assert_onchain_exists(&vault).await; + + // Note: Token vault decompression not tested - requires TokenAccountVariant } /// Tests D5AllMarkers: #[rentfree] + #[rentfree_token] combined @@ -1267,6 +1621,12 @@ async fn test_d5_all_markers() { // Verify both PDA record and token vault exist ctx.assert_onchain_exists(&d5_all_record).await; ctx.assert_onchain_exists(&d5_all_vault).await; + + // Full lifecycle: compression + decompression (PDA only) + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D5AllRecordSeeds; + ctx.assert_lifecycle(&d5_all_record, D5AllRecordSeeds { owner }) + .await; + // Note: Token vault decompression not tested - requires TokenAccountVariant } // ============================================================================= @@ -1335,6 +1695,8 @@ async fn test_d7_ctoken_config() { // Verify token vault exists ctx.assert_onchain_exists(&d7_ctoken_vault).await; + + // Note: Token vault decompression not tested - requires TokenAccountVariant } /// Tests D7AllNames: payer + ctoken_config/rent_sponsor naming combined @@ -1408,4 +1770,10 @@ async fn test_d7_all_names() { // Verify both PDA record and token vault exist ctx.assert_onchain_exists(&d7_all_record).await; ctx.assert_onchain_exists(&d7_all_vault).await; + + // Full lifecycle: compression + decompression (PDA only) + use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7AllRecordSeeds; + ctx.assert_lifecycle(&d7_all_record, D7AllRecordSeeds { owner }) + .await; + // Note: Token vault decompression not tested - requires TokenAccountVariant } From 3c62e2f57addf994bee86c97ad8b0e76633b2e1e Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 05:02:06 +0000 Subject: [PATCH 08/11] test: compress as features --- .../macros/docs/account/compressible_pack.md | 11 +++ .../{rentfree.md => accounts/architecture.md} | 0 .../sdk/src/compressible/compression_info.rs | 2 +- .../csdk-anchor-full-derived-test/src/lib.rs | 3 +- .../src/state/mod.rs | 2 +- .../account_macros/d4_all_composition_test.rs | 40 +++++++++ .../tests/basic_test.rs | 82 ++++++++++++++++++- 7 files changed, 135 insertions(+), 5 deletions(-) rename sdk-libs/macros/docs/{rentfree.md => accounts/architecture.md} (100%) diff --git a/sdk-libs/macros/docs/account/compressible_pack.md b/sdk-libs/macros/docs/account/compressible_pack.md index e573ef8e2c..d10d657c34 100644 --- a/sdk-libs/macros/docs/account/compressible_pack.md +++ b/sdk-libs/macros/docs/account/compressible_pack.md @@ -320,6 +320,17 @@ let user_record = packed_record.unpack(ctx.remaining_accounts)?; - All methods are marked `#[inline(never)]` for smaller program size - The packed struct derives `AnchorSerialize` and `AnchorDeserialize` +### Limitation: Option Fields + +Only direct `Pubkey` fields are converted to `u8` indices. `Option` fields remain as `Option` in the packed struct because `None` doesn't map cleanly to an index. + +```rust +pub struct Record { + pub owner: Pubkey, // -> u8 in packed struct + pub delegate: Option, // -> Option in packed struct (unchanged) +} +``` + --- ## 10. Related Macros diff --git a/sdk-libs/macros/docs/rentfree.md b/sdk-libs/macros/docs/accounts/architecture.md similarity index 100% rename from sdk-libs/macros/docs/rentfree.md rename to sdk-libs/macros/docs/accounts/architecture.md diff --git a/sdk-libs/sdk/src/compressible/compression_info.rs b/sdk-libs/sdk/src/compressible/compression_info.rs index 273eb0ed59..3ffb42d6c9 100644 --- a/sdk-libs/sdk/src/compressible/compression_info.rs +++ b/sdk-libs/sdk/src/compressible/compression_info.rs @@ -58,7 +58,7 @@ pub trait CompressAs { fn compress_as(&self) -> Cow<'_, Self::Output>; } -#[derive(Debug, Clone, Default, AnchorSerialize, AnchorDeserialize)] +#[derive(Debug, Clone, Default, PartialEq, AnchorSerialize, AnchorDeserialize)] pub struct CompressionInfo { /// Version of the compressible config used to initialize this account. pub config_version: u16, 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 297b14ab83..1ebfe30cc3 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -112,7 +112,6 @@ pub mod csdk_anchor_full_derived_test { ctx: Context<'_, '_, '_, 'info, CreatePdasAndMintAuto<'info>>, params: FullAutoWithMintParams, ) -> Result<()> { - use anchor_lang::solana_program::sysvar::clock::Clock; use light_token_sdk::token::{ CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi as CTokenMintToCpi, }; @@ -127,7 +126,7 @@ pub mod csdk_anchor_full_derived_test { 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.start_time = 2; // Hardcoded non-zero for compress_as test game_session.end_time = None; game_session.score = 0; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs index 8354724d0f..308d7eadc8 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs @@ -26,7 +26,7 @@ pub struct UserRecord { pub category_id: u64, } -#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[derive(Default, Debug, PartialEq, InitSpace, RentFreeAccount)] #[compress_as(start_time = 0, end_time = None, score = 0)] #[account] pub struct GameSession { diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs index c9d22b0dd6..7c1be18865 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs @@ -408,6 +408,46 @@ fn test_pack_does_not_apply_compress_as_overrides() { assert_eq!(packed.close_authority, Some(close_authority)); } +#[test] +fn test_compress_as_then_pack_applies_overrides() { + // The correct way to pack with compress_as overrides: + // call compress_as() first, then pack() the result + let close_authority = Pubkey::new_unique(); + let record = AllCompositionRecord { + owner: Pubkey::new_unique(), + delegate: Pubkey::new_unique(), + authority: Pubkey::new_unique(), + close_authority: Some(close_authority), + compression_info: Some(CompressionInfo::default()), + name: "test".to_string(), + hash: [0u8; 32], + start_time: 100, + cached_time: 999, // Should become 0 after compress_as + end_time: Some(999), // Should become None after compress_as + counter_1: 1, + counter_2: 2, + counter_3: 3, + flag_1: true, + flag_2: false, + score: Some(50), + }; + + // Chain compress_as() then pack() + let compressed = record.compress_as(); + let mut packed_accounts = PackedAccounts::default(); + let packed = compressed.pack(&mut packed_accounts); + + // compress_as overrides ARE applied when chained + assert_eq!(packed.cached_time, 0, "compress_as().pack() applies cached_time = 0 override"); + assert!( + packed.end_time.is_none(), + "compress_as().pack() applies end_time = None override" + ); + // Non-overridden fields preserved + assert_eq!(packed.start_time, 100); + assert_eq!(packed.counter_1, 1); +} + #[test] fn test_pack_preserves_start_time_without_override() { let start_time_value = 555u64; 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 3f26e3e3ad..2d26f9d62b 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 @@ -25,7 +25,7 @@ const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcA async fn test_create_pdas_and_mint_auto() { use csdk_anchor_full_derived_test::{ instruction_accounts::{LP_MINT_SIGNER_SEED, VAULT_SEED}, - FullAutoWithMintParams, + FullAutoWithMintParams, GameSession, }; use light_token_interface::state::Token; use light_token_sdk::token::{ @@ -245,6 +245,42 @@ async fn test_create_pdas_and_mint_auto() { assert_eq!(compressed_cmint.address.unwrap(), mint_compressed_address); assert!(compressed_cmint.data.as_ref().unwrap().data.is_empty()); + // Verify GameSession initial state before compression + // Fields with compress_as overrides should have their original values + let initial_game_session_data = rpc + .get_account(game_session_pda) + .await + .unwrap() + .expect("GameSession should exist after init"); + let initial_game_session: GameSession = + borsh::BorshDeserialize::deserialize(&mut &initial_game_session_data.data[8..]) + .expect("Failed to deserialize initial GameSession"); + + // Verify initial state: start_time should be hardcoded value (2) + assert_eq!( + initial_game_session.start_time, 2, + "Initial start_time should be 2 (hardcoded non-zero), got: {}", + initial_game_session.start_time + ); + assert_eq!( + initial_game_session.session_id, session_id, + "session_id should be preserved" + ); + assert_eq!( + initial_game_session.player, + payer.pubkey(), + "player should be payer" + ); + assert_eq!( + initial_game_session.game_type, "Auto Game With Mint", + "game_type should match" + ); + assert_eq!(initial_game_session.end_time, None, "end_time should be None"); + assert_eq!(initial_game_session.score, 0, "score should be 0"); + + // Store initial start_time for comparison after decompress + let initial_start_time = initial_game_session.start_time; + // PHASE 2: Warp to trigger auto-compression rpc.warp_slot_forward(SLOTS_PER_EPOCH * 30).await.unwrap(); @@ -383,4 +419,48 @@ async fn test_create_pdas_and_mint_auto() { .value .items; assert!(remaining_vault.is_empty()); + + // PHASE 4: Verify compress_as field overrides on GameSession + // After decompress, fields with #[compress_as(...)] should be reset to override values + let game_session_data = rpc + .get_account(game_session_pda) + .await + .unwrap() + .expect("GameSession account should exist"); + let game_session: GameSession = + borsh::BorshDeserialize::deserialize(&mut &game_session_data.data[8..]) // Skip anchor discriminator + .expect("Failed to deserialize GameSession"); + + // Verify start_time was reset by compress_as override + // Initial: Clock timestamp (non-zero), After decompress: 0 + assert_ne!( + initial_start_time, 0, + "Initial start_time should have been non-zero" + ); + assert_eq!( + game_session.start_time, 0, + "start_time should be reset to 0 by compress_as override (was: {})", + initial_start_time + ); + + // Extract runtime-specific value (compression_info set during transaction) + let compression_info = game_session.compression_info.clone(); + + // Build expected struct with compress_as overrides applied: + // #[compress_as(start_time = 0, end_time = None, score = 0)] + let expected_game_session = GameSession { + compression_info, // Runtime-specific, extracted from actual + session_id, // 222 - preserved + player: payer.pubkey(), // Preserved + game_type: "Auto Game With Mint".to_string(), // Preserved + start_time: 0, // compress_as override (was Clock timestamp) + end_time: None, // compress_as override + score: 0, // compress_as override + }; + + // Single assert comparing full struct + assert_eq!( + game_session, expected_game_session, + "GameSession should match expected after decompress with compress_as overrides" + ); } From 32ec53224776f8acd06ee62be6c3e38fdd4125c2 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 06:21:13 +0000 Subject: [PATCH 09/11] stash --- .../rentfree/account/decompress_context.rs | 69 ++---- .../src/rentfree/account/seed_extraction.rs | 33 --- sdk-libs/sdk/src/compressible/close.rs | 4 +- sdk-tests/sdk-token-test/src/lib.rs | 12 + .../src/process_create_two_mints.rs | 221 ++++++++++++++++++ .../tests/test_create_two_mints.rs | 161 +++++++++++++ 6 files changed, 417 insertions(+), 83 deletions(-) create mode 100644 sdk-tests/sdk-token-test/src/process_create_two_mints.rs create mode 100644 sdk-tests/sdk-token-test/tests/test_create_two_mints.rs diff --git a/sdk-libs/macros/src/rentfree/account/decompress_context.rs b/sdk-libs/macros/src/rentfree/account/decompress_context.rs index 177bbbfb55..31fd02aecf 100644 --- a/sdk-libs/macros/src/rentfree/account/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/account/decompress_context.rs @@ -74,55 +74,28 @@ pub fn generate_decompress_context_trait_impl( }).collect(); quote! { variant_seed_params = SeedParams { #(#field_inits,)* ..Default::default() }; } }; - // When no ctx_fields or params_only_fields, use simple pattern - if ctx_fields.is_empty() && params_only_fields.is_empty() { - quote! { - RentFreeAccountVariant::#packed_variant_name { data: packed, .. } => { - #ctx_seeds_construction - #seed_params_update - light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( - &*self.rent_sponsor, - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &program_id, - &ctx_seeds, - std::option::Option::Some(&variant_seed_params), - )?; - } - RentFreeAccountVariant::#variant_name { .. } => { - unreachable!("Unpacked variants should not be present during decompression"); - } + quote! { + RentFreeAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* #(#params_field_patterns,)* .. } => { + #(#resolve_ctx_seeds)* + #ctx_seeds_construction + #seed_params_update + light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( + &*self.rent_sponsor, + cpi_accounts, + address_space, + &solana_accounts[i], + i, + &packed, + &meta, + post_system_accounts, + &mut compressed_pda_infos, + &program_id, + &ctx_seeds, + std::option::Option::Some(&variant_seed_params), + )?; } - } else { - quote! { - RentFreeAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* #(#params_field_patterns,)* .. } => { - #(#resolve_ctx_seeds)* - #ctx_seeds_construction - #seed_params_update - light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( - &*self.rent_sponsor, - cpi_accounts, - address_space, - &solana_accounts[i], - i, - &packed, - &meta, - post_system_accounts, - &mut compressed_pda_infos, - &program_id, - &ctx_seeds, - std::option::Option::Some(&variant_seed_params), - )?; - } - RentFreeAccountVariant::#variant_name { .. } => { - unreachable!("Unpacked variants should not be present during decompression"); - } + RentFreeAccountVariant::#variant_name { .. } => { + unreachable!("Unpacked variants should not be present during decompression"); } } }) diff --git a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs index 9463a7b268..60a4afb3d1 100644 --- a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs @@ -622,39 +622,6 @@ pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> fields } -/// Get params-only seed fields (data.* fields that don't exist on state struct). -/// Returns (field_name, field_type, has_conversion). -/// - has_conversion is true for fields using to_le_bytes(), to_be_bytes(), etc. -/// - field_type is u64 for fields with byte conversion, Pubkey otherwise. -pub fn get_params_only_seed_fields( - seeds: &[ClassifiedSeed], - state_field_names: &std::collections::HashSet, -) -> Vec<(Ident, syn::Type, bool)> { - let mut fields = Vec::new(); - for seed in seeds { - if let ClassifiedSeed::DataField { - field_name, - conversion, - } = seed - { - let field_str = field_name.to_string(); - // Only include fields that are NOT on the state struct - if !state_field_names.contains(&field_str) { - if !fields.iter().any(|(f, _, _): &(Ident, _, _)| f == field_name) { - let has_conversion = conversion.is_some(); - let field_type: syn::Type = if has_conversion { - syn::parse_quote!(u64) - } else { - syn::parse_quote!(solana_pubkey::Pubkey) - }; - fields.push((field_name.clone(), field_type, has_conversion)); - } - } - } - } - fields -} - /// Get params-only seed fields from a TokenSeedSpec. /// This is a convenience wrapper that works with the SeedElement type. pub fn get_params_only_seed_fields_from_spec( diff --git a/sdk-libs/sdk/src/compressible/close.rs b/sdk-libs/sdk/src/compressible/close.rs index f50404d0c8..a240d3aae3 100644 --- a/sdk-libs/sdk/src/compressible/close.rs +++ b/sdk-libs/sdk/src/compressible/close.rs @@ -11,7 +11,7 @@ pub fn close<'info>( if info.key == sol_destination.key { info.assign(&system_program_id); - info.realloc(0, false) + info.resize(0) .map_err(|_| LightSdkError::ConstraintViolation)?; return Ok(()); } @@ -38,7 +38,7 @@ pub fn close<'info>( } info.assign(&system_program_id); - info.realloc(0, false) + info.resize(0) .map_err(|_| LightSdkError::ConstraintViolation)?; Ok(()) diff --git a/sdk-tests/sdk-token-test/src/lib.rs b/sdk-tests/sdk-token-test/src/lib.rs index 560cb7c44e..e324db7ec3 100644 --- a/sdk-tests/sdk-token-test/src/lib.rs +++ b/sdk-tests/sdk-token-test/src/lib.rs @@ -24,6 +24,7 @@ mod process_four_invokes; pub mod process_four_transfer2; mod process_transfer_tokens; mod process_update_deposit; +mod process_create_two_mints; use light_sdk::instruction::account_meta::CompressedAccountMeta; use light_sdk_types::cpi_accounts::{v2::CpiAccounts, CpiAccountsConfig}; @@ -40,6 +41,8 @@ use process_four_invokes::process_four_invokes; pub use process_four_invokes::{CompressParams, FourInvokesParams, TransferParams}; use process_four_transfer2::process_four_transfer2; use process_transfer_tokens::process_transfer_tokens; +use process_create_two_mints::process_create_two_mints; +pub use process_create_two_mints::{CreateMintParamsData, CreateTwoMintsData}; declare_id!("5p1t1GAaKtK1FKCh5Hd2Gu8JCu3eREhJm4Q2qYfTEPYK"); @@ -338,6 +341,15 @@ pub mod sdk_token_test { ) -> Result<()> { process_ctoken_pda(ctx, input) } + + /// Create two compressed mints using CPI context in a single transaction. + /// First CPI writes first mint to CPI context, second CPI executes both with proof. + pub fn create_two_mints<'info>( + ctx: Context<'_, '_, '_, 'info, Generic<'info>>, + data: CreateTwoMintsData, + ) -> Result<()> { + process_create_two_mints(ctx, data) + } } #[derive(Accounts)] diff --git a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs new file mode 100644 index 0000000000..6a6044bb35 --- /dev/null +++ b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs @@ -0,0 +1,221 @@ +use anchor_lang::prelude::*; +use anchor_lang::solana_program::pubkey::Pubkey as SolanaPubkey; +use anchor_lang::solana_program::{instruction::Instruction, program::invoke}; +use light_compressed_account::instruction_data::traits::LightInstructionData; +use light_token_interface::instructions::mint_action::{ + CpiContext, MintActionCompressedInstructionData, MintInstructionData, +}; +use light_token_interface::state::MintMetadata; +use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; +use light_token_sdk::{ + compressed_token::{ + ctoken_instruction::CTokenInstruction, + mint_action::{ + get_mint_action_instruction_account_metas_cpi_write, MintActionCpiAccounts, + MintActionMetaConfigCpiWrite, + }, + }, + CompressedProof, +}; + +/// Instruction data for creating two compressed mints with CPI context. +/// +/// First CPI writes first mint creation to CPI context, second CPI executes both. +/// Both mints remain as compressed accounts (no auto-decompress). +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct CreateTwoMintsData { + /// Params for first mint (written to CPI context) + pub params_1: CreateMintParamsData, + /// Params for second mint (executed with proof) + pub params_2: CreateMintParamsData, + /// Single proof covering both new addresses + pub proof: CompressedProof, +} + +/// Serializable version of CreateMintParams for anchor +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct CreateMintParamsData { + pub decimals: u8, + pub address_merkle_tree_root_index: u16, + pub mint_authority: Pubkey, + pub compression_address: [u8; 32], + pub mint: Pubkey, + pub bump: u8, + pub freeze_authority: Option, +} + +/// Process instruction to create two compressed mints using CPI context. +/// +/// The signer (ctx.accounts.signer) is used as both fee_payer and authority. +/// +/// Account layout (remaining_accounts): +/// - accounts[0]: light_system_program +/// - accounts[1]: mint_signer_1 (SIGNER) +/// - accounts[2]: mint_signer_2 (SIGNER) +/// - accounts[3]: cpi_authority_pda +/// - accounts[4]: registered_program_pda +/// - accounts[5]: account_compression_authority +/// - accounts[6]: account_compression_program +/// - accounts[7]: system_program +/// - accounts[8]: cpi_context_account (writable) +/// - accounts[9]: output_queue (writable) +/// - accounts[10]: address_tree (writable) +/// - accounts[11]: compressed_token_program (for CPI) +pub fn process_create_two_mints<'info>( + ctx: Context<'_, '_, '_, 'info, crate::Generic<'info>>, + data: CreateTwoMintsData, +) -> Result<()> { + let accounts = ctx.remaining_accounts; + let payer = ctx.accounts.signer.to_account_info(); + + // === CPI 1: Write first mint to CPI context (no proof) === + let mint_instruction_data_1 = MintInstructionData { + supply: 0, + decimals: data.params_1.decimals, + metadata: MintMetadata { + version: 3, + mint: data.params_1.mint.to_bytes().into(), + mint_decompressed: false, + mint_signer: accounts[1].key().to_bytes(), + bump: data.params_1.bump, + }, + mint_authority: Some(data.params_1.mint_authority.to_bytes().into()), + freeze_authority: data + .params_1 + .freeze_authority + .map(|auth| auth.to_bytes().into()), + extensions: None, + }; + + let cpi_context_1 = CpiContext { + set_context: false, + first_set_context: true, + in_tree_index: 1, + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: 0, + read_only_address_trees: [0; 4], + address_tree_pubkey: accounts[10].key().to_bytes(), + }; + + let instruction_data_1 = MintActionCompressedInstructionData::new_mint_write_to_cpi_context( + data.params_1.address_merkle_tree_root_index, + mint_instruction_data_1, + cpi_context_1, + ); + + // Build account metas for CPI write (minimal accounts) + let cpi_write_config = MintActionMetaConfigCpiWrite { + fee_payer: SolanaPubkey::new_from_array(payer.key().to_bytes()), + mint_signer: Some(SolanaPubkey::new_from_array(accounts[1].key().to_bytes())), + authority: SolanaPubkey::new_from_array(payer.key().to_bytes()), + cpi_context: SolanaPubkey::new_from_array(accounts[8].key().to_bytes()), + }; + + let account_metas_1 = get_mint_action_instruction_account_metas_cpi_write(cpi_write_config); + + let ix_data_1 = instruction_data_1 + .data() + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotSerialize)?; + + let instruction_1 = Instruction { + program_id: SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas_1, + data: ix_data_1, + }; + + // Invoke first CPI (write to context) + let account_infos_1 = vec![ + accounts[0].clone(), // light_system_program + accounts[1].clone(), // mint_signer_1 + payer.clone(), // authority (same as fee_payer) + payer.clone(), // fee_payer + accounts[3].clone(), // cpi_authority_pda + accounts[8].clone(), // cpi_context_account + accounts[11].clone(), // compressed_token_program + ]; + + invoke(&instruction_1, &account_infos_1)?; + + msg!("CPI 1: First mint written to CPI context"); + + // === CPI 2: Execute with proof (creates both compressed mints) === + let mint_instruction_data_2 = MintInstructionData { + supply: 0, + decimals: data.params_2.decimals, + metadata: MintMetadata { + version: 3, + mint: data.params_2.mint.to_bytes().into(), + mint_decompressed: false, + mint_signer: accounts[2].key().to_bytes(), + bump: data.params_2.bump, + }, + mint_authority: Some(data.params_2.mint_authority.to_bytes().into()), + freeze_authority: data + .params_2 + .freeze_authority + .map(|auth| auth.to_bytes().into()), + extensions: None, + }; + + // Execute from CPI context: set cpi_context with set_context=false, first_set_context=false + // This tells the program to READ from CPI context and execute + // For create_mint in execute mode, in_tree_index must be 1 (hardcoded requirement) + // Packed accounts: [0]=cpi_context, [1]=output_queue, [2]=address_tree + let cpi_context_2 = CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, // MUST be 1 for create_mint in execute mode with CPI context + in_queue_index: 0, // not used for create_mint + out_queue_index: 0, // output_queue index + token_out_queue_index: 0, + assigned_account_index: 1, // Second output account (first is from CPI context) + read_only_address_trees: [0; 4], + address_tree_pubkey: accounts[10].key().to_bytes(), + }; + + let instruction_data_2 = MintActionCompressedInstructionData::new_mint( + data.params_2.address_merkle_tree_root_index, + data.proof, + mint_instruction_data_2, + ) + .with_cpi_context(cpi_context_2); + + // Build account structure for CPI using MintActionCpiAccounts + let empty_vec: Vec> = vec![]; + let mint_action_accounts = MintActionCpiAccounts { + compressed_token_program: &accounts[11], + light_system_program: &accounts[0], + mint_signer: Some(&accounts[2]), + authority: &payer, + fee_payer: &payer, + compressed_token_cpi_authority: &accounts[3], + registered_program_pda: &accounts[4], + account_compression_authority: &accounts[5], + account_compression_program: &accounts[6], + system_program: &accounts[7], + cpi_context: Some(&accounts[8]), + out_output_queue: &accounts[9], + in_merkle_tree: &accounts[10], + in_output_queue: None, + tokens_out_queue: None, + ctoken_accounts: &empty_vec, + }; + + // Build instruction using the trait method + let instruction_2 = instruction_data_2 + .instruction(&mint_action_accounts) + .unwrap(); + + // Invoke second CPI (execute with proof, reads from CPI context) + let account_infos_2: Vec<_> = std::iter::once(payer) + .chain(accounts.iter().cloned()) + .collect(); + + invoke(&instruction_2, &account_infos_2)?; + + msg!("CPI 2: Both compressed mints created with single proof"); + + Ok(()) +} diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs new file mode 100644 index 0000000000..4f54643aa3 --- /dev/null +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -0,0 +1,161 @@ +use anchor_lang::InstructionData; +use light_program_test::{AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, Rpc}; +use light_token_sdk::token::{derive_mint_compressed_address, find_mint_address, SystemAccounts}; +use sdk_token_test::{CreateMintParamsData, CreateTwoMintsData}; +use solana_sdk::{ + instruction::{AccountMeta, Instruction}, + signature::{Keypair, Signer}, +}; + +/// Test creating two compressed mints using CPI context in a single transaction. +/// First CPI writes first mint to CPI context, second CPI executes both with single proof. +/// Both mints remain as compressed accounts (no Solana account created). +#[tokio::test] +async fn test_create_two_compressed_mints_cpi_context() { + // 1. Setup test environment + let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( + false, + Some(vec![("sdk_token_test", sdk_token_test::ID)]), + )) + .await + .unwrap(); + + let payer = rpc.get_payer().insecure_clone(); + + // 2. Generate two mint signers + let mint_signer_1 = Keypair::new(); + let mint_signer_2 = Keypair::new(); + + // 3. Get tree info + let address_tree_info = rpc.get_address_tree_v2(); + let state_tree_info = rpc.get_random_state_tree_info().unwrap(); + + // 4. Derive addresses for both mints + let compression_address_1 = + derive_mint_compressed_address(&mint_signer_1.pubkey(), &address_tree_info.tree); + let compression_address_2 = + derive_mint_compressed_address(&mint_signer_2.pubkey(), &address_tree_info.tree); + let (mint_pda_1, bump_1) = find_mint_address(&mint_signer_1.pubkey()); + let (mint_pda_2, bump_2) = find_mint_address(&mint_signer_2.pubkey()); + + // 5. Get SINGLE validity proof for BOTH addresses + let proof_result = rpc + .get_validity_proof( + vec![], + vec![ + AddressWithTree { + address: compression_address_1, + tree: address_tree_info.tree, + }, + AddressWithTree { + address: compression_address_2, + tree: address_tree_info.tree, + }, + ], + None, + ) + .await + .unwrap() + .value; + + // 6. Build CreateMintParamsData for both mints + let params_1 = CreateMintParamsData { + decimals: 9, + address_merkle_tree_root_index: proof_result.addresses[0].root_index, + mint_authority: payer.pubkey(), + compression_address: compression_address_1, + mint: mint_pda_1, + bump: bump_1, + freeze_authority: None, + }; + + let params_2 = CreateMintParamsData { + decimals: 6, + address_merkle_tree_root_index: proof_result.addresses[1].root_index, + mint_authority: payer.pubkey(), + compression_address: compression_address_2, + mint: mint_pda_2, + bump: bump_2, + freeze_authority: None, + }; + + // 7. Build instruction data + let data = CreateTwoMintsData { + params_1, + params_2, + proof: proof_result.proof.0.unwrap(), + }; + + // 8. Build account metas + let system_accounts = SystemAccounts::default(); + let cpi_context_pubkey = state_tree_info + .cpi_context + .expect("CPI context account required"); + let compressed_token_program_id = + solana_sdk::pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); + + // Account layout (remaining_accounts): + // - accounts[0]: light_system_program + // - accounts[1]: mint_signer_1 (SIGNER) + // - accounts[2]: mint_signer_2 (SIGNER) + // - accounts[3]: cpi_authority_pda + // - accounts[4]: registered_program_pda + // - accounts[5]: account_compression_authority + // - accounts[6]: account_compression_program + // - accounts[7]: system_program + // - accounts[8]: cpi_context_account (writable) + // - accounts[9]: output_queue (writable) + // - accounts[10]: address_tree (writable) + // - accounts[11]: compressed_token_program (for CPI) + let accounts = vec![ + // Anchor accounts (signer is the payer) + AccountMeta::new(payer.pubkey(), true), + // remaining_accounts + AccountMeta::new_readonly(system_accounts.light_system_program, false), // [0] + AccountMeta::new_readonly(mint_signer_1.pubkey(), true), // [1] SIGNER + AccountMeta::new_readonly(mint_signer_2.pubkey(), true), // [2] SIGNER + AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false), // [3] + AccountMeta::new_readonly(system_accounts.registered_program_pda, false), // [4] + AccountMeta::new_readonly(system_accounts.account_compression_authority, false), // [5] + AccountMeta::new_readonly(system_accounts.account_compression_program, false), // [6] + AccountMeta::new_readonly(system_accounts.system_program, false), // [7] + AccountMeta::new(cpi_context_pubkey, false), // [8] + AccountMeta::new(state_tree_info.queue, false), // [9] + AccountMeta::new(address_tree_info.tree, false), // [10] + AccountMeta::new_readonly(compressed_token_program_id, false), // [11] + ]; + + let instruction = Instruction { + program_id: sdk_token_test::ID, + accounts, + data: sdk_token_test::instruction::CreateTwoMints { data }.data(), + }; + + // 9. Send transaction + rpc.create_and_send_transaction( + &[instruction], + &payer.pubkey(), + &[&payer, &mint_signer_1, &mint_signer_2], + ) + .await + .unwrap(); + + // 10. Verify both compressed mints were created by querying the indexer + // Since these are compressed accounts (no Solana account), we verify via indexer + let compressed_accounts = rpc + .indexer() + .unwrap() + .get_compressed_accounts_by_owner(&payer.pubkey(), None, None) + .await + .unwrap(); + + // Check that we have at least 2 compressed accounts (the mints) + assert!( + compressed_accounts.value.items.len() >= 2, + "Should have at least 2 compressed accounts" + ); + + println!("Successfully created two compressed mints in single transaction!"); + println!(" Mint 1 address: {:?}", compression_address_1); + println!(" Mint 2 address: {:?}", compression_address_2); +} From 623e45de2c20aef62805355ca6add003f3175136 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 08:18:55 +0000 Subject: [PATCH 10/11] feat: add create cmints --- Cargo.lock | 3 + programs/system/src/cpi_context/state.rs | 6 +- sdk-libs/token-sdk/Cargo.toml | 1 + sdk-libs/token-sdk/src/token/create_mints.rs | 677 ++++++++++++++++++ sdk-libs/token-sdk/src/token/mod.rs | 3 + sdk-tests/sdk-token-test/Cargo.toml | 4 +- sdk-tests/sdk-token-test/src/lib.rs | 21 +- .../src/process_create_two_mints.rs | 246 ++----- .../tests/test_create_two_mints.rs | 227 +++--- 9 files changed, 864 insertions(+), 324 deletions(-) create mode 100644 sdk-libs/token-sdk/src/token/create_mints.rs diff --git a/Cargo.lock b/Cargo.lock index 46a10fedc5..b8ae90aa66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4247,6 +4247,7 @@ dependencies = [ "arrayvec", "borsh 0.10.4", "light-account-checks", + "light-batched-merkle-tree", "light-compressed-account", "light-compressed-token", "light-compressible", @@ -6117,6 +6118,8 @@ dependencies = [ "light-token-types", "light-zero-copy", "serial_test", + "solana-account-info", + "solana-pubkey 2.4.0", "solana-sdk", "tokio", ] diff --git a/programs/system/src/cpi_context/state.rs b/programs/system/src/cpi_context/state.rs index d9acfa94f6..b4814711d9 100644 --- a/programs/system/src/cpi_context/state.rs +++ b/programs/system/src/cpi_context/state.rs @@ -138,10 +138,8 @@ impl<'a> ZCpiContextAccount2<'a> { // Store new addresses for address in instruction_data.new_addresses() { let assigned_index = address.assigned_compressed_account_index(); - // Use checked arithmetic to prevent overflow - let assigned_account_index = (assigned_index.unwrap_or(0) as u8) - .checked_add(pre_address_len as u8) - .ok_or(ZeroCopyError::Size)?; + // Use the assigned index directly - caller provides absolute index + let assigned_account_index = assigned_index.unwrap_or(0) as u8; let new_address = CpiContextNewAddressParamsAssignedPacked { owner: owner_bytes, // Use cached owner bytes seed: address.seed(), diff --git a/sdk-libs/token-sdk/Cargo.toml b/sdk-libs/token-sdk/Cargo.toml index 5f54839537..f959a411fc 100644 --- a/sdk-libs/token-sdk/Cargo.toml +++ b/sdk-libs/token-sdk/Cargo.toml @@ -31,6 +31,7 @@ light-compressed-account = { workspace = true, features = ["std", "solana"] } light-compressible = { workspace = true } light-token-interface = { workspace = true } light-sdk = { workspace = true, features = ["v2"] } +light-batched-merkle-tree = { workspace = true } light-macros = { workspace = true } thiserror = { workspace = true } # Serialization diff --git a/sdk-libs/token-sdk/src/token/create_mints.rs b/sdk-libs/token-sdk/src/token/create_mints.rs new file mode 100644 index 0000000000..40c927986d --- /dev/null +++ b/sdk-libs/token-sdk/src/token/create_mints.rs @@ -0,0 +1,677 @@ +//! Create multiple compressed mints and decompress all to Solana Mint accounts. +//! +//! This module provides functionality for batch creating compressed mints with +//! optimal CPI batching. When creating multiple mints, it uses the CPI context +//! pattern to minimize transaction overhead. +//! +//! # Flow +//! +//! - N=1: Single CPI (create + decompress) +//! - N>1: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress) + +use light_batched_merkle_tree::queue::BatchedQueueAccount; +use light_compressed_account::instruction_data::traits::LightInstructionData; +use light_token_interface::{ + instructions::mint_action::{ + Action, CpiContext, DecompressMintAction, MintActionCompressedInstructionData, + MintInstructionData, + }, + state::MintMetadata, + LIGHT_TOKEN_PROGRAM_ID, +}; +use solana_account_info::AccountInfo; +use solana_cpi::invoke; +use solana_instruction::Instruction; +use solana_program_error::ProgramError; +use solana_pubkey::Pubkey; + +use crate::compressed_token::mint_action::{ + get_mint_action_instruction_account_metas_cpi_write, MintActionMetaConfig, + MintActionMetaConfigCpiWrite, +}; + +use super::SystemAccountInfos; + +/// Default rent payment epochs (~24 hours) +pub const DEFAULT_RENT_PAYMENT: u8 = 16; +/// Default lamports for write operations (~3 hours per write) +pub const DEFAULT_WRITE_TOP_UP: u32 = 766; + +/// Parameters for a single mint within a batch creation. +/// +/// Does not include proof since proof is shared across all mints in the batch. +#[derive(Debug, Clone)] +pub struct SingleMintParams<'a> { + pub decimals: u8, + pub address_merkle_tree_root_index: u16, + pub mint_authority: Pubkey, + pub compression_address: [u8; 32], + pub mint: Pubkey, + pub bump: u8, + pub freeze_authority: Option, + /// Mint seed pubkey (signer) for this mint + pub mint_seed_pubkey: Pubkey, + /// Optional authority seeds for PDA signing + pub authority_seeds: Option<&'a [&'a [u8]]>, + /// Optional mint signer seeds for PDA signing + pub mint_signer_seeds: Option<&'a [&'a [u8]]>, +} + +/// Parameters for creating one or more compressed mints with decompression. +/// +/// Creates N compressed mints and decompresses all to Solana Mint accounts. +/// Uses CPI context pattern when N > 1 for efficiency. +#[derive(Debug, Clone)] +pub struct CreateMintsParams<'a> { + /// Parameters for each mint to create + pub mints: &'a [SingleMintParams<'a>], + /// Single proof covering all new addresses + pub proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + /// Rent payment in epochs for the Mint account (must be 0 or >= 2). + /// Default: 16 (~24 hours) + pub rent_payment: u8, + /// Lamports allocated for future write operations. + /// Default: 766 (~3 hours per write) + pub write_top_up: u32, +} + +impl<'a> CreateMintsParams<'a> { + pub fn new( + mints: &'a [SingleMintParams<'a>], + proof: light_compressed_account::instruction_data::compressed_proof::CompressedProof, + ) -> Self { + Self { + mints, + proof, + rent_payment: DEFAULT_RENT_PAYMENT, + write_top_up: DEFAULT_WRITE_TOP_UP, + } + } + + pub fn with_rent_payment(mut self, rent_payment: u8) -> Self { + self.rent_payment = rent_payment; + self + } + + pub fn with_write_top_up(mut self, write_top_up: u32) -> Self { + self.write_top_up = write_top_up; + self + } +} + +/// CPI struct for on-chain programs to create multiple mints. +/// +/// Uses named account fields for clarity and safety - no manual index calculations. +/// +/// # Example +/// +/// ```rust,ignore +/// use light_token_sdk::token::{CreateMintsCpi, CreateMintsParams, SingleMintParams, SystemAccountInfos}; +/// +/// let params = CreateMintsParams::new(vec![mint_params_1, mint_params_2], proof); +/// +/// CreateMintsCpi { +/// mint_seeds: vec![mint_signer1.clone(), mint_signer2.clone()], +/// payer: payer.clone(), +/// address_tree: address_tree.clone(), +/// output_queue: output_queue.clone(), +/// state_merkle_tree: state_tree.clone(), +/// compressible_config: config.clone(), +/// mints: vec![mint_pda1.clone(), mint_pda2.clone()], +/// rent_sponsor: rent_sponsor.clone(), +/// system_accounts: SystemAccountInfos { ... }, +/// cpi_context_account: cpi_context.clone(), +/// params, +/// }.invoke()?; +/// ``` +pub struct CreateMintsCpi<'a, 'info> { + /// Mint seed accounts (signers) - one per mint + pub mint_seed_accounts: &'info [AccountInfo<'info>], + /// Fee payer (also used as authority) + pub payer: AccountInfo<'info>, + /// Address tree for new mint addresses + pub address_tree: AccountInfo<'info>, + /// Output queue for compressed accounts + pub output_queue: AccountInfo<'info>, + /// State merkle tree (for decompress of earlier mints) + pub state_merkle_tree: AccountInfo<'info>, + /// CompressibleConfig account + pub compressible_config: AccountInfo<'info>, + /// Mint PDA accounts (writable) - one per mint + pub mints: &'info [AccountInfo<'info>], + /// Rent sponsor PDA + pub rent_sponsor: AccountInfo<'info>, + /// Standard Light Protocol system accounts + pub system_accounts: SystemAccountInfos<'info>, + /// CPI context account + pub cpi_context_account: AccountInfo<'info>, + /// Parameters + pub params: CreateMintsParams<'a>, +} + +impl<'a, 'info> CreateMintsCpi<'a, 'info> { + /// Validate that the struct is properly constructed. + pub fn validate(&self) -> Result<(), ProgramError> { + let n = self.params.mints.len(); + if n == 0 { + return Err(ProgramError::InvalidArgument); + } + if self.mint_seed_accounts.len() != n { + return Err(ProgramError::InvalidArgument); + } + if self.mints.len() != n { + return Err(ProgramError::InvalidArgument); + } + Ok(()) + } + + /// Execute all CPIs to create and decompress all mints. + pub fn invoke(self) -> Result<(), ProgramError> { + self.invoke_impl(None) + } + + /// Execute all CPIs to create and decompress all mints with PDA signing. + pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { + self.invoke_impl(Some(signer_seeds)) + } + + fn invoke_impl(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { + self.validate()?; + let n = self.params.mints.len(); + + if n == 1 { + self.invoke_single_mint(signer_seeds) + } else { + self.invoke_multiple_mints(signer_seeds) + } + } + + /// Handle the single mint case: create + decompress in one CPI. + #[inline(never)] + fn invoke_single_mint(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { + let mint_params = &self.params.mints[0]; + + let mint_data = build_mint_instruction_data(mint_params, self.mint_seed_accounts[0].key); + + let decompress_action = DecompressMintAction { + rent_payment: self.params.rent_payment, + write_top_up: self.params.write_top_up, + }; + + let instruction_data = MintActionCompressedInstructionData::new_mint( + mint_params.address_merkle_tree_root_index, + self.params.proof.clone(), + mint_data, + ) + .with_decompress_mint(decompress_action); + + let mut meta_config = MintActionMetaConfig::new_create_mint( + *self.payer.key, + *self.payer.key, + *self.mint_seed_accounts[0].key, + *self.address_tree.key, + *self.output_queue.key, + ) + .with_compressible_mint( + *self.mints[0].key, + *self.compressible_config.key, + *self.rent_sponsor.key, + ); + meta_config.input_queue = Some(*self.output_queue.key); + + self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + } + + /// Handle the multiple mints case: N-1 writes + 1 execute + N-1 decompress. + #[inline(never)] + fn invoke_multiple_mints(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { + let n = self.params.mints.len(); + + // Get base leaf index before any CPIs modify the queue + let base_leaf_index = get_base_leaf_index(&self.output_queue)?; + + let decompress_action = DecompressMintAction { + rent_payment: self.params.rent_payment, + write_top_up: self.params.write_top_up, + }; + + // Write mints 0..N-2 to CPI context + for i in 0..(n - 1) { + self.invoke_cpi_write(i, signer_seeds)?; + } + + // Execute: create last mint + decompress it + self.invoke_execute(n - 1, &decompress_action, signer_seeds)?; + + // Decompress remaining mints (0..N-2) + for i in 0..(n - 1) { + self.invoke_decompress(i, base_leaf_index, &decompress_action, signer_seeds)?; + } + + Ok(()) + } + + /// Invoke a CPI write instruction for a single mint. + #[inline(never)] + fn invoke_cpi_write( + &self, + index: usize, + signer_seeds: Option<&[&[&[u8]]]>, + ) -> Result<(), ProgramError> { + let mint_params = &self.params.mints[index]; + + let cpi_context = CpiContext { + set_context: index > 0, + first_set_context: index == 0, + in_tree_index: 1, + in_queue_index: 0, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: index as u8, + read_only_address_trees: [0; 4], + address_tree_pubkey: self.address_tree.key.to_bytes(), + }; + + let mint_data = + build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key); + + let instruction_data = MintActionCompressedInstructionData::new_mint_write_to_cpi_context( + mint_params.address_merkle_tree_root_index, + mint_data, + cpi_context, + ); + + let cpi_write_config = MintActionMetaConfigCpiWrite { + fee_payer: *self.payer.key, + mint_signer: Some(*self.mint_seed_accounts[index].key), + authority: *self.payer.key, + cpi_context: *self.cpi_context_account.key, + }; + + let account_metas = get_mint_action_instruction_account_metas_cpi_write(cpi_write_config); + let ix_data = instruction_data + .data() + .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; + + // Account order matches get_mint_action_instruction_account_metas_cpi_write: + // [0]: light_system_program + // [1]: mint_signer (optional, when present) + // [2]: authority + // [3]: fee_payer + // [4]: cpi_authority_pda + // [5]: cpi_context + let account_infos = [ + self.system_accounts.light_system_program.clone(), + self.mint_seed_accounts[index].clone(), + self.payer.clone(), + self.payer.clone(), + self.system_accounts.cpi_authority_pda.clone(), + self.cpi_context_account.clone(), + ]; + let instruction = Instruction { + program_id: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + if let Some(seeds) = signer_seeds { + solana_cpi::invoke_signed(&instruction, &account_infos, seeds) + } else { + invoke(&instruction, &account_infos) + } + } + + /// Invoke the execute instruction (create last mint + decompress). + #[inline(never)] + fn invoke_execute( + &self, + last_idx: usize, + decompress_action: &DecompressMintAction, + signer_seeds: Option<&[&[&[u8]]]>, + ) -> Result<(), ProgramError> { + let mint_params = &self.params.mints[last_idx]; + + let execute_cpi_context = CpiContext { + set_context: false, + first_set_context: false, + in_tree_index: 1, + in_queue_index: 1, + out_queue_index: 0, + token_out_queue_index: 0, + assigned_account_index: last_idx as u8, + read_only_address_trees: [0; 4], + address_tree_pubkey: self.address_tree.key.to_bytes(), + }; + + let mint_data = + build_mint_instruction_data(mint_params, self.mint_seed_accounts[last_idx].key); + + let instruction_data = MintActionCompressedInstructionData::new_mint( + mint_params.address_merkle_tree_root_index, + self.params.proof.clone(), + mint_data, + ) + .with_cpi_context(execute_cpi_context) + .with_decompress_mint(decompress_action.clone()); + + let mut meta_config = MintActionMetaConfig::new_create_mint( + *self.payer.key, + *self.payer.key, + *self.mint_seed_accounts[last_idx].key, + *self.address_tree.key, + *self.output_queue.key, + ) + .with_compressible_mint( + *self.mints[last_idx].key, + *self.compressible_config.key, + *self.rent_sponsor.key, + ); + meta_config.cpi_context = Some(*self.cpi_context_account.key); + meta_config.input_queue = Some(*self.output_queue.key); + + self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + } + + /// Invoke decompress for a single mint. + #[inline(never)] + fn invoke_decompress( + &self, + index: usize, + base_leaf_index: u32, + decompress_action: &DecompressMintAction, + signer_seeds: Option<&[&[&[u8]]]>, + ) -> Result<(), ProgramError> { + let mint_params = &self.params.mints[index]; + + let mint_data = + build_mint_instruction_data(mint_params, self.mint_seed_accounts[index].key); + + let instruction_data = MintActionCompressedInstructionData { + leaf_index: base_leaf_index + index as u32, + prove_by_index: true, + root_index: 0, + max_top_up: 0, + create_mint: None, + actions: vec![Action::DecompressMint(decompress_action.clone())], + proof: None, + cpi_context: None, + mint: Some(mint_data), + }; + + let meta_config = MintActionMetaConfig::new( + *self.payer.key, + *self.payer.key, + *self.state_merkle_tree.key, + *self.output_queue.key, + *self.output_queue.key, + ) + .with_compressible_mint( + *self.mints[index].key, + *self.compressible_config.key, + *self.rent_sponsor.key, + ); + + self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + } + + /// Invoke a mint action instruction. + #[inline(never)] + fn invoke_mint_action( + &self, + instruction_data: MintActionCompressedInstructionData, + meta_config: MintActionMetaConfig, + signer_seeds: Option<&[&[&[u8]]]>, + ) -> Result<(), ProgramError> { + let account_metas = meta_config.to_account_metas(); + let ix_data = instruction_data + .data() + .map_err(|e| ProgramError::BorshIoError(e.to_string()))?; + + // Collect all account infos needed for the CPI + let mut account_infos = vec![self.payer.clone()]; + + // System accounts + account_infos.push(self.system_accounts.light_system_program.clone()); + + // Add all mint seeds + for mint_seed in self.mint_seed_accounts { + account_infos.push(mint_seed.clone()); + } + + // More system accounts + account_infos.push(self.system_accounts.cpi_authority_pda.clone()); + account_infos.push(self.system_accounts.registered_program_pda.clone()); + account_infos.push(self.system_accounts.account_compression_authority.clone()); + account_infos.push(self.system_accounts.account_compression_program.clone()); + account_infos.push(self.system_accounts.system_program.clone()); + + // CPI context, queues, trees + account_infos.push(self.cpi_context_account.clone()); + account_infos.push(self.output_queue.clone()); + account_infos.push(self.address_tree.clone()); + account_infos.push(self.compressible_config.clone()); + account_infos.push(self.rent_sponsor.clone()); + account_infos.push(self.state_merkle_tree.clone()); + + // Add all mint PDAs + for mint in self.mints { + account_infos.push(mint.clone()); + } + + let instruction = Instruction { + program_id: Pubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + if let Some(seeds) = signer_seeds { + solana_cpi::invoke_signed(&instruction, &account_infos, seeds) + } else { + invoke(&instruction, &account_infos) + } + } +} + +/// Build MintInstructionData for a single mint. +#[inline(never)] +fn build_mint_instruction_data( + mint_params: &SingleMintParams<'_>, + mint_signer: &Pubkey, +) -> MintInstructionData { + MintInstructionData { + supply: 0, + decimals: mint_params.decimals, + metadata: MintMetadata { + version: 3, + mint: mint_params.mint.to_bytes().into(), + mint_decompressed: false, + mint_signer: mint_signer.to_bytes(), + bump: mint_params.bump, + }, + mint_authority: Some(mint_params.mint_authority.to_bytes().into()), + freeze_authority: mint_params.freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + } +} + +/// Get base leaf index from output queue account. +#[inline(never)] +fn get_base_leaf_index(output_queue: &AccountInfo) -> Result { + let queue = BatchedQueueAccount::output_from_account_info(output_queue) + .map_err(|_| ProgramError::InvalidAccountData)?; + Ok(queue.batch_metadata.next_index as u32) +} + +/// Create multiple mints and decompress all to Solana accounts. +/// +/// Convenience function that builds a [`CreateMintsCpi`] from a slice of accounts. +/// +/// # Arguments +/// +/// * `payer` - The fee payer account +/// * `accounts` - The remaining accounts in the expected layout +/// * `params` - Parameters for creating the mints +/// +/// # Account Layout +/// +/// - `[0]`: light_system_program +/// - `[1..N+1]`: mint_signers (SIGNER) +/// - `[N+1..N+6]`: system PDAs (cpi_authority, registered_program, compression_authority, compression_program, system_program) +/// - `[N+6]`: cpi_context_account (writable) +/// - `[N+7]`: output_queue (writable) +/// - `[N+8]`: address_tree (writable) +/// - `[N+9]`: compressible_config +/// - `[N+10]`: rent_sponsor (writable) +/// - `[N+11]`: state_merkle_tree (writable) +/// - `[N+12..2N+12]`: mint_pdas (writable) +/// - `[2N+12]`: compressed_token_program (for CPI) +pub fn create_mints<'a, 'info>( + payer: &AccountInfo<'info>, + accounts: &'info [AccountInfo<'info>], + params: CreateMintsParams<'a>, +) -> Result<(), ProgramError> { + if params.mints.is_empty() { + return Err(ProgramError::InvalidArgument); + } + + let n = params.mints.len(); + let mint_signers_start = 1; + let cpi_authority_idx = n + 1; + let registered_program_idx = n + 2; + let compression_authority_idx = n + 3; + let compression_program_idx = n + 4; + let system_program_idx = n + 5; + let cpi_context_idx = n + 6; + let output_queue_idx = n + 7; + let address_tree_idx = n + 8; + let compressible_config_idx = n + 9; + let rent_sponsor_idx = n + 10; + let state_merkle_tree_idx = n + 11; + let mint_pdas_start = n + 12; + + // Build named struct from accounts slice + CreateMintsCpi { + mint_seed_accounts: &accounts[mint_signers_start..mint_signers_start + n], + payer: payer.clone(), + address_tree: accounts[address_tree_idx].clone(), + output_queue: accounts[output_queue_idx].clone(), + state_merkle_tree: accounts[state_merkle_tree_idx].clone(), + compressible_config: accounts[compressible_config_idx].clone(), + mints: &accounts[mint_pdas_start..mint_pdas_start + n], + rent_sponsor: accounts[rent_sponsor_idx].clone(), + system_accounts: SystemAccountInfos { + light_system_program: accounts[0].clone(), + cpi_authority_pda: accounts[cpi_authority_idx].clone(), + registered_program_pda: accounts[registered_program_idx].clone(), + account_compression_authority: accounts[compression_authority_idx].clone(), + account_compression_program: accounts[compression_program_idx].clone(), + system_program: accounts[system_program_idx].clone(), + }, + cpi_context_account: accounts[cpi_context_idx].clone(), + params, + } + .invoke() +} + +// // ============================================================================ +// // Client-side instruction builder +// // ============================================================================ + +// /// Client-side instruction builder for creating multiple mints. +// /// +// /// This struct is used to build instructions for client-side transaction construction. +// /// For CPI usage within Solana programs, use [`CreateMintsCpi`] instead. +// /// +// /// # Example +// /// +// /// ```rust,ignore +// /// use light_token_sdk::token::{CreateMints, CreateMintsParams, SingleMintParams}; +// /// +// /// let params = CreateMintsParams::new(vec![mint1_params, mint2_params], proof); +// /// +// /// let instructions = CreateMints::new( +// /// params, +// /// mint_seed_pubkeys, +// /// payer, +// /// address_tree_pubkey, +// /// output_queue, +// /// state_merkle_tree, +// /// cpi_context_pubkey, +// /// ).instructions()?; +// /// ``` +// #[derive(Debug, Clone)] +// pub struct CreateMints<'a> { +// pub payer: Pubkey, +// pub address_tree_pubkey: Pubkey, +// pub output_queue: Pubkey, +// pub state_merkle_tree: Pubkey, +// pub cpi_context_pubkey: Pubkey, +// pub params: CreateMintsParams<'a>, +// } + +// impl<'a> CreateMints<'a> { +// #[allow(clippy::too_many_arguments)] +// pub fn new( +// params: CreateMintsParams<'a>, +// payer: Pubkey, +// address_tree_pubkey: Pubkey, +// output_queue: Pubkey, +// state_merkle_tree: Pubkey, +// cpi_context_pubkey: Pubkey, +// ) -> Self { +// Self { +// payer, +// address_tree_pubkey, +// output_queue, +// state_merkle_tree, +// cpi_context_pubkey, +// params, +// } +// } + +// /// Build account metas for the instruction. +// pub fn build_account_metas(&self) -> Vec { +// let system_accounts = SystemAccounts::default(); + +// let mut accounts = vec![AccountMeta::new_readonly( +// system_accounts.light_system_program, +// false, +// )]; + +// // Add mint signers (from each SingleMintParams) +// for mint_params in self.params.mints { +// accounts.push(AccountMeta::new_readonly( +// mint_params.mint_seed_pubkey, +// true, +// )); +// } + +// // Add system PDAs +// accounts.extend(vec![ +// AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false), +// AccountMeta::new_readonly(system_accounts.registered_program_pda, false), +// AccountMeta::new_readonly(system_accounts.account_compression_authority, false), +// AccountMeta::new_readonly(system_accounts.account_compression_program, false), +// AccountMeta::new_readonly(system_accounts.system_program, false), +// ]); + +// // CPI context, output queue, address tree +// accounts.push(AccountMeta::new(self.cpi_context_pubkey, false)); +// accounts.push(AccountMeta::new(self.output_queue, false)); +// accounts.push(AccountMeta::new(self.address_tree_pubkey, false)); + +// // Config, rent sponsor +// accounts.push(AccountMeta::new_readonly(config_pda(), false)); +// accounts.push(AccountMeta::new(rent_sponsor_pda(), false)); + +// // State merkle tree +// accounts.push(AccountMeta::new(self.state_merkle_tree, false)); + +// // Add mint PDAs +// for mint_params in self.params.mints { +// accounts.push(AccountMeta::new(mint_params.mint, false)); +// } + +// accounts +// } +// } diff --git a/sdk-libs/token-sdk/src/token/mod.rs b/sdk-libs/token-sdk/src/token/mod.rs index 999d68cc85..b646594031 100644 --- a/sdk-libs/token-sdk/src/token/mod.rs +++ b/sdk-libs/token-sdk/src/token/mod.rs @@ -25,6 +25,7 @@ //! ## Mint //! //! - [`CreateMint`] - Create cMint +//! - [`CreateMints`] - Create multiple cMints in a batch //! - [`MintTo`] - Mint tokens to ctoken accounts //! //! ## Revoke and Thaw @@ -100,6 +101,7 @@ mod compressible; mod create; mod create_ata; mod create_mint; +mod create_mints; mod decompress; mod decompress_mint; mod freeze; @@ -125,6 +127,7 @@ pub use create_ata::{ CreateTokenAtaCpi as CreateAssociatedAccountCpi, CreateTokenAtaCpi, }; pub use create_mint::*; +pub use create_mints::*; pub use decompress::Decompress; pub use decompress_mint::*; pub use freeze::*; diff --git a/sdk-tests/sdk-token-test/Cargo.toml b/sdk-tests/sdk-token-test/Cargo.toml index 9871497514..41b64523e7 100644 --- a/sdk-tests/sdk-token-test/Cargo.toml +++ b/sdk-tests/sdk-token-test/Cargo.toml @@ -34,10 +34,12 @@ profile-heap = [ light-token-sdk = { workspace = true, features = ["anchor", "cpi-context", "v1"] } light-token-types = { workspace = true } anchor-lang = { workspace = true } +solana-pubkey = { workspace = true } +solana-account-info = { workspace = true } light-hasher = { workspace = true } light-sdk = { workspace = true, features = ["v2", "cpi-context"] } light-sdk-types = { workspace = true, features = ["cpi-context"] } -light-compressed-account = { workspace = true, features = ["std"] } +light-compressed-account = { workspace = true, features = ["std", "anchor"] } arrayvec = { workspace = true } light-batched-merkle-tree = { workspace = true } light-token-interface = { workspace = true, features = ["anchor"] } diff --git a/sdk-tests/sdk-token-test/src/lib.rs b/sdk-tests/sdk-token-test/src/lib.rs index e324db7ec3..245112a40d 100644 --- a/sdk-tests/sdk-token-test/src/lib.rs +++ b/sdk-tests/sdk-token-test/src/lib.rs @@ -18,13 +18,13 @@ mod process_compress_tokens; mod process_create_compressed_account; mod process_create_ctoken_with_compress_to_pubkey; mod process_create_escrow_pda; +mod process_create_two_mints; mod process_decompress_full_cpi_context; mod process_decompress_tokens; mod process_four_invokes; pub mod process_four_transfer2; mod process_transfer_tokens; mod process_update_deposit; -mod process_create_two_mints; use light_sdk::instruction::account_meta::CompressedAccountMeta; use light_sdk_types::cpi_accounts::{v2::CpiAccounts, CpiAccountsConfig}; @@ -35,14 +35,14 @@ use process_compress_tokens::process_compress_tokens; use process_create_compressed_account::process_create_compressed_account; use process_create_ctoken_with_compress_to_pubkey::process_create_ctoken_with_compress_to_pubkey; use process_create_escrow_pda::process_create_escrow_pda; +use process_create_two_mints::process_create_mints; +pub use process_create_two_mints::{CreateMintsParams, MintParams}; use process_decompress_full_cpi_context::process_decompress_full_cpi_context; use process_decompress_tokens::process_decompress_tokens; use process_four_invokes::process_four_invokes; pub use process_four_invokes::{CompressParams, FourInvokesParams, TransferParams}; use process_four_transfer2::process_four_transfer2; use process_transfer_tokens::process_transfer_tokens; -use process_create_two_mints::process_create_two_mints; -pub use process_create_two_mints::{CreateMintParamsData, CreateTwoMintsData}; declare_id!("5p1t1GAaKtK1FKCh5Hd2Gu8JCu3eREhJm4Q2qYfTEPYK"); @@ -342,13 +342,16 @@ pub mod sdk_token_test { process_ctoken_pda(ctx, input) } - /// Create two compressed mints using CPI context in a single transaction. - /// First CPI writes first mint to CPI context, second CPI executes both with proof. - pub fn create_two_mints<'info>( - ctx: Context<'_, '_, '_, 'info, Generic<'info>>, - data: CreateTwoMintsData, + /// Create one or more compressed mints and decompress all to Solana accounts. + /// + /// Flow: + /// - N=1: Single CPI (create + decompress) + /// - N>1: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress) + pub fn create_mints<'a, 'info>( + ctx: Context<'a, '_, 'info, 'info, Generic<'info>>, + params: CreateMintsParams, ) -> Result<()> { - process_create_two_mints(ctx, data) + process_create_mints(ctx, params) } } diff --git a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs index 6a6044bb35..6c950232ee 100644 --- a/sdk-tests/sdk-token-test/src/process_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/src/process_create_two_mints.rs @@ -1,40 +1,13 @@ use anchor_lang::prelude::*; -use anchor_lang::solana_program::pubkey::Pubkey as SolanaPubkey; -use anchor_lang::solana_program::{instruction::Instruction, program::invoke}; -use light_compressed_account::instruction_data::traits::LightInstructionData; -use light_token_interface::instructions::mint_action::{ - CpiContext, MintActionCompressedInstructionData, MintInstructionData, -}; -use light_token_interface::state::MintMetadata; -use light_token_interface::LIGHT_TOKEN_PROGRAM_ID; use light_token_sdk::{ - compressed_token::{ - ctoken_instruction::CTokenInstruction, - mint_action::{ - get_mint_action_instruction_account_metas_cpi_write, MintActionCpiAccounts, - MintActionMetaConfigCpiWrite, - }, - }, + token::{create_mints, CreateMintsParams as SdkCreateMintsParams, SingleMintParams}, CompressedProof, }; -/// Instruction data for creating two compressed mints with CPI context. -/// -/// First CPI writes first mint creation to CPI context, second CPI executes both. -/// Both mints remain as compressed accounts (no auto-decompress). -#[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct CreateTwoMintsData { - /// Params for first mint (written to CPI context) - pub params_1: CreateMintParamsData, - /// Params for second mint (executed with proof) - pub params_2: CreateMintParamsData, - /// Single proof covering both new addresses - pub proof: CompressedProof, -} - -/// Serializable version of CreateMintParams for anchor +/// Parameters for a single mint within a batch creation. +/// Does not include proof since proof is shared across all mints. #[derive(Clone, AnchorSerialize, AnchorDeserialize)] -pub struct CreateMintParamsData { +pub struct MintParams { pub decimals: u8, pub address_merkle_tree_root_index: u16, pub mint_authority: Pubkey, @@ -42,180 +15,61 @@ pub struct CreateMintParamsData { pub mint: Pubkey, pub bump: u8, pub freeze_authority: Option, + pub mint_seed_pubkey: Pubkey, } -/// Process instruction to create two compressed mints using CPI context. +/// Parameters for creating one or more compressed mints with decompression. /// -/// The signer (ctx.accounts.signer) is used as both fee_payer and authority. +/// Creates N compressed mints and decompresses all to Solana Mint accounts. +/// Uses CPI context pattern when N > 1 for efficiency. /// -/// Account layout (remaining_accounts): -/// - accounts[0]: light_system_program -/// - accounts[1]: mint_signer_1 (SIGNER) -/// - accounts[2]: mint_signer_2 (SIGNER) -/// - accounts[3]: cpi_authority_pda -/// - accounts[4]: registered_program_pda -/// - accounts[5]: account_compression_authority -/// - accounts[6]: account_compression_program -/// - accounts[7]: system_program -/// - accounts[8]: cpi_context_account (writable) -/// - accounts[9]: output_queue (writable) -/// - accounts[10]: address_tree (writable) -/// - accounts[11]: compressed_token_program (for CPI) -pub fn process_create_two_mints<'info>( - ctx: Context<'_, '_, '_, 'info, crate::Generic<'info>>, - data: CreateTwoMintsData, -) -> Result<()> { - let accounts = ctx.remaining_accounts; - let payer = ctx.accounts.signer.to_account_info(); - - // === CPI 1: Write first mint to CPI context (no proof) === - let mint_instruction_data_1 = MintInstructionData { - supply: 0, - decimals: data.params_1.decimals, - metadata: MintMetadata { - version: 3, - mint: data.params_1.mint.to_bytes().into(), - mint_decompressed: false, - mint_signer: accounts[1].key().to_bytes(), - bump: data.params_1.bump, - }, - mint_authority: Some(data.params_1.mint_authority.to_bytes().into()), - freeze_authority: data - .params_1 - .freeze_authority - .map(|auth| auth.to_bytes().into()), - extensions: None, - }; - - let cpi_context_1 = CpiContext { - set_context: false, - first_set_context: true, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, - token_out_queue_index: 0, - assigned_account_index: 0, - read_only_address_trees: [0; 4], - address_tree_pubkey: accounts[10].key().to_bytes(), - }; - - let instruction_data_1 = MintActionCompressedInstructionData::new_mint_write_to_cpi_context( - data.params_1.address_merkle_tree_root_index, - mint_instruction_data_1, - cpi_context_1, - ); - - // Build account metas for CPI write (minimal accounts) - let cpi_write_config = MintActionMetaConfigCpiWrite { - fee_payer: SolanaPubkey::new_from_array(payer.key().to_bytes()), - mint_signer: Some(SolanaPubkey::new_from_array(accounts[1].key().to_bytes())), - authority: SolanaPubkey::new_from_array(payer.key().to_bytes()), - cpi_context: SolanaPubkey::new_from_array(accounts[8].key().to_bytes()), - }; - - let account_metas_1 = get_mint_action_instruction_account_metas_cpi_write(cpi_write_config); - - let ix_data_1 = instruction_data_1 - .data() - .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotSerialize)?; - - let instruction_1 = Instruction { - program_id: SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID), - accounts: account_metas_1, - data: ix_data_1, - }; - - // Invoke first CPI (write to context) - let account_infos_1 = vec![ - accounts[0].clone(), // light_system_program - accounts[1].clone(), // mint_signer_1 - payer.clone(), // authority (same as fee_payer) - payer.clone(), // fee_payer - accounts[3].clone(), // cpi_authority_pda - accounts[8].clone(), // cpi_context_account - accounts[11].clone(), // compressed_token_program - ]; - - invoke(&instruction_1, &account_infos_1)?; - - msg!("CPI 1: First mint written to CPI context"); - - // === CPI 2: Execute with proof (creates both compressed mints) === - let mint_instruction_data_2 = MintInstructionData { - supply: 0, - decimals: data.params_2.decimals, - metadata: MintMetadata { - version: 3, - mint: data.params_2.mint.to_bytes().into(), - mint_decompressed: false, - mint_signer: accounts[2].key().to_bytes(), - bump: data.params_2.bump, - }, - mint_authority: Some(data.params_2.mint_authority.to_bytes().into()), - freeze_authority: data - .params_2 - .freeze_authority - .map(|auth| auth.to_bytes().into()), - extensions: None, - }; - - // Execute from CPI context: set cpi_context with set_context=false, first_set_context=false - // This tells the program to READ from CPI context and execute - // For create_mint in execute mode, in_tree_index must be 1 (hardcoded requirement) - // Packed accounts: [0]=cpi_context, [1]=output_queue, [2]=address_tree - let cpi_context_2 = CpiContext { - set_context: false, - first_set_context: false, - in_tree_index: 1, // MUST be 1 for create_mint in execute mode with CPI context - in_queue_index: 0, // not used for create_mint - out_queue_index: 0, // output_queue index - token_out_queue_index: 0, - assigned_account_index: 1, // Second output account (first is from CPI context) - read_only_address_trees: [0; 4], - address_tree_pubkey: accounts[10].key().to_bytes(), - }; - - let instruction_data_2 = MintActionCompressedInstructionData::new_mint( - data.params_2.address_merkle_tree_root_index, - data.proof, - mint_instruction_data_2, - ) - .with_cpi_context(cpi_context_2); - - // Build account structure for CPI using MintActionCpiAccounts - let empty_vec: Vec> = vec![]; - let mint_action_accounts = MintActionCpiAccounts { - compressed_token_program: &accounts[11], - light_system_program: &accounts[0], - mint_signer: Some(&accounts[2]), - authority: &payer, - fee_payer: &payer, - compressed_token_cpi_authority: &accounts[3], - registered_program_pda: &accounts[4], - account_compression_authority: &accounts[5], - account_compression_program: &accounts[6], - system_program: &accounts[7], - cpi_context: Some(&accounts[8]), - out_output_queue: &accounts[9], - in_merkle_tree: &accounts[10], - in_output_queue: None, - tokens_out_queue: None, - ctoken_accounts: &empty_vec, - }; +/// Flow: +/// - N=1: Single CPI (create + decompress) +/// - N>1: 2N-1 CPIs (N-1 writes + 1 execute with decompress + N-1 decompress) +#[derive(Clone, AnchorSerialize, AnchorDeserialize)] +pub struct CreateMintsParams { + /// Parameters for each mint to create + pub mints: Vec, + /// Single proof covering all new addresses + pub proof: CompressedProof, +} - // Build instruction using the trait method - let instruction_2 = instruction_data_2 - .instruction(&mint_action_accounts) - .unwrap(); +impl CreateMintsParams { + pub fn new(mints: Vec, proof: CompressedProof) -> Self { + Self { mints, proof } + } +} - // Invoke second CPI (execute with proof, reads from CPI context) - let account_infos_2: Vec<_> = std::iter::once(payer) - .chain(accounts.iter().cloned()) +/// Anchor instruction wrapper for create_mints. +pub fn process_create_mints<'a, 'info>( + ctx: Context<'a, '_, 'info, 'info, crate::Generic<'info>>, + params: CreateMintsParams, +) -> Result<()> { + // Convert anchor types to SDK types + let sdk_mints: Vec> = params + .mints + .iter() + .map(|m| SingleMintParams { + decimals: m.decimals, + address_merkle_tree_root_index: m.address_merkle_tree_root_index, + mint_authority: solana_pubkey::Pubkey::new_from_array(m.mint_authority.to_bytes()), + compression_address: m.compression_address, + mint: solana_pubkey::Pubkey::new_from_array(m.mint.to_bytes()), + bump: m.bump, + freeze_authority: m + .freeze_authority + .map(|a| solana_pubkey::Pubkey::new_from_array(a.to_bytes())), + mint_seed_pubkey: solana_pubkey::Pubkey::new_from_array(m.mint_seed_pubkey.to_bytes()), + authority_seeds: None, + mint_signer_seeds: None, + }) .collect(); - invoke(&instruction_2, &account_infos_2)?; + let sdk_params = SdkCreateMintsParams::new(&sdk_mints, params.proof); - msg!("CPI 2: Both compressed mints created with single proof"); + let payer = ctx.accounts.signer.to_account_info(); + create_mints(&payer, ctx.remaining_accounts, sdk_params) + .map_err(|_| anchor_lang::error::ErrorCode::InstructionDidNotSerialize)?; Ok(()) } diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs index 4f54643aa3..c2300e0076 100644 --- a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -1,18 +1,31 @@ use anchor_lang::InstructionData; use light_program_test::{AddressWithTree, Indexer, LightProgramTest, ProgramTestConfig, Rpc}; -use light_token_sdk::token::{derive_mint_compressed_address, find_mint_address, SystemAccounts}; -use sdk_token_test::{CreateMintParamsData, CreateTwoMintsData}; +use light_token_sdk::token::{ + config_pda, derive_mint_compressed_address, find_mint_address, rent_sponsor_pda, + SystemAccounts, LIGHT_TOKEN_PROGRAM_ID, +}; +use sdk_token_test::{CreateMintsParams, MintParams}; use solana_sdk::{ instruction::{AccountMeta, Instruction}, signature::{Keypair, Signer}, }; -/// Test creating two compressed mints using CPI context in a single transaction. -/// First CPI writes first mint to CPI context, second CPI executes both with single proof. -/// Both mints remain as compressed accounts (no Solana account created). #[tokio::test] -async fn test_create_two_compressed_mints_cpi_context() { - // 1. Setup test environment +async fn test_create_single_mint() { + test_create_mints(1).await; +} + +#[tokio::test] +async fn test_create_two_mints() { + test_create_mints(2).await; +} + +#[tokio::test] +async fn test_create_three_mints() { + test_create_mints(3).await; +} + +async fn test_create_mints(n: usize) { let mut rpc = LightProgramTest::new(ProgramTestConfig::new_v2( false, Some(vec![("sdk_token_test", sdk_token_test::ID)]), @@ -21,141 +34,127 @@ async fn test_create_two_compressed_mints_cpi_context() { .unwrap(); let payer = rpc.get_payer().insecure_clone(); + let mint_signers: Vec = (0..n).map(|_| Keypair::new()).collect(); - // 2. Generate two mint signers - let mint_signer_1 = Keypair::new(); - let mint_signer_2 = Keypair::new(); - - // 3. Get tree info let address_tree_info = rpc.get_address_tree_v2(); let state_tree_info = rpc.get_random_state_tree_info().unwrap(); - // 4. Derive addresses for both mints - let compression_address_1 = - derive_mint_compressed_address(&mint_signer_1.pubkey(), &address_tree_info.tree); - let compression_address_2 = - derive_mint_compressed_address(&mint_signer_2.pubkey(), &address_tree_info.tree); - let (mint_pda_1, bump_1) = find_mint_address(&mint_signer_1.pubkey()); - let (mint_pda_2, bump_2) = find_mint_address(&mint_signer_2.pubkey()); + let compression_addresses: Vec<[u8; 32]> = mint_signers + .iter() + .map(|signer| derive_mint_compressed_address(&signer.pubkey(), &address_tree_info.tree)) + .collect(); + + let mint_pdas: Vec<(solana_sdk::pubkey::Pubkey, u8)> = mint_signers + .iter() + .map(|signer| find_mint_address(&signer.pubkey())) + .collect(); + + let addresses_with_trees: Vec = compression_addresses + .iter() + .map(|addr| AddressWithTree { + address: *addr, + tree: address_tree_info.tree, + }) + .collect(); - // 5. Get SINGLE validity proof for BOTH addresses let proof_result = rpc - .get_validity_proof( - vec![], - vec![ - AddressWithTree { - address: compression_address_1, - tree: address_tree_info.tree, - }, - AddressWithTree { - address: compression_address_2, - tree: address_tree_info.tree, - }, - ], - None, - ) + .get_validity_proof(vec![], addresses_with_trees, None) .await .unwrap() .value; - // 6. Build CreateMintParamsData for both mints - let params_1 = CreateMintParamsData { - decimals: 9, - address_merkle_tree_root_index: proof_result.addresses[0].root_index, - mint_authority: payer.pubkey(), - compression_address: compression_address_1, - mint: mint_pda_1, - bump: bump_1, - freeze_authority: None, - }; - - let params_2 = CreateMintParamsData { - decimals: 6, - address_merkle_tree_root_index: proof_result.addresses[1].root_index, - mint_authority: payer.pubkey(), - compression_address: compression_address_2, - mint: mint_pda_2, - bump: bump_2, - freeze_authority: None, - }; + let mints: Vec = mint_signers + .iter() + .zip(compression_addresses.iter()) + .zip(mint_pdas.iter()) + .enumerate() + .map( + |(i, ((signer, compression_address), (mint_pda, bump)))| MintParams { + decimals: (6 + i) as u8, + address_merkle_tree_root_index: proof_result.addresses[i].root_index, + mint_authority: payer.pubkey(), + compression_address: *compression_address, + mint: *mint_pda, + bump: *bump, + freeze_authority: None, + mint_seed_pubkey: signer.pubkey(), + }, + ) + .collect(); - // 7. Build instruction data - let data = CreateTwoMintsData { - params_1, - params_2, - proof: proof_result.proof.0.unwrap(), - }; + let params = CreateMintsParams::new(mints, proof_result.proof.0.unwrap()); - // 8. Build account metas let system_accounts = SystemAccounts::default(); let cpi_context_pubkey = state_tree_info .cpi_context .expect("CPI context account required"); - let compressed_token_program_id = - solana_sdk::pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID); // Account layout (remaining_accounts): - // - accounts[0]: light_system_program - // - accounts[1]: mint_signer_1 (SIGNER) - // - accounts[2]: mint_signer_2 (SIGNER) - // - accounts[3]: cpi_authority_pda - // - accounts[4]: registered_program_pda - // - accounts[5]: account_compression_authority - // - accounts[6]: account_compression_program - // - accounts[7]: system_program - // - accounts[8]: cpi_context_account (writable) - // - accounts[9]: output_queue (writable) - // - accounts[10]: address_tree (writable) - // - accounts[11]: compressed_token_program (for CPI) - let accounts = vec![ - // Anchor accounts (signer is the payer) + // [0]: light_system_program + // [1..N+1]: mint_signers (SIGNER) + // [N+1..N+6]: system PDAs (cpi_authority, registered_program, compression_authority, compression_program, system_program) + // [N+6]: cpi_context_account + // [N+7]: output_queue + // [N+8]: address_tree + // [N+9]: compressible_config + // [N+10]: rent_sponsor + // [N+11]: state_merkle_tree + // [N+12..2N+12]: mint_pdas + // [2N+12]: compressed_token_program (for CPI) + let mut accounts = vec![ AccountMeta::new(payer.pubkey(), true), - // remaining_accounts - AccountMeta::new_readonly(system_accounts.light_system_program, false), // [0] - AccountMeta::new_readonly(mint_signer_1.pubkey(), true), // [1] SIGNER - AccountMeta::new_readonly(mint_signer_2.pubkey(), true), // [2] SIGNER - AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false), // [3] - AccountMeta::new_readonly(system_accounts.registered_program_pda, false), // [4] - AccountMeta::new_readonly(system_accounts.account_compression_authority, false), // [5] - AccountMeta::new_readonly(system_accounts.account_compression_program, false), // [6] - AccountMeta::new_readonly(system_accounts.system_program, false), // [7] - AccountMeta::new(cpi_context_pubkey, false), // [8] - AccountMeta::new(state_tree_info.queue, false), // [9] - AccountMeta::new(address_tree_info.tree, false), // [10] - AccountMeta::new_readonly(compressed_token_program_id, false), // [11] + AccountMeta::new_readonly(system_accounts.light_system_program, false), ]; + for signer in &mint_signers { + accounts.push(AccountMeta::new_readonly(signer.pubkey(), true)); + } + + accounts.extend(vec![ + AccountMeta::new_readonly(system_accounts.cpi_authority_pda, false), + AccountMeta::new_readonly(system_accounts.registered_program_pda, false), + AccountMeta::new_readonly(system_accounts.account_compression_authority, false), + AccountMeta::new_readonly(system_accounts.account_compression_program, false), + AccountMeta::new_readonly(system_accounts.system_program, false), + AccountMeta::new(cpi_context_pubkey, false), + AccountMeta::new(state_tree_info.queue, false), + AccountMeta::new(address_tree_info.tree, false), + AccountMeta::new_readonly(config_pda().into(), false), + AccountMeta::new(rent_sponsor_pda().into(), false), + AccountMeta::new(state_tree_info.tree, false), + ]); + + for (mint_pda, _) in &mint_pdas { + accounts.push(AccountMeta::new(*mint_pda, false)); + } + + // Append compressed token program at the end for CPI + accounts.push(AccountMeta::new_readonly(LIGHT_TOKEN_PROGRAM_ID, false)); + let instruction = Instruction { program_id: sdk_token_test::ID, accounts, - data: sdk_token_test::instruction::CreateTwoMints { data }.data(), + data: sdk_token_test::instruction::CreateMints { params }.data(), }; - // 9. Send transaction - rpc.create_and_send_transaction( - &[instruction], - &payer.pubkey(), - &[&payer, &mint_signer_1, &mint_signer_2], - ) - .await - .unwrap(); + let mut signers: Vec<&Keypair> = vec![&payer]; + signers.extend(mint_signers.iter()); - // 10. Verify both compressed mints were created by querying the indexer - // Since these are compressed accounts (no Solana account), we verify via indexer - let compressed_accounts = rpc - .indexer() - .unwrap() - .get_compressed_accounts_by_owner(&payer.pubkey(), None, None) + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &signers) .await .unwrap(); - // Check that we have at least 2 compressed accounts (the mints) - assert!( - compressed_accounts.value.items.len() >= 2, - "Should have at least 2 compressed accounts" - ); - - println!("Successfully created two compressed mints in single transaction!"); - println!(" Mint 1 address: {:?}", compression_address_1); - println!(" Mint 2 address: {:?}", compression_address_2); + for (i, (mint_pda, _)) in mint_pdas.iter().enumerate() { + let mint_account = rpc + .get_account(*mint_pda) + .await + .expect("Failed to get mint account") + .expect(&format!("Mint PDA {} should exist after decompress", i + 1)); + + assert!( + !mint_account.data.is_empty(), + "Mint {} account should have data", + i + 1 + ); + } } From 9c461266c928a15edbdfe229180f4327efee16d4 Mon Sep 17 00:00:00 2001 From: ananas Date: Sun, 18 Jan 2026 10:18:20 +0000 Subject: [PATCH 11/11] add multiple mint support to sdk macros --- program-libs/compressible/src/lib.rs | 3 + programs/system/src/cpi_context/state.rs | 1 - .../src/create_accounts_proof.rs | 29 +- sdk-libs/compressible-client/src/pack.rs | 56 ++- .../src/rentfree/account/seed_extraction.rs | 44 +- .../macros/src/rentfree/accounts/builder.rs | 45 +- .../src/rentfree/accounts/light_mint.rs | 450 ++++++++---------- .../macros/src/rentfree/program/decompress.rs | 9 +- .../src/rentfree/program/instructions.rs | 5 +- sdk-libs/sdk-types/src/cpi_accounts/v2.rs | 7 + sdk-libs/token-sdk/src/token/create_mints.rs | 199 +++++--- .../src/instruction_accounts.rs | 188 ++++++++ .../csdk-anchor-full-derived-test/src/lib.rs | 29 +- .../amm_observation_state_test.rs | 43 +- .../account_macros/amm_pool_state_test.rs | 7 +- .../account_macros/core_game_session_test.rs | 5 +- .../core_placeholder_record_test.rs | 10 +- .../account_macros/core_user_record_test.rs | 10 +- .../account_macros/d1_all_field_types_test.rs | 31 +- .../tests/account_macros/d1_array_test.rs | 9 +- .../account_macros/d1_multi_pubkey_test.rs | 7 +- .../tests/account_macros/d1_no_pubkey_test.rs | 14 +- .../tests/account_macros/d1_non_copy_test.rs | 14 +- .../d1_option_primitive_test.rs | 9 +- .../account_macros/d1_option_pubkey_test.rs | 11 +- .../account_macros/d1_single_pubkey_test.rs | 5 +- .../account_macros/d2_all_compress_as_test.rs | 19 +- .../d2_multiple_compress_as_test.rs | 11 +- .../account_macros/d2_no_compress_as_test.rs | 14 +- .../d2_option_none_compress_as_test.rs | 19 +- .../d2_single_compress_as_test.rs | 10 +- .../account_macros/d4_all_composition_test.rs | 27 +- .../tests/account_macros/d4_info_last_test.rs | 11 +- .../tests/account_macros/d4_large_test.rs | 10 +- .../tests/account_macros/d4_minimal_test.rs | 5 +- .../tests/account_macros/shared.rs | 26 +- .../tests/basic_test.rs | 396 ++++++++++++++- .../tests/integration_tests.rs | 57 +-- .../tests/test_create_two_mints.rs | 22 +- 39 files changed, 1295 insertions(+), 572 deletions(-) diff --git a/program-libs/compressible/src/lib.rs b/program-libs/compressible/src/lib.rs index dc16a63c3d..1edbcaaddd 100644 --- a/program-libs/compressible/src/lib.rs +++ b/program-libs/compressible/src/lib.rs @@ -22,4 +22,7 @@ pub struct CreateAccountsProof { pub address_tree_info: PackedAddressTreeInfo, /// Output state tree index for new compressed accounts. pub output_state_tree_index: u8, + /// State merkle tree index (needed for mint creation decompress validation). + /// This is optional to maintain backwards compatibility. + pub state_tree_index: Option, } diff --git a/programs/system/src/cpi_context/state.rs b/programs/system/src/cpi_context/state.rs index b4814711d9..0123216878 100644 --- a/programs/system/src/cpi_context/state.rs +++ b/programs/system/src/cpi_context/state.rs @@ -131,7 +131,6 @@ impl<'a> ZCpiContextAccount2<'a> { &'a mut self, instruction_data: &WrappedInstructionData<'b, T>, ) -> Result<(), SystemProgramError> { - let pre_address_len = self.new_addresses.len(); // Cache owner bytes to avoid repeated calls let owner_bytes = instruction_data.owner().to_bytes(); diff --git a/sdk-libs/compressible-client/src/create_accounts_proof.rs b/sdk-libs/compressible-client/src/create_accounts_proof.rs index ff20f46d41..4761ac676a 100644 --- a/sdk-libs/compressible-client/src/create_accounts_proof.rs +++ b/sdk-libs/compressible-client/src/create_accounts_proof.rs @@ -17,7 +17,7 @@ use solana_instruction::AccountMeta; use solana_pubkey::Pubkey; use thiserror::Error; -use crate::pack::{pack_proof, PackError}; +use crate::pack::{pack_proof, pack_proof_for_mints, PackError}; /// Error type for create accounts proof operations. #[derive(Debug, Error)] @@ -156,6 +156,7 @@ pub async fn get_create_accounts_proof( proof: ValidityProof::default(), address_tree_info: PackedAddressTreeInfo::default(), output_state_tree_index: packed.output_tree_index, + state_tree_index: None, }, remaining_accounts: packed.remaining_accounts, }); @@ -191,7 +192,7 @@ pub async fn get_create_accounts_proof( .get_random_state_tree_info() .map_err(CreateAccountsProofError::Rpc)?; - // 6. Determine CPI context + // 6. Determine CPI context and whether we have mints // For INIT with mints: need CPI context for cross-program invocation let has_mints = inputs .iter() @@ -202,13 +203,22 @@ pub async fn get_create_accounts_proof( None }; - // 7. Pack proof - let packed = pack_proof( - program_id, - validity_proof.clone(), - &state_tree_info, - cpi_context, - )?; + // 7. Pack proof (use mint-aware packing if mints are present) + let packed = if has_mints { + pack_proof_for_mints( + program_id, + validity_proof.clone(), + &state_tree_info, + cpi_context, + )? + } else { + 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 @@ -223,6 +233,7 @@ pub async fn get_create_accounts_proof( proof: validity_proof.proof, address_tree_info, output_state_tree_index: packed.output_tree_index, + state_tree_index: packed.state_tree_index, }, remaining_accounts: packed.remaining_accounts, }) diff --git a/sdk-libs/compressible-client/src/pack.rs b/sdk-libs/compressible-client/src/pack.rs index 180a28462e..d212d61f7f 100644 --- a/sdk-libs/compressible-client/src/pack.rs +++ b/sdk-libs/compressible-client/src/pack.rs @@ -56,6 +56,8 @@ pub struct PackedProofResult { pub packed_tree_infos: PackedTreeInfos, /// Index of output tree in remaining accounts. Pass to instruction data. pub output_tree_index: u8, + /// Index of state merkle tree in remaining accounts (when included for mint creation). + pub state_tree_index: Option, /// Offset where system accounts start. Pass to instruction data if needed. pub system_accounts_offset: u8, } @@ -81,6 +83,39 @@ pub fn pack_proof( proof: ValidityProofWithContext, output_tree: &TreeInfo, cpi_context: Option, +) -> Result { + pack_proof_internal(program_id, proof, output_tree, cpi_context, false) +} + +/// Packs a validity proof with state merkle tree for mint creation. +/// +/// Same as `pack_proof` but also includes the state merkle tree in remaining accounts. +/// This is required for mint creation because the decompress operation needs the state +/// merkle tree for discriminator validation. +/// +/// # 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 for mint creation. +/// +/// # Returns +/// `PackedProofResult` with `state_tree_index` populated. +pub fn pack_proof_for_mints( + program_id: &Pubkey, + proof: ValidityProofWithContext, + output_tree: &TreeInfo, + cpi_context: Option, +) -> Result { + pack_proof_internal(program_id, proof, output_tree, cpi_context, true) +} + +fn pack_proof_internal( + program_id: &Pubkey, + proof: ValidityProofWithContext, + output_tree: &TreeInfo, + cpi_context: Option, + include_state_tree: bool, ) -> Result { let mut packed = PackedAccounts::default(); @@ -97,7 +132,25 @@ pub fn pack_proof( .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); + // For mint creation: pack address tree first (must be at index 1 per program validation), + // then state tree. For non-mint: just pack tree infos normally. + let (client_packed_tree_infos, state_tree_index) = if include_state_tree { + // Pack tree infos first to ensure address tree is at index 1 + let tree_infos = proof.pack_tree_infos(&mut packed); + + // Then add state tree (will be after address tree) + let state_tree = output_tree + .next_tree_info + .as_ref() + .map(|n| n.tree) + .unwrap_or(output_tree.tree); + let state_idx = packed.insert_or_get(state_tree); + + (tree_infos, Some(state_idx)) + } else { + let tree_infos = proof.pack_tree_infos(&mut packed); + (tree_infos, None) + }; let (remaining_accounts, system_offset, _) = packed.to_account_metas(); // Convert from light_client's types to our local types @@ -115,6 +168,7 @@ pub fn pack_proof( remaining_accounts, packed_tree_infos, output_tree_index, + state_tree_index, system_accounts_offset: system_offset as u8, }) } diff --git a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs index 60a4afb3d1..fc6150f614 100644 --- a/sdk-libs/macros/src/rentfree/account/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/account/seed_extraction.rs @@ -69,6 +69,8 @@ pub struct ExtractedAccountsInfo { pub struct_name: Ident, pub pda_fields: Vec, pub token_fields: Vec, + /// True if struct has any #[light_mint] fields + pub has_light_mint_fields: bool, } /// Extract rentfree field info from an Accounts struct @@ -82,6 +84,7 @@ pub fn extract_from_accounts_struct( let mut pda_fields = Vec::new(); let mut token_fields = Vec::new(); + let mut has_light_mint_fields = false; for field in fields { let field_ident = match &field.ident { @@ -95,6 +98,16 @@ pub fn extract_from_accounts_struct( .iter() .any(|attr| attr.path().is_ident("rentfree")); + // Check for #[light_mint(...)] attribute + let has_light_mint = field + .attrs + .iter() + .any(|attr| attr.path().is_ident("light_mint")); + + if has_light_mint { + has_light_mint_fields = true; + } + // Check for #[rentfree_token(...)] attribute let token_attr = extract_rentfree_token_attr(&field.attrs); @@ -146,8 +159,8 @@ pub fn extract_from_accounts_struct( } } - // If no rentfree fields found, return None - if pda_fields.is_empty() && token_fields.is_empty() { + // If no rentfree/light_mint fields found, return None + if pda_fields.is_empty() && token_fields.is_empty() && !has_light_mint_fields { return Ok(None); } @@ -190,6 +203,7 @@ pub fn extract_from_accounts_struct( struct_name: item.ident.clone(), pda_fields, token_fields, + has_light_mint_fields, })) } @@ -635,16 +649,18 @@ pub fn get_params_only_seed_fields_from_spec( if let SeedElement::Expression(expr) = seed { if let Some((field_name, has_conversion)) = extract_data_field_from_expr(expr) { let field_str = field_name.to_string(); - // Only include fields that are NOT on the state struct - if !state_field_names.contains(&field_str) { - if !fields.iter().any(|(f, _, _): &(Ident, _, _)| f == &field_name) { - let field_type: syn::Type = if has_conversion { - syn::parse_quote!(u64) - } else { - syn::parse_quote!(solana_pubkey::Pubkey) - }; - fields.push((field_name, field_type, has_conversion)); - } + // Only include fields that are NOT on the state struct and not already added + if !state_field_names.contains(&field_str) + && !fields + .iter() + .any(|(f, _, _): &(Ident, _, _)| f == &field_name) + { + let field_type: syn::Type = if has_conversion { + syn::parse_quote!(u64) + } else { + syn::parse_quote!(solana_pubkey::Pubkey) + }; + fields.push((field_name, field_type, has_conversion)); } } } @@ -668,8 +684,8 @@ fn extract_data_field_from_expr(expr: &syn::Expr) -> Option<(Ident, bool)> { } syn::Expr::MethodCall(method_call) => { // Handle data.field.to_le_bytes().as_ref() etc. - let has_bytes_conversion = method_call.method == "to_le_bytes" - || method_call.method == "to_be_bytes"; + let has_bytes_conversion = + method_call.method == "to_le_bytes" || method_call.method == "to_be_bytes"; if has_bytes_conversion { return extract_data_field_from_expr(&method_call.receiver) .map(|(name, _)| (name, true)); diff --git a/sdk-libs/macros/src/rentfree/accounts/builder.rs b/sdk-libs/macros/src/rentfree/accounts/builder.rs index a6d0cee7bb..14ba9d463c 100644 --- a/sdk-libs/macros/src/rentfree/accounts/builder.rs +++ b/sdk-libs/macros/src/rentfree/accounts/builder.rs @@ -8,7 +8,7 @@ use quote::quote; use syn::DeriveInput; use super::{ - light_mint::{InfraRefs, LightMintBuilder}, + light_mint::{InfraRefs, LightMintsBuilder}, parse::ParsedRentFreeStruct, pda::generate_pda_compress_blocks, }; @@ -96,8 +96,8 @@ impl RentFreeBuilder { /// 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 + /// 2. Invoke CreateMintsCpi with CPI context offset + /// After this, Mints are "hot" and usable in instruction body pub fn generate_pre_init_pdas_and_mints(&self) -> TokenStream { let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&self.parsed.rentfree_fields); @@ -117,17 +117,10 @@ impl RentFreeBuilder { // Get the first PDA's output tree index (for the state tree output queue) let first_pda_output_tree = &self.parsed.rentfree_fields[0].output_tree; - // TODO(diff-pr): Support multiple #[light_mint] fields by looping here. - // Each mint would get assigned_account_index = pda_count + mint_index. - // Also add support for #[rentfree_token] fields for token ATAs. - let mint = &self.parsed.light_mint_fields[0]; - - // assigned_account_index for mint is after PDAs - let mint_assigned_index = pda_count as u8; - - // Generate mint action invocation with CPI context - let mint_invocation = LightMintBuilder::new(mint, params_ident, &self.infra) - .with_cpi_context(quote! { #first_pda_output_tree }, mint_assigned_index) + // Generate CreateMintsCpi invocation for all mints with PDA context offset + let mints = &self.parsed.light_mint_fields; + let mint_invocation = LightMintsBuilder::new(mints, params_ident, &self.infra) + .with_pda_context(pda_count, quote! { #first_pda_output_tree }) .generate_invocation(); // Infrastructure field references for quote! interpolation @@ -171,7 +164,7 @@ impl RentFreeBuilder { .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 + // Step 2: Create mints using CreateMintsCpi with CPI context offset #mint_invocation Ok(true) @@ -179,8 +172,8 @@ impl RentFreeBuilder { } /// Generate LightPreInit body for mints-only (no PDAs): - /// Invoke mint_action with decompress directly - /// After this, CMint is "hot" and usable in instruction body + /// Invoke CreateMintsCpi with decompress directly + /// After this, Mints are "hot" and usable in instruction body pub fn generate_pre_init_mints_only(&self) -> TokenStream { // Get instruction param ident let params_ident = &self @@ -192,27 +185,23 @@ impl RentFreeBuilder { .unwrap() .name; - // TODO(diff-pr): Support multiple #[light_mint] fields by looping here. - // Each mint would get assigned_account_index = mint_index. - // Also add support for #[rentfree_token] fields for token ATAs. - let mint = &self.parsed.light_mint_fields[0]; - - // Generate mint action invocation without CPI context + // Generate CreateMintsCpi invocation for all mints (no PDA context) + let mints = &self.parsed.light_mint_fields; let mint_invocation = - LightMintBuilder::new(mint, params_ident, &self.infra).generate_invocation(); + LightMintsBuilder::new(mints, params_ident, &self.infra).generate_invocation(); // Infrastructure field reference for quote! interpolation let fee_payer = &self.infra.fee_payer; quote! { - // Build CPI accounts (no CPI context needed for mints-only) - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + // Build CPI accounts with CPI context enabled (mints use CPI context for batching) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( &self.#fee_payer, _remaining, - crate::LIGHT_CPI_SIGNER, + light_sdk::cpi::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), ); - // Build and invoke mint_action with decompress + // Create mints using CreateMintsCpi #mint_invocation Ok(true) diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index cbdbebfa40..42f2fb60a6 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -143,7 +143,6 @@ pub(super) struct InfraRefs { pub compression_config: TokenStream, pub ctoken_config: TokenStream, pub ctoken_rent_sponsor: TokenStream, - pub light_token_program: TokenStream, pub ctoken_cpi_authority: TokenStream, } @@ -158,7 +157,6 @@ impl InfraRefs { &infra.ctoken_rent_sponsor, "ctoken_rent_sponsor", ), - light_token_program: resolve_field_name(&infra.ctoken_program, "light_token_program"), ctoken_cpi_authority: resolve_field_name( &infra.ctoken_cpi_authority, "ctoken_cpi_authority", @@ -167,290 +165,264 @@ impl InfraRefs { } } -/// Parts of generated code that differ based on CPI context presence. +/// Builder for generating code that creates multiple compressed mints using CreateMintsCpi. /// -/// - **With CPI context**: Used when batching mint creation with PDA compression. -/// The mint shares output tree with PDAs, uses assigned_account_index for ordering. -/// -/// - **Without CPI context**: Used for mint-only instructions. -/// The mint uses its own address tree info directly. -struct CpiContextParts { - /// Queue access expression (how to get output queue index) - queue_access: TokenStream, - /// Setup block (defines __output_tree_index if needed) - setup: TokenStream, - /// Method chain for CPI context configuration on instruction data - chain: TokenStream, - /// Meta config assignment (sets cpi_context on meta_config) - meta_assignment: TokenStream, - /// Variable binding for instruction_data (mut or not) - data_binding: TokenStream, -} - -impl CpiContextParts { - fn new(cpi_context: &Option<(TokenStream, u8)>) -> Self { - match cpi_context { - Some((tree_expr, assigned_idx)) => Self { - // With CPI context - batching with PDAs - queue_access: quote! { __output_tree_index as usize }, - setup: quote! { let __output_tree_index = #tree_expr; }, - chain: quote! { - .with_cpi_context(light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: __tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, - in_tree_index: #tree_expr + 1, - in_queue_index: #tree_expr, - out_queue_index: #tree_expr, - token_out_queue_index: 0, - assigned_account_index: #assigned_idx, - read_only_address_trees: [0; 4], - }) - }, - meta_assignment: quote! { meta_config.cpi_context = Some(*cpi_accounts.cpi_context()?.key); }, - data_binding: quote! { let mut instruction_data }, - }, - None => Self { - // Without CPI context - mint only - queue_access: quote! { __tree_info.address_queue_pubkey_index as usize }, - setup: quote! {}, - chain: quote! {}, - meta_assignment: quote! {}, - data_binding: quote! { let instruction_data }, - }, - } - } -} - -/// Builder for mint code generation. +/// This replaces the previous single-mint LightMintBuilder with support for N mints. +/// Generated code uses `CreateMintsCpi` from light_token_sdk for optimal batching. /// /// Usage: /// ```ignore -/// LightMintBuilder::new(mint, params_ident, &infra) -/// .with_cpi_context(quote! { #first_pda_output_tree }, mint_assigned_index) +/// LightMintsBuilder::new(mints, params_ident, &infra) +/// .with_pda_context(pda_count, quote! { #first_pda_output_tree }) /// .generate_invocation() /// ``` -pub(super) struct LightMintBuilder<'a> { - mint: &'a LightMintField, +pub(super) struct LightMintsBuilder<'a> { + mints: &'a [LightMintField], params_ident: &'a Ident, infra: &'a InfraRefs, - cpi_context: Option<(TokenStream, u8)>, + /// PDA context: (pda_count, output_tree_expr) for batching with PDAs + pda_context: Option<(usize, TokenStream)>, } -impl<'a> LightMintBuilder<'a> { +impl<'a> LightMintsBuilder<'a> { /// Create builder with required fields. - pub fn new(mint: &'a LightMintField, params_ident: &'a Ident, infra: &'a InfraRefs) -> Self { + pub fn new(mints: &'a [LightMintField], params_ident: &'a Ident, infra: &'a InfraRefs) -> Self { Self { - mint, + mints, params_ident, infra, - cpi_context: None, + pda_context: None, } } - /// Configure CPI context for batching with PDAs. - pub fn with_cpi_context(mut self, tree_expr: TokenStream, assigned_idx: u8) -> Self { - self.cpi_context = Some((tree_expr, assigned_idx)); + /// Configure for batching with PDAs. + /// + /// When PDAs are written to CPI context first, this sets the offset for mint indices + /// so they don't collide with PDA indices. + pub fn with_pda_context(mut self, pda_count: usize, output_tree_expr: TokenStream) -> Self { + self.pda_context = Some((pda_count, output_tree_expr)); self } - /// Generate mint_action CPI invocation code. + /// Generate CreateMintsCpi invocation code for all mints. pub fn generate_invocation(self) -> TokenStream { - generate_mint_invocation(&self) + generate_mints_invocation(&self) } } -/// Generate mint_action invocation code. +/// Generate CreateMintsCpi invocation code for multiple mints. /// -/// This is the main orchestration function. Shows the high-level flow: -/// 1. Determine CPI context parts (single branching point for all CPI differences) -/// 2. Generate optional field expressions (signer_seeds, freeze_authority, etc.) -/// 3. Generate the complete mint_action CPI invocation block -fn generate_mint_invocation(builder: &LightMintBuilder) -> TokenStream { - let mint = builder.mint; +/// Flow: +/// 1. For each mint: derive PDA, build SingleMintParams +/// 2. Build arrays for mint_seed_accounts, mints +/// 3. Construct CreateMintsCpi struct +/// 4. Call invoke() - seeds are extracted from SingleMintParams internally +fn generate_mints_invocation(builder: &LightMintsBuilder) -> TokenStream { + let mints = builder.mints; let params_ident = builder.params_ident; - let infra = &builder.infra; - - // 2. Generate optional field expressions - let mint_seeds = &mint.mint_seeds; - let authority_seeds = &mint.authority_seeds; - let freeze_authority = mint - .freeze_authority - .as_ref() - .map(|f| quote! { Some(*self.#f.to_account_info().key) }) - .unwrap_or_else(|| quote! { None }); - let rent_payment = quote_option_or(&mint.rent_payment, quote! { 2u8 }); - let write_top_up = quote_option_or(&mint.write_top_up, quote! { 0u32 }); - - // 3. Generate the mint_action CPI block - 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; + let infra = builder.infra; + let mint_count = mints.len(); + // Infrastructure field references let fee_payer = &infra.fee_payer; let ctoken_config = &infra.ctoken_config; let ctoken_rent_sponsor = &infra.ctoken_rent_sponsor; - let light_token_program = &infra.light_token_program; let ctoken_cpi_authority = &infra.ctoken_cpi_authority; - // 1. Determine CPI context parts (single branching point) - let cpi = CpiContextParts::new(&builder.cpi_context); - - // Destructure CPI parts for use in quote - let CpiContextParts { - queue_access, - setup: cpi_setup, - chain: cpi_chain, - meta_assignment: cpi_meta_assignment, - data_binding, - } = cpi; - - // Generate invoke_signed call with appropriate signer seeds - let invoke_signed_call = match authority_seeds { - Some(auth_seeds) => { + // Determine CPI context offset based on PDA context + let (cpi_context_offset, output_tree_setup) = match &builder.pda_context { + Some((pda_count, tree_expr)) => { + let offset = *pda_count as u8; + ( + quote! { #offset }, + quote! { let __output_tree_index = #tree_expr; }, + ) + } + None => (quote! { 0u8 }, quote! {}), + }; + + // Generate code for each mint to build SingleMintParams + let mint_params_builds: Vec = mints + .iter() + .enumerate() + .map(|(idx, mint)| { + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + let freeze_authority = mint + .freeze_authority + .as_ref() + .map(|f| quote! { Some(*self.#f.to_account_info().key) }) + .unwrap_or_else(|| quote! { None }); + let mint_seeds = &mint.mint_seeds; + let authority_seeds = &mint.authority_seeds; + + let idx_ident = format_ident!("__mint_param_{}", idx); + let pda_ident = format_ident!("__mint_pda_{}", idx); + let bump_ident = format_ident!("__mint_bump_{}", idx); + let signer_key_ident = format_ident!("__mint_signer_key_{}", idx); + let mint_seeds_ident = format_ident!("__mint_seeds_{}", idx); + let authority_seeds_ident = format_ident!("__authority_seeds_{}", idx); + + // Generate optional authority seeds binding + let authority_seeds_binding = match authority_seeds { + Some(seeds) => quote! { + let #authority_seeds_ident: &[&[u8]] = #seeds; + let #authority_seeds_ident = Some(#authority_seeds_ident); + }, + None => quote! { + let #authority_seeds_ident: Option<&[&[u8]]> = None; + }, + }; + quote! { - let authority_seeds: &[&[u8]] = #auth_seeds; - anchor_lang::solana_program::program::invoke_signed( - &mint_action_ix, - &account_infos, - &[mint_seeds, authority_seeds] - )?; + // Mint #idx: derive PDA and build params + let #signer_key_ident = *self.#mint_signer.to_account_info().key; + let (#pda_ident, #bump_ident) = light_token_sdk::token::find_mint_address(&#signer_key_ident); + + let #mint_seeds_ident: &[&[u8]] = #mint_seeds; + #authority_seeds_binding + + let __tree_info = &#address_tree_info; + + let #idx_ident = light_token_sdk::token::SingleMintParams { + decimals: #decimals, + address_merkle_tree_root_index: __tree_info.root_index, + mint_authority: *self.#authority.to_account_info().key, + compression_address: #pda_ident.to_bytes(), + mint: #pda_ident, + bump: #bump_ident, + freeze_authority: #freeze_authority, + mint_seed_pubkey: #signer_key_ident, + authority_seeds: #authority_seeds_ident, + mint_signer_seeds: Some(#mint_seeds_ident), + }; } - } - None => { - // authority_seeds not provided - authority must be a transaction signer + }) + .collect(); + + // Generate array of SingleMintParams + let param_idents: Vec = (0..mint_count) + .map(|idx| { + let ident = format_ident!("__mint_param_{}", idx); + quote! { #ident } + }) + .collect(); + + // Generate array of mint seed AccountInfos + let mint_seed_account_exprs: Vec = mints + .iter() + .map(|mint| { + let mint_signer = &mint.mint_signer; + quote! { self.#mint_signer.to_account_info() } + }) + .collect(); + + // Generate array of mint AccountInfos + let mint_account_exprs: Vec = mints + .iter() + .map(|mint| { + let field_ident = &mint.field_ident; + quote! { self.#field_ident.to_account_info() } + }) + .collect(); + + // Get rent_payment and write_top_up from first mint (all mints share same params for now) + let rent_payment = quote_option_or(&mints[0].rent_payment, quote! { 16u8 }); + let write_top_up = quote_option_or(&mints[0].write_top_up, quote! { 766u32 }); + + // Authority signer check for mints without authority_seeds + let authority_signer_checks: Vec = mints + .iter() + .filter(|m| m.authority_seeds.is_none()) + .map(|mint| { + let authority = &mint.authority; quote! { - // Verify authority is a signer since authority_seeds was not provided if !self.#authority.to_account_info().is_signer { return Err(anchor_lang::solana_program::program_error::ProgramError::MissingRequiredSignature.into()); } - anchor_lang::solana_program::program::invoke_signed( - &mint_action_ix, - &account_infos, - &[mint_seeds] - )?; } - } - }; + }) + .collect(); - // ------------------------------------------------------------------------- - // Generated code block for mint_action CPI invocation. - // - // Interpolated variables from CpiContextParts (see struct for with/without cases): - // #cpi_setup - defines __output_tree_index when batching with PDAs - // #queue_access - expression to get output queue index - // #data_binding - "let mut" (with CPI) or "let" (without CPI) - // #cpi_chain - adds .with_cpi_context(...) when batching - // #cpi_meta_assignment - sets meta_config.cpi_context when batching - // - // Interpolated variables from #[light_mint(...)] attributes: - // #address_tree_info - tree info (default: params.create_accounts_proof.address_tree_info) - // #mint_signer - field that seeds the mint PDA - // #authority - mint authority field - // #decimals - mint decimals - // #freeze_authority - optional freeze authority (Some(*self.field.key) or None) - // #rent_payment - rent epochs for decompression (default: 2u8) - // #write_top_up - write top-up lamports (default: 0u32) - // #mint_seeds - PDA signer seeds for mint_signer (default: &[] as &[&[u8]]) - // #authority_seeds - PDA signer seeds for authority (optional, if authority is a PDA) - // - // Interpolated variables from infrastructure fields: - // #fee_payer, #ctoken_config, #ctoken_rent_sponsor, - // #light_token_program, #ctoken_cpi_authority, #mint_field_ident - // ------------------------------------------------------------------------- quote! { { - // Step 1: Resolve tree accounts - let __tree_info = &#address_tree_info; - let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; - #cpi_setup - let output_queue = cpi_accounts.get_tree_account_info(#queue_access)?; - let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); - - // Step 2: Derive mint PDA from mint_signer - 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); - - // Step 3: Extract proof from instruction params + #output_tree_setup + + // Extract proof from instruction params 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; - - // Step 4: Build mint instruction data - let compressed_mint_data = light_token_interface::instructions::mint_action::MintInstructionData { - supply: 0, - decimals: #decimals, - metadata: light_token_interface::state::MintMetadata { - version: 3, - mint: mint_pda.to_bytes().into(), - mint_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, - }; - - // Step 5: Build compressed instruction data with decompress config - #data_binding = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( - __tree_info.root_index, + // Build SingleMintParams for each mint + #(#mint_params_builds)* + + // Array of mint params + let __mint_params: [light_token_sdk::token::SingleMintParams<'_>; #mint_count] = [ + #(#param_idents),* + ]; + + // Array of mint seed AccountInfos + let __mint_seed_accounts: [solana_account_info::AccountInfo<'info>; #mint_count] = [ + #(#mint_seed_account_exprs),* + ]; + + // Array of mint AccountInfos + let __mint_accounts: [solana_account_info::AccountInfo<'info>; #mint_count] = [ + #(#mint_account_exprs),* + ]; + + // Get tree accounts and indices + // Output queue for state (compressed accounts) is at tree index 0 + // State merkle tree index comes from the proof (set by pack_proof_for_mints) + // Address merkle tree index comes from the proof's address_tree_info + let __tree_info = &#params_ident.create_accounts_proof.address_tree_info; + let __output_queue_index: u8 = 0; + let __state_tree_index: u8 = #params_ident.create_accounts_proof.state_tree_index + .ok_or(anchor_lang::prelude::ProgramError::InvalidArgument)?; + let __address_tree_index: u8 = __tree_info.address_merkle_tree_pubkey_index; + let __output_queue = cpi_accounts.get_tree_account_info(__output_queue_index as usize)?; + let __state_merkle_tree = cpi_accounts.get_tree_account_info(__state_tree_index as usize)?; + let __address_tree = cpi_accounts.get_tree_account_info(__address_tree_index as usize)?; + + // Build CreateMintsParams with tree indices + let __create_mints_params = light_token_sdk::token::CreateMintsParams::new( + &__mint_params, __proof, - compressed_mint_data, ) - .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { - rent_payment: #rent_payment, - write_top_up: #write_top_up, - }) - #cpi_chain; - - // Step 6: Build account metas for CPI - 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, - ); - - #cpi_meta_assignment - - let account_metas = meta_config.to_account_metas(); - - // Step 7: Serialize instruction data - use light_compressed_account::instruction_data::traits::LightInstructionData; - let ix_data = instruction_data.data() - .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; - - // Step 8: Build the CPI instruction - 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, - }; - - // Step 9: Collect account infos for CPI - let mut account_infos = cpi_accounts.to_account_infos(); - 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()); - - // Step 10: Invoke CPI with signer seeds - let mint_seeds: &[&[u8]] = #mint_seeds; - #invoke_signed_call + .with_rent_payment(#rent_payment) + .with_write_top_up(#write_top_up) // TODO: discuss to allow a different one per mint. + .with_cpi_context_offset(#cpi_context_offset) + .with_output_queue_index(__output_queue_index) + .with_address_tree_index(__address_tree_index) + .with_state_tree_index(__state_tree_index); + + // Check authority signers for mints without authority_seeds + #(#authority_signer_checks)* + + // Build and invoke CreateMintsCpi + // Seeds are extracted from SingleMintParams internally + light_token_sdk::token::CreateMintsCpi { + mint_seed_accounts: &__mint_seed_accounts, + payer: self.#fee_payer.to_account_info(), + address_tree: __address_tree.clone(), + output_queue: __output_queue.clone(), + state_merkle_tree: __state_merkle_tree.clone(), + compressible_config: self.#ctoken_config.to_account_info(), + mints: &__mint_accounts, + rent_sponsor: self.#ctoken_rent_sponsor.to_account_info(), + system_accounts: light_token_sdk::token::SystemAccountInfos { + light_system_program: cpi_accounts.light_system_program()?.clone(), + cpi_authority_pda: self.#ctoken_cpi_authority.to_account_info(), + registered_program_pda: cpi_accounts.registered_program_pda()?.clone(), + account_compression_authority: cpi_accounts.account_compression_authority()?.clone(), + account_compression_program: cpi_accounts.account_compression_program()?.clone(), + system_program: cpi_accounts.system_program()?.clone(), + }, + cpi_context_account: cpi_accounts.cpi_context()?.clone(), + params: __create_mints_params, + } + .invoke()?; } } } diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index 28dddd0031..a2148e7fc0 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -213,7 +213,8 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( // If so, use seed_params.field instead of skipping if let Some(field_name) = get_params_only_field_name(expr, state_field_names) { if params_only_names.contains(&field_name) { - let field_ident = syn::Ident::new(&field_name, proc_macro2::Span::call_site()); + let field_ident = + syn::Ident::new(&field_name, proc_macro2::Span::call_site()); let binding_name = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); @@ -226,8 +227,10 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( if has_conversion { // u64 field with to_le_bytes conversion // Must bind bytes to a variable to avoid temporary value dropped while borrowed - let bytes_binding_name = - syn::Ident::new(&format!("{}_bytes", binding_name), proc_macro2::Span::call_site()); + let bytes_binding_name = syn::Ident::new( + &format!("{}_bytes", binding_name), + proc_macro2::Span::call_site(), + ); bindings.push(quote! { let #binding_name = seed_params.#field_ident .ok_or(solana_program_error::ProgramError::InvalidAccountData)?; diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index c8a78521f2..1ca6f24e1b 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -500,7 +500,10 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< for item_struct in crate_ctx.structs_with_derive("Accounts") { if let Some(info) = extract_from_accounts_struct(item_struct)? { - if !info.pda_fields.is_empty() || !info.token_fields.is_empty() { + if !info.pda_fields.is_empty() + || !info.token_fields.is_empty() + || info.has_light_mint_fields + { rentfree_struct_names.insert(info.struct_name.to_string()); pda_specs.extend(info.pda_fields); token_specs.extend(info.token_fields); diff --git a/sdk-libs/sdk-types/src/cpi_accounts/v2.rs b/sdk-libs/sdk-types/src/cpi_accounts/v2.rs index 46af83683b..8773961509 100644 --- a/sdk-libs/sdk-types/src/cpi_accounts/v2.rs +++ b/sdk-libs/sdk-types/src/cpi_accounts/v2.rs @@ -88,6 +88,13 @@ impl<'a, T: AccountInfoTrait + Clone> CpiAccounts<'a, T> { self.fee_payer } + pub fn light_system_program(&self) -> Result<&'a T> { + let index = CompressionCpiAccountIndex::LightSystemProgram as usize; + self.accounts + .get(index) + .ok_or(LightSdkTypesError::CpiAccountsIndexOutOfBounds(index)) + } + pub fn authority(&self) -> Result<&'a T> { let index = CompressionCpiAccountIndex::Authority as usize; self.accounts diff --git a/sdk-libs/token-sdk/src/token/create_mints.rs b/sdk-libs/token-sdk/src/token/create_mints.rs index 40c927986d..8ffa836fe5 100644 --- a/sdk-libs/token-sdk/src/token/create_mints.rs +++ b/sdk-libs/token-sdk/src/token/create_mints.rs @@ -20,18 +20,16 @@ use light_token_interface::{ LIGHT_TOKEN_PROGRAM_ID, }; use solana_account_info::AccountInfo; -use solana_cpi::invoke; use solana_instruction::Instruction; use solana_program_error::ProgramError; use solana_pubkey::Pubkey; +use super::SystemAccountInfos; use crate::compressed_token::mint_action::{ get_mint_action_instruction_account_metas_cpi_write, MintActionMetaConfig, MintActionMetaConfigCpiWrite, }; -use super::SystemAccountInfos; - /// Default rent payment epochs (~24 hours) pub const DEFAULT_RENT_PAYMENT: u8 = 16; /// Default lamports for write operations (~3 hours per write) @@ -73,6 +71,21 @@ pub struct CreateMintsParams<'a> { /// Lamports allocated for future write operations. /// Default: 766 (~3 hours per write) pub write_top_up: u32, + /// Offset for assigned_account_index when sharing CPI context with other accounts. + /// When creating mints alongside PDAs, this offset should be set to the number of + /// PDAs already written to the CPI context. + /// Default: 0 (no offset) + pub cpi_context_offset: u8, + /// Index of the output queue in tree accounts. + /// Default: 0 + pub output_queue_index: u8, + /// Index of the address merkle tree in tree accounts. + /// Default: 1 + pub address_tree_index: u8, + /// Index of the state merkle tree in tree accounts. + /// Required for decompress operations (discriminator validation). + /// Default: 2 + pub state_tree_index: u8, } impl<'a> CreateMintsParams<'a> { @@ -85,6 +98,10 @@ impl<'a> CreateMintsParams<'a> { proof, rent_payment: DEFAULT_RENT_PAYMENT, write_top_up: DEFAULT_WRITE_TOP_UP, + cpi_context_offset: 0, + output_queue_index: 0, + address_tree_index: 1, + state_tree_index: 2, } } @@ -97,6 +114,34 @@ impl<'a> CreateMintsParams<'a> { self.write_top_up = write_top_up; self } + + /// Set offset for assigned_account_index when sharing CPI context. + /// + /// Use this when creating mints alongside PDAs. The offset should be + /// the number of accounts already written to the CPI context. + pub fn with_cpi_context_offset(mut self, offset: u8) -> Self { + self.cpi_context_offset = offset; + self + } + + /// Set the output queue index in tree accounts. + pub fn with_output_queue_index(mut self, index: u8) -> Self { + self.output_queue_index = index; + self + } + + /// Set the address merkle tree index in tree accounts. + pub fn with_address_tree_index(mut self, index: u8) -> Self { + self.address_tree_index = index; + self + } + + /// Set the state merkle tree index in tree accounts. + /// Required for decompress operations (discriminator validation). + pub fn with_state_tree_index(mut self, index: u8) -> Self { + self.state_tree_index = index; + self + } } /// CPI struct for on-chain programs to create multiple mints. @@ -115,7 +160,6 @@ impl<'a> CreateMintsParams<'a> { /// payer: payer.clone(), /// address_tree: address_tree.clone(), /// output_queue: output_queue.clone(), -/// state_merkle_tree: state_tree.clone(), /// compressible_config: config.clone(), /// mints: vec![mint_pda1.clone(), mint_pda2.clone()], /// rent_sponsor: rent_sponsor.clone(), @@ -126,19 +170,19 @@ impl<'a> CreateMintsParams<'a> { /// ``` pub struct CreateMintsCpi<'a, 'info> { /// Mint seed accounts (signers) - one per mint - pub mint_seed_accounts: &'info [AccountInfo<'info>], + pub mint_seed_accounts: &'a [AccountInfo<'info>], /// Fee payer (also used as authority) pub payer: AccountInfo<'info>, /// Address tree for new mint addresses pub address_tree: AccountInfo<'info>, /// Output queue for compressed accounts pub output_queue: AccountInfo<'info>, - /// State merkle tree (for decompress of earlier mints) + /// State merkle tree (required for decompress discriminator validation) pub state_merkle_tree: AccountInfo<'info>, /// CompressibleConfig account pub compressible_config: AccountInfo<'info>, /// Mint PDA accounts (writable) - one per mint - pub mints: &'info [AccountInfo<'info>], + pub mints: &'a [AccountInfo<'info>], /// Rent sponsor PDA pub rent_sponsor: AccountInfo<'info>, /// Standard Light Protocol system accounts @@ -166,29 +210,26 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { } /// Execute all CPIs to create and decompress all mints. + /// + /// Signer seeds are extracted from `SingleMintParams::mint_signer_seeds` and + /// `SingleMintParams::authority_seeds` for each CPI call (0, 1, or 2 seeds per call). pub fn invoke(self) -> Result<(), ProgramError> { - self.invoke_impl(None) - } - - /// Execute all CPIs to create and decompress all mints with PDA signing. - pub fn invoke_signed(self, signer_seeds: &[&[&[u8]]]) -> Result<(), ProgramError> { - self.invoke_impl(Some(signer_seeds)) - } - - fn invoke_impl(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { self.validate()?; let n = self.params.mints.len(); - if n == 1 { - self.invoke_single_mint(signer_seeds) + // Use single mint path only when: + // - N=1 AND + // - No CPI context offset (no PDAs were written to CPI context first) + if n == 1 && self.params.cpi_context_offset == 0 { + self.invoke_single_mint() } else { - self.invoke_multiple_mints(signer_seeds) + self.invoke_multiple_mints() } } /// Handle the single mint case: create + decompress in one CPI. #[inline(never)] - fn invoke_single_mint(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { + fn invoke_single_mint(self) -> Result<(), ProgramError> { let mint_params = &self.params.mints[0]; let mint_data = build_mint_instruction_data(mint_params, self.mint_seed_accounts[0].key); @@ -200,7 +241,7 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { let instruction_data = MintActionCompressedInstructionData::new_mint( mint_params.address_merkle_tree_root_index, - self.params.proof.clone(), + self.params.proof, mint_data, ) .with_decompress_mint(decompress_action); @@ -219,12 +260,12 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { ); meta_config.input_queue = Some(*self.output_queue.key); - self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + self.invoke_mint_action(instruction_data, meta_config, 0) } /// Handle the multiple mints case: N-1 writes + 1 execute + N-1 decompress. #[inline(never)] - fn invoke_multiple_mints(self, signer_seeds: Option<&[&[&[u8]]]>) -> Result<(), ProgramError> { + fn invoke_multiple_mints(self) -> Result<(), ProgramError> { let n = self.params.mints.len(); // Get base leaf index before any CPIs modify the queue @@ -237,37 +278,39 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { // Write mints 0..N-2 to CPI context for i in 0..(n - 1) { - self.invoke_cpi_write(i, signer_seeds)?; + self.invoke_cpi_write(i)?; } // Execute: create last mint + decompress it - self.invoke_execute(n - 1, &decompress_action, signer_seeds)?; + self.invoke_execute(n - 1, &decompress_action)?; // Decompress remaining mints (0..N-2) for i in 0..(n - 1) { - self.invoke_decompress(i, base_leaf_index, &decompress_action, signer_seeds)?; + self.invoke_decompress(i, base_leaf_index, &decompress_action)?; } Ok(()) } /// Invoke a CPI write instruction for a single mint. + /// Extracts signer seeds from mint params (0, 1, or 2 seeds). #[inline(never)] - fn invoke_cpi_write( - &self, - index: usize, - signer_seeds: Option<&[&[&[u8]]]>, - ) -> Result<(), ProgramError> { + fn invoke_cpi_write(&self, index: usize) -> Result<(), ProgramError> { let mint_params = &self.params.mints[index]; + let offset = self.params.cpi_context_offset; + // When sharing CPI context with PDAs: + // - first_set_context: only true for index 0 AND offset 0 (first write to context) + // - set_context: true if appending to existing context (index > 0 or offset > 0) + // - assigned_account_index: offset + index (to not collide with PDA indices) let cpi_context = CpiContext { - set_context: index > 0, - first_set_context: index == 0, - in_tree_index: 1, - in_queue_index: 0, - out_queue_index: 0, + set_context: index > 0 || offset > 0, + first_set_context: index == 0 && offset == 0, + in_tree_index: self.params.address_tree_index, + in_queue_index: self.params.output_queue_index, + out_queue_index: self.params.output_queue_index, token_out_queue_index: 0, - assigned_account_index: index as u8, + assigned_account_index: offset + index as u8, read_only_address_trees: [0; 4], address_tree_pubkey: self.address_tree.key.to_bytes(), }; @@ -314,31 +357,39 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { data: ix_data, }; - if let Some(seeds) = signer_seeds { - solana_cpi::invoke_signed(&instruction, &account_infos, seeds) - } else { - invoke(&instruction, &account_infos) + // Build signer seeds - pack present seeds at start of array + let mut seeds: [&[&[u8]]; 2] = [&[], &[]]; + let mut num_signers = 0; + if let Some(s) = mint_params.mint_signer_seeds { + seeds[num_signers] = s; + num_signers += 1; } + if let Some(s) = mint_params.authority_seeds { + seeds[num_signers] = s; + num_signers += 1; + } + solana_cpi::invoke_signed(&instruction, &account_infos, &seeds[..num_signers]) } /// Invoke the execute instruction (create last mint + decompress). + /// Extracts signer seeds from mint params (0, 1, or 2 seeds). #[inline(never)] fn invoke_execute( &self, last_idx: usize, decompress_action: &DecompressMintAction, - signer_seeds: Option<&[&[&[u8]]]>, ) -> Result<(), ProgramError> { let mint_params = &self.params.mints[last_idx]; + let offset = self.params.cpi_context_offset; let execute_cpi_context = CpiContext { set_context: false, first_set_context: false, - in_tree_index: 1, - in_queue_index: 1, - out_queue_index: 0, + in_tree_index: self.params.address_tree_index, + in_queue_index: self.params.address_tree_index, // CPI context queue index + out_queue_index: self.params.output_queue_index, token_out_queue_index: 0, - assigned_account_index: last_idx as u8, + assigned_account_index: offset + last_idx as u8, read_only_address_trees: [0; 4], address_tree_pubkey: self.address_tree.key.to_bytes(), }; @@ -348,11 +399,11 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { let instruction_data = MintActionCompressedInstructionData::new_mint( mint_params.address_merkle_tree_root_index, - self.params.proof.clone(), + self.params.proof, mint_data, ) .with_cpi_context(execute_cpi_context) - .with_decompress_mint(decompress_action.clone()); + .with_decompress_mint(*decompress_action); let mut meta_config = MintActionMetaConfig::new_create_mint( *self.payer.key, @@ -369,17 +420,17 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { meta_config.cpi_context = Some(*self.cpi_context_account.key); meta_config.input_queue = Some(*self.output_queue.key); - self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + self.invoke_mint_action(instruction_data, meta_config, last_idx) } /// Invoke decompress for a single mint. + /// Extracts signer seeds from mint params (0, 1, or 2 seeds). #[inline(never)] fn invoke_decompress( &self, index: usize, base_leaf_index: u32, decompress_action: &DecompressMintAction, - signer_seeds: Option<&[&[&[u8]]]>, ) -> Result<(), ProgramError> { let mint_params = &self.params.mints[index]; @@ -392,18 +443,19 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { root_index: 0, max_top_up: 0, create_mint: None, - actions: vec![Action::DecompressMint(decompress_action.clone())], + actions: vec![Action::DecompressMint(*decompress_action)], proof: None, cpi_context: None, mint: Some(mint_data), }; + // For prove_by_index, the tree_pubkey must be state_merkle_tree for discriminator validation let meta_config = MintActionMetaConfig::new( *self.payer.key, *self.payer.key, - *self.state_merkle_tree.key, - *self.output_queue.key, - *self.output_queue.key, + *self.state_merkle_tree.key, // tree_pubkey - state merkle tree for discriminator check + *self.output_queue.key, // input_queue + *self.output_queue.key, // output_queue ) .with_compressible_mint( *self.mints[index].key, @@ -411,16 +463,17 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { *self.rent_sponsor.key, ); - self.invoke_mint_action(instruction_data, meta_config, signer_seeds) + self.invoke_mint_action(instruction_data, meta_config, index) } /// Invoke a mint action instruction. + /// Extracts signer seeds from mint params at the given index (0, 1, or 2 seeds). #[inline(never)] fn invoke_mint_action( &self, instruction_data: MintActionCompressedInstructionData, meta_config: MintActionMetaConfig, - signer_seeds: Option<&[&[&[u8]]]>, + mint_index: usize, ) -> Result<(), ProgramError> { let account_metas = meta_config.to_account_metas(); let ix_data = instruction_data @@ -448,10 +501,10 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { // CPI context, queues, trees account_infos.push(self.cpi_context_account.clone()); account_infos.push(self.output_queue.clone()); + account_infos.push(self.state_merkle_tree.clone()); account_infos.push(self.address_tree.clone()); account_infos.push(self.compressible_config.clone()); account_infos.push(self.rent_sponsor.clone()); - account_infos.push(self.state_merkle_tree.clone()); // Add all mint PDAs for mint in self.mints { @@ -464,11 +517,19 @@ impl<'a, 'info> CreateMintsCpi<'a, 'info> { data: ix_data, }; - if let Some(seeds) = signer_seeds { - solana_cpi::invoke_signed(&instruction, &account_infos, seeds) - } else { - invoke(&instruction, &account_infos) + // Build signer seeds - pack present seeds at start of array + let mint_params = &self.params.mints[mint_index]; + let mut seeds: [&[&[u8]]; 2] = [&[], &[]]; + let mut num_signers = 0; + if let Some(s) = mint_params.mint_signer_seeds { + seeds[num_signers] = s; + num_signers += 1; + } + if let Some(s) = mint_params.authority_seeds { + seeds[num_signers] = s; + num_signers += 1; } + solana_cpi::invoke_signed(&instruction, &account_infos, &seeds[..num_signers]) } } @@ -519,10 +580,10 @@ fn get_base_leaf_index(output_queue: &AccountInfo) -> Result /// - `[N+1..N+6]`: system PDAs (cpi_authority, registered_program, compression_authority, compression_program, system_program) /// - `[N+6]`: cpi_context_account (writable) /// - `[N+7]`: output_queue (writable) -/// - `[N+8]`: address_tree (writable) -/// - `[N+9]`: compressible_config -/// - `[N+10]`: rent_sponsor (writable) -/// - `[N+11]`: state_merkle_tree (writable) +/// - `[N+8]`: state_merkle_tree (writable) +/// - `[N+9]`: address_tree (writable) +/// - `[N+10]`: compressible_config +/// - `[N+11]`: rent_sponsor (writable) /// - `[N+12..2N+12]`: mint_pdas (writable) /// - `[2N+12]`: compressed_token_program (for CPI) pub fn create_mints<'a, 'info>( @@ -543,10 +604,10 @@ pub fn create_mints<'a, 'info>( let system_program_idx = n + 5; let cpi_context_idx = n + 6; let output_queue_idx = n + 7; - let address_tree_idx = n + 8; - let compressible_config_idx = n + 9; - let rent_sponsor_idx = n + 10; - let state_merkle_tree_idx = n + 11; + let state_merkle_tree_idx = n + 8; + let address_tree_idx = n + 9; + let compressible_config_idx = n + 10; + let rent_sponsor_idx = n + 11; let mint_pdas_start = n + 12; // Build named struct from accounts slice 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 cc16540a9b..5c7185f4fb 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 @@ -116,3 +116,191 @@ pub struct CreatePdasAndMintAuto<'info> { } pub const VAULT_SEED: &[u8] = b"vault"; + +// ============================================================================= +// Two Mints Test +// ============================================================================= + +pub const MINT_SIGNER_A_SEED: &[u8] = b"mint_signer_a"; +pub const MINT_SIGNER_B_SEED: &[u8] = b"mint_signer_b"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateTwoMintsParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_a_bump: u8, + pub mint_signer_b_bump: u8, +} + +/// Test instruction with 2 #[light_mint] fields to verify multi-mint support. +#[derive(Accounts, RentFree)] +#[instruction(params: CreateTwoMintsParams)] +pub struct CreateTwoMints<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority for mint A + #[account( + seeds = [MINT_SIGNER_A_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_a: UncheckedAccount<'info>, + + /// CHECK: PDA derived from authority for mint B + #[account( + seeds = [MINT_SIGNER_B_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_b: UncheckedAccount<'info>, + + /// CHECK: Initialized by mint_action - first mint + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_a, + authority = fee_payer, + decimals = 6, + mint_seeds = &[MINT_SIGNER_A_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_a_bump]] + )] + pub cmint_a: UncheckedAccount<'info>, + + /// CHECK: Initialized by mint_action - second mint + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_b, + authority = fee_payer, + decimals = 9, + mint_seeds = &[MINT_SIGNER_B_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_b_bump]] + )] + pub cmint_b: UncheckedAccount<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + pub ctoken_compressible_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut)] + 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>, +} + +// ============================================================================= +// Four Mints Test +// ============================================================================= + +pub const MINT_SIGNER_C_SEED: &[u8] = b"mint_signer_c"; +pub const MINT_SIGNER_D_SEED: &[u8] = b"mint_signer_d"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct CreateFourMintsParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_a_bump: u8, + pub mint_signer_b_bump: u8, + pub mint_signer_c_bump: u8, + pub mint_signer_d_bump: u8, +} + +/// Test instruction with 4 #[light_mint] fields to verify multi-mint support. +#[derive(Accounts, RentFree)] +#[instruction(params: CreateFourMintsParams)] +pub struct CreateFourMints<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority for mint A + #[account( + seeds = [MINT_SIGNER_A_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_a: UncheckedAccount<'info>, + + /// CHECK: PDA derived from authority for mint B + #[account( + seeds = [MINT_SIGNER_B_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_b: UncheckedAccount<'info>, + + /// CHECK: PDA derived from authority for mint C + #[account( + seeds = [MINT_SIGNER_C_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_c: UncheckedAccount<'info>, + + /// CHECK: PDA derived from authority for mint D + #[account( + seeds = [MINT_SIGNER_D_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer_d: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_a, + authority = fee_payer, + decimals = 6, + mint_seeds = &[MINT_SIGNER_A_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_a_bump]] + )] + pub cmint_a: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_b, + authority = fee_payer, + decimals = 8, + mint_seeds = &[MINT_SIGNER_B_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_b_bump]] + )] + pub cmint_b: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_c, + authority = fee_payer, + decimals = 9, + mint_seeds = &[MINT_SIGNER_C_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_c_bump]] + )] + pub cmint_c: UncheckedAccount<'info>, + + /// CHECK: Initialized by light_mint CPI + #[account(mut)] + #[light_mint( + mint_signer = mint_signer_d, + authority = fee_payer, + decimals = 12, + mint_seeds = &[MINT_SIGNER_D_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_d_bump]] + )] + pub cmint_d: UncheckedAccount<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + pub ctoken_compressible_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut)] + 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>, +} 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 1ebfe30cc3..3da00b3738 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -104,7 +104,10 @@ pub mod csdk_anchor_full_derived_test { D9FunctionCall, D9FunctionCallParams, D9Literal, D9LiteralParams, D9Mixed, D9MixedParams, D9Param, D9ParamBytes, D9ParamBytesParams, D9ParamParams, }, - instruction_accounts::CreatePdasAndMintAuto, + instruction_accounts::{ + CreateFourMints, CreateFourMintsParams, CreatePdasAndMintAuto, CreateTwoMints, + CreateTwoMintsParams, + }, FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; @@ -200,6 +203,30 @@ pub mod csdk_anchor_full_derived_test { crate::processors::process_create_single_record(ctx, params) } + /// Test instruction that creates 2 mints in a single transaction. + /// Tests the multi-mint support in the RentFree macro. + #[allow(unused_variables)] + pub fn create_two_mints<'info>( + ctx: Context<'_, '_, '_, 'info, CreateTwoMints<'info>>, + params: CreateTwoMintsParams, + ) -> Result<()> { + // Both mints are created by the RentFree macro in pre_init + // Nothing to do here - just verify both mints exist + Ok(()) + } + + /// Test instruction that creates 4 mints in a single transaction. + /// Tests the multi-mint support in the RentFree macro scales beyond 2. + #[allow(unused_variables)] + pub fn create_four_mints<'info>( + ctx: Context<'_, '_, '_, 'info, CreateFourMints<'info>>, + params: CreateFourMintsParams, + ) -> Result<()> { + // All 4 mints are created by the RentFree macro in pre_init + // Nothing to do here - just verify all mints exist + Ok(()) + } + /// AMM initialize instruction with all rentfree markers. /// Tests: 2x #[rentfree], 2x #[rentfree_token], 1x #[light_mint], /// CreateTokenAccountCpi.rent_free(), CreateTokenAtaCpi.rent_free(), MintToCpi diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs index 0f59fc79c2..fa7288df1c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_observation_state_test.rs @@ -9,9 +9,7 @@ //! ObservationState has 1 Pubkey field (pool_id) and a nested array of Observation structs, //! testing Pack/Unpack behavior with array fields and nested data structures. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; -use csdk_anchor_full_derived_test::{PackedObservationState, ObservationState, Observation}; +use csdk_anchor_full_derived_test::{Observation, ObservationState, PackedObservationState}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ compressible::{CompressAs, CompressionInfo, Pack}, @@ -19,6 +17,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -107,7 +108,7 @@ fn test_compress_as_preserves_pool_id() { let inner = compressed.into_owned(); assert_eq!(inner.pool_id, pool_id); - assert_eq!(inner.initialized, true); + assert!(inner.initialized); assert_eq!(inner.observation_index, 5); } @@ -157,12 +158,8 @@ fn test_hash_differs_for_different_pool_id() { observation1.pool_id = Pubkey::new_unique(); observation2.pool_id = Pubkey::new_unique(); - let hash1 = observation1 - .hash::() - .expect("hash should succeed"); - let hash2 = observation2 - .hash::() - .expect("hash should succeed"); + let hash1 = observation1.hash::().expect("hash should succeed"); + let hash2 = observation2.hash::().expect("hash should succeed"); assert_ne!( hash1, hash2, @@ -178,12 +175,8 @@ fn test_hash_differs_for_different_initialized() { observation1.initialized = true; observation2.initialized = false; - let hash1 = observation1 - .hash::() - .expect("hash should succeed"); - let hash2 = observation2 - .hash::() - .expect("hash should succeed"); + let hash1 = observation1.hash::().expect("hash should succeed"); + let hash2 = observation2.hash::().expect("hash should succeed"); assert_ne!( hash1, hash2, @@ -199,12 +192,8 @@ fn test_hash_differs_for_different_observation_index() { observation1.observation_index = 1; observation2.observation_index = 2; - let hash1 = observation1 - .hash::() - .expect("hash should succeed"); - let hash2 = observation2 - .hash::() - .expect("hash should succeed"); + let hash1 = observation1.hash::().expect("hash should succeed"); + let hash2 = observation2.hash::().expect("hash should succeed"); assert_ne!( hash1, hash2, @@ -220,12 +209,8 @@ fn test_hash_differs_for_different_observation_data() { observation1.observations[0].block_timestamp = 1000; observation2.observations[0].block_timestamp = 2000; - let hash1 = observation1 - .hash::() - .expect("hash should succeed"); - let hash2 = observation2 - .hash::() - .expect("hash should succeed"); + let hash1 = observation1.hash::().expect("hash should succeed"); + let hash2 = observation2.hash::().expect("hash should succeed"); assert_ne!( hash1, hash2, @@ -359,7 +344,7 @@ fn test_pack_preserves_all_fields() { let mut packed_accounts = PackedAccounts::default(); let packed = observation_state.pack(&mut packed_accounts); - assert_eq!(packed.initialized, true); + assert!(packed.initialized); assert_eq!(packed.observation_index, 42); assert_eq!(packed.observations[0].block_timestamp, 1000); assert_eq!(packed.observations[0].cumulative_token_0_price_x32, 5000); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs index 85603f5690..b0cdbd986b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/amm_pool_state_test.rs @@ -9,8 +9,6 @@ //! PoolState has 10 Pubkey fields and multiple numeric fields, testing //! comprehensive Pack/Unpack behavior with multiple pubkey indices. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{PackedPoolState, PoolState}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -19,6 +17,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -552,7 +553,7 @@ fn test_pack_stores_all_pubkeys_in_packed_accounts() { }; let mut packed_accounts = PackedAccounts::default(); - let packed = pool.pack(&mut packed_accounts); + let _packed = pool.pack(&mut packed_accounts); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 10, "should have 10 pubkeys stored"); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs index 4f17335401..064ffd25de 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_game_session_test.rs @@ -9,8 +9,6 @@ //! GameSession has #[compress_as(start_time = 0, end_time = None, score = 0)] //! which overrides field values during compression. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{GameSession, PackedGameSession}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -19,6 +17,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs index 290f61a948..e0ea23b2bc 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_placeholder_record_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedPlaceholderRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{PackedPlaceholderRecord, PlaceholderRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -153,10 +154,7 @@ fn test_hash_differs_for_different_name() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different name should produce different hash" - ); + assert_ne!(hash1, hash2, "different name should produce different hash"); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs index 54c482c9c0..93eb0bbadd 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/core_user_record_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedUserRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{PackedUserRecord, UserRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -153,10 +154,7 @@ fn test_hash_differs_for_different_name() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different name should produce different hash" - ); + assert_ne!(hash1, hash2, "different name should produce different hash"); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs index d187dddf76..5d71081a7f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_all_field_types_test.rs @@ -14,9 +14,7 @@ //! - Option (end_time, enabled) -> unchanged //! - Regular primitives (counter, flag) -> direct copy -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; -use csdk_anchor_full_derived_test::{PackedAllFieldTypesRecord, AllFieldTypesRecord}; +use csdk_anchor_full_derived_test::{AllFieldTypesRecord, PackedAllFieldTypesRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ compressible::{CompressAs, CompressionInfo, Pack}, @@ -24,6 +22,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -184,7 +185,10 @@ fn test_hash_differs_for_different_pubkey_field() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!(hash1, hash2, "different owner should produce different hash"); + assert_ne!( + hash1, hash2, + "different owner should produce different hash" + ); } #[test] @@ -308,7 +312,10 @@ fn test_hash_differs_for_different_array_field() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!(hash1, hash2, "different hash array should produce different hash"); + assert_ne!( + hash1, hash2, + "different hash array should produce different hash" + ); } #[test] @@ -387,7 +394,10 @@ fn test_hash_differs_for_different_primitive() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!(hash1, hash2, "different counter should produce different hash"); + assert_ne!( + hash1, hash2, + "different counter should produce different hash" + ); } // ============================================================================= @@ -419,7 +429,7 @@ fn test_packed_struct_has_all_types_converted() { assert_eq!(packed.close_authority, Some(close_authority)); assert_eq!(packed.name, "test".to_string()); assert_eq!(packed.counter, 42u64); - assert_eq!(packed.flag, false); + assert!(!packed.flag); } #[test] @@ -455,7 +465,7 @@ fn test_pack_converts_all_pubkey_types() { assert_eq!(packed.close_authority, Some(close_authority)); assert_eq!(packed.name, name); assert_eq!(packed.counter, 100); - assert_eq!(packed.flag, true); + assert!(packed.flag); // Only direct Pubkey fields are stored in packed_accounts (not Option) let stored_pubkeys = packed_accounts.packed_pubkeys(); @@ -492,7 +502,10 @@ fn test_pack_with_option_pubkey_none() { assert_eq!(packed.owner, 0u8); assert_eq!(packed.delegate, 1u8); assert_eq!(packed.authority, 2u8); - assert_eq!(packed.close_authority, None, "Option::None should remain None"); + assert_eq!( + packed.close_authority, None, + "Option::None should remain None" + ); let stored_pubkeys = packed_accounts.packed_pubkeys(); assert_eq!(stored_pubkeys.len(), 3); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs index dc95cd2fa9..d7f1bc9970 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_array_test.rs @@ -10,13 +10,12 @@ //! implementation where Packed = Self. Array fields are directly copied in pack/unpack. //! Therefore, no Pack/Unpack tests are needed. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::ArrayRecord; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo}, -}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; // ============================================================================= // Factory Implementation diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs index f94f9cfc3c..7019ee783c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_multi_pubkey_test.rs @@ -6,9 +6,7 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedMultiPubkeyRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; -use csdk_anchor_full_derived_test::{PackedMultiPubkeyRecord, MultiPubkeyRecord}; +use csdk_anchor_full_derived_test::{MultiPubkeyRecord, PackedMultiPubkeyRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ compressible::{CompressAs, CompressionInfo, Pack}, @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs index 009d022259..6547c3e76a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_no_pubkey_test.rs @@ -10,13 +10,12 @@ //! implementation where Packed = Self. Therefore, no Pack/Unpack tests are needed - the //! struct is packed as-is without transformation. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::NoPubkeyRecord; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo}, -}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; // ============================================================================= // Factory Implementation @@ -141,10 +140,7 @@ fn test_hash_differs_for_different_flag() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different flag should produce different hash" - ); + assert_ne!(hash1, hash2, "different flag should produce different hash"); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs index 28e6d3f550..9f28d3adbf 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_non_copy_test.rs @@ -10,13 +10,12 @@ //! implementation where Packed = Self. String fields use the clone() code path in pack/unpack. //! Therefore, no Pack/Unpack tests are needed. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::NonCopyRecord; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo}, -}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; // ============================================================================= // Factory Implementation @@ -141,10 +140,7 @@ fn test_hash_differs_for_different_name() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different name should produce different hash" - ); + assert_ne!(hash1, hash2, "different name should produce different hash"); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs index 35db5c59e2..eec38f168d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_primitive_test.rs @@ -10,13 +10,12 @@ //! implementation where Packed = Self. Option types remain unchanged in the packed //! struct (not converted to Option). Therefore, no Pack/Unpack tests are needed. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::OptionPrimitiveRecord; use light_hasher::{DataHasher, Sha256}; -use light_sdk::{ - compressible::{CompressAs, CompressionInfo}, -}; +use light_sdk::compressible::{CompressAs, CompressionInfo}; + +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; // ============================================================================= // Factory Implementation diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs index dc78daca4e..86fb31a17f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_option_pubkey_test.rs @@ -10,8 +10,6 @@ //! Only direct Pubkey fields (like `owner: Pubkey`) are converted to u8 indices. //! Option fields remain as Option in the packed struct. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::OptionPubkeyRecord; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -20,6 +18,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -324,7 +325,11 @@ fn test_pack_option_pubkey_none_stays_none() { assert_eq!(packed.owner, 0u8); // Option fields stay as Option - NOT converted to Option assert_eq!(packed.delegate, None, "Option::None stays None"); - assert_eq!(packed.close_authority, Some(close_authority), "Option::Some stays Some"); + assert_eq!( + packed.close_authority, + Some(close_authority), + "Option::Some stays Some" + ); // Only the direct Pubkey field (owner) is stored in packed_accounts let stored_pubkeys = packed_accounts.packed_pubkeys(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs index 1371287936..468a184aff 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d1_single_pubkey_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedSinglePubkeyRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{PackedSinglePubkeyRecord, SinglePubkeyRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs index 105445729b..ebf4f35d15 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_all_compress_as_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedAllCompressAsRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{AllCompressAsRecord, PackedAllCompressAsRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -238,10 +239,7 @@ fn test_hash_differs_for_different_flag() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different flag should produce different hash" - ); + assert_ne!(hash1, hash2, "different flag should produce different hash"); } #[test] @@ -273,10 +271,7 @@ fn test_hash_differs_for_different_time() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different time should produce different hash" - ); + assert_ne!(hash1, hash2, "different time should produce different hash"); } #[test] @@ -335,7 +330,7 @@ fn test_packed_struct_has_u8_owner() { assert_eq!(packed.cached, 44u64); assert_eq!(packed.end, None); assert_eq!(packed.counter, 100u64); - assert_eq!(packed.flag, true); + assert!(packed.flag); } #[test] @@ -361,7 +356,7 @@ fn test_pack_converts_pubkey_to_index() { assert_eq!(packed.cached, 70); assert_eq!(packed.end, Some(80)); assert_eq!(packed.counter, 100); - assert_eq!(packed.flag, true); + assert!(packed.flag); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs index 8295f1aeb0..df1d5c07a1 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_multiple_compress_as_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedMultipleCompressAsRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{MultipleCompressAsRecord, PackedMultipleCompressAsRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -62,9 +63,9 @@ fn test_compress_as_overrides_all_marked_fields() { let record = MultipleCompressAsRecord { compression_info: Some(CompressionInfo::default()), owner, - start: 888, // Original value - score: 777, // Original value - cached: 666, // Original value + start: 888, // Original value + score: 777, // Original value + cached: 666, // Original value counter, }; diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs index 3d6d52b7cb..dbe1f6cf89 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_no_compress_as_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedNoCompressAsRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{NoCompressAsRecord, PackedNoCompressAsRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -164,10 +165,7 @@ fn test_hash_differs_for_different_flag() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_ne!( - hash1, hash2, - "different flag should produce different hash" - ); + assert_ne!(hash1, hash2, "different flag should produce different hash"); } #[test] @@ -210,7 +208,7 @@ fn test_packed_struct_has_u8_owner() { assert_eq!(packed.owner, 0u8); assert_eq!(packed.counter, 42u64); - assert_eq!(packed.flag, true); + assert!(packed.flag); } #[test] @@ -228,7 +226,7 @@ fn test_pack_converts_pubkey_to_index() { assert_eq!(packed.owner, 0u8); assert_eq!(packed.counter, 100); - assert_eq!(packed.flag, true); + assert!(packed.flag); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs index 480a2834a1..84e0ce5131 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_option_none_compress_as_test.rs @@ -6,11 +6,7 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedOptionNoneCompressAsRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; -use csdk_anchor_full_derived_test::{ - OptionNoneCompressAsRecord, PackedOptionNoneCompressAsRecord, -}; +use csdk_anchor_full_derived_test::{OptionNoneCompressAsRecord, PackedOptionNoneCompressAsRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ compressible::{CompressAs, CompressionInfo, Pack}, @@ -18,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -71,7 +70,10 @@ fn test_compress_as_overrides_end_time_to_none() { let compressed = record.compress_as(); // Per #[compress_as(end_time = None)], end_time should be None in compressed form - assert_eq!(compressed.end_time, None, "end_time should be None after compress_as"); + assert_eq!( + compressed.end_time, None, + "end_time should be None after compress_as" + ); // Other fields should be preserved assert_eq!(compressed.owner, owner); assert_eq!(compressed.start_time, start_time); @@ -139,7 +141,10 @@ fn test_compress_as_with_various_end_time_values() { let compressed = record.compress_as(); // All should compress end_time to None - assert_eq!(compressed.end_time, None, "end_time should always be None after compress_as"); + assert_eq!( + compressed.end_time, None, + "end_time should always be None after compress_as" + ); } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs index 9b28833e42..4a5246d45b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d2_single_compress_as_test.rs @@ -6,8 +6,6 @@ //! - Compressible -> HasCompressionInfo + CompressAs + Size + CompressedInitSpace //! - CompressiblePack -> Pack + Unpack + PackedSingleCompressAsRecord -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{PackedSingleCompressAsRecord, SingleCompressAsRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -16,6 +14,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -101,7 +102,10 @@ fn test_compress_as_with_multiple_cached_values() { let compressed = record.compress_as(); // All should compress cached to 0 - assert_eq!(compressed.cached, 0, "cached should always be 0 after compress_as"); + assert_eq!( + compressed.cached, 0, + "cached should always be 0 after compress_as" + ); } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs index 7c1be18865..cbc11bfc64 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_all_composition_test.rs @@ -10,8 +10,6 @@ //! #[compress_as(cached_time = 0, end_time = None)] to override field values. //! This tests full Pack/Unpack behavior with compress_as attribute overrides. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{AllCompositionRecord, PackedAllCompositionRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -20,6 +18,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -207,8 +208,8 @@ fn test_compress_as_preserves_non_override_fields() { assert_eq!(compressed.counter_1, 11); assert_eq!(compressed.counter_2, 22); assert_eq!(compressed.counter_3, 33); - assert_eq!(compressed.flag_1, false); - assert_eq!(compressed.flag_2, true); + assert!(!compressed.flag_1); + assert!(compressed.flag_2); assert_eq!(compressed.score, Some(99)); } @@ -271,7 +272,7 @@ fn test_hash_differs_for_different_counter_3() { let delegate = Pubkey::new_unique(); let authority = Pubkey::new_unique(); - let mut record1 = AllCompositionRecord { + let record1 = AllCompositionRecord { compression_info: None, owner, delegate, @@ -369,7 +370,7 @@ fn test_pack_converts_all_pubkeys_to_indices() { assert_eq!(packed.owner, 0u8); // First pubkey assert_eq!(packed.delegate, 1u8); // Second pubkey assert_eq!(packed.authority, 2u8); // Third pubkey - // Option is NOT converted to Option - it stays as Option + // Option is NOT converted to Option - it stays as Option assert_eq!(packed.close_authority, Some(close_authority)); } @@ -422,8 +423,8 @@ fn test_compress_as_then_pack_applies_overrides() { name: "test".to_string(), hash: [0u8; 32], start_time: 100, - cached_time: 999, // Should become 0 after compress_as - end_time: Some(999), // Should become None after compress_as + cached_time: 999, // Should become 0 after compress_as + end_time: Some(999), // Should become None after compress_as counter_1: 1, counter_2: 2, counter_3: 3, @@ -438,7 +439,10 @@ fn test_compress_as_then_pack_applies_overrides() { let packed = compressed.pack(&mut packed_accounts); // compress_as overrides ARE applied when chained - assert_eq!(packed.cached_time, 0, "compress_as().pack() applies cached_time = 0 override"); + assert_eq!( + packed.cached_time, 0, + "compress_as().pack() applies cached_time = 0 override" + ); assert!( packed.end_time.is_none(), "compress_as().pack() applies end_time = None override" @@ -508,7 +512,10 @@ fn test_pack_reuses_duplicate_pubkeys_for_direct_fields() { let packed = record1.pack(&mut packed_accounts); // owner and delegate are the same pubkey, should get the same index - assert_eq!(packed.owner, packed.delegate, "same pubkey should get same index"); + assert_eq!( + packed.owner, packed.delegate, + "same pubkey should get same index" + ); // Option is NOT converted to Option - it stays as Option assert_eq!(packed.close_authority, Some(shared_pubkey)); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs index 6f8b9f13b3..a2a023e49d 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_info_last_test.rs @@ -9,8 +9,6 @@ //! InfoLastRecord has 1 Pubkey field (owner) and demonstrates that //! compression_info can be placed in non-first position (ordering test). -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::{InfoLastRecord, PackedInfoLastRecord}; use light_hasher::{DataHasher, Sha256}; use light_sdk::{ @@ -19,6 +17,9 @@ use light_sdk::{ }; use solana_pubkey::Pubkey; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -88,7 +89,7 @@ fn test_compress_as_preserves_all_field_types() { // Verify all fields are preserved despite compression_info being last assert_eq!(compressed.owner, owner); assert_eq!(compressed.counter, 42); - assert_eq!(compressed.flag, true); + assert!(compressed.flag); assert!(compressed.compression_info.is_none()); } @@ -189,7 +190,7 @@ fn test_packed_struct_has_u8_owner() { assert_eq!(packed.owner, 0u8); assert_eq!(packed.counter, 42u64); - assert_eq!(packed.flag, true); + assert!(packed.flag); } #[test] @@ -209,7 +210,7 @@ fn test_pack_converts_pubkey_to_index() { // and packed.owner should be the index (0 for first pubkey) assert_eq!(packed.owner, 0u8); assert_eq!(packed.counter, 100); - assert_eq!(packed.flag, false); + assert!(!packed.flag); let mut packed_accounts = PackedAccounts::default(); packed_accounts.insert_or_get(Pubkey::new_unique()); diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs index 49117be7ca..144b2c8bfa 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_large_test.rs @@ -9,12 +9,13 @@ //! This exercises the SHA256 hash mode for large structs. //! Pack/Unpack traits are NOT generated because there are no Pubkey fields. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::LargeRecord; use light_hasher::{DataHasher, Sha256}; use light_sdk::compressible::{CompressAs, CompressionInfo}; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= @@ -198,7 +199,10 @@ fn test_hash_same_for_same_large_struct() { let hash1 = record1.hash::().expect("hash should succeed"); let hash2 = record2.hash::().expect("hash should succeed"); - assert_eq!(hash1, hash2, "identical large records should produce same hash"); + assert_eq!( + hash1, hash2, + "identical large records should produce same hash" + ); } #[test] diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs index bb074b49bf..e07cb324fd 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/d4_minimal_test.rs @@ -7,12 +7,13 @@ //! //! MinimalRecord has NO Pubkey fields, so Pack/Unpack traits are NOT generated. -use super::shared::CompressibleTestFactory; -use crate::generate_trait_tests; use csdk_anchor_full_derived_test::MinimalRecord; use light_hasher::{DataHasher, Sha256}; use light_sdk::compressible::{CompressAs, CompressionInfo}; +use super::shared::CompressibleTestFactory; +use crate::generate_trait_tests; + // ============================================================================= // Factory Implementation // ============================================================================= diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs index 55721419a4..3a5cdf6755 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/account_macros/shared.rs @@ -75,8 +75,9 @@ pub fn assert_discriminator_slice_matches_array() { // ============================================================================= /// Verifies compression_info() returns a valid reference when Some. -pub fn assert_compression_info_returns_reference() -{ +pub fn assert_compression_info_returns_reference< + T: HasCompressionInfo + CompressibleTestFactory, +>() { let record = T::with_compression_info(); let info = record.compression_info(); // Just verify we can access it - the default values @@ -135,8 +136,7 @@ pub fn assert_set_compression_info_none_works() -{ +pub fn assert_compression_info_panics_when_none() { let record = T::without_compression_info(); // This should panic since compression_info is None let _ = record.compression_info(); @@ -279,9 +279,10 @@ pub fn assert_hash_includes_compression_info { mod discriminator_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_discriminator_is_8_bytes() { assert_discriminator_is_8_bytes::<$type>(); @@ -304,9 +305,10 @@ macro_rules! generate_trait_tests { } mod has_compression_info_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_compression_info_returns_reference() { assert_compression_info_returns_reference::<$type>(); @@ -341,9 +343,10 @@ macro_rules! generate_trait_tests { } mod compress_as_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_compress_as_sets_compression_info_to_none() { assert_compress_as_sets_compression_info_to_none::<$type>(); @@ -356,9 +359,10 @@ macro_rules! generate_trait_tests { } mod size_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_size_returns_positive() { assert_size_returns_positive::<$type>(); @@ -371,9 +375,10 @@ macro_rules! generate_trait_tests { } mod compressed_init_space_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_compressed_init_space_includes_discriminator() { assert_compressed_init_space_includes_discriminator::<$type>(); @@ -381,9 +386,10 @@ macro_rules! generate_trait_tests { } mod data_hasher_tests { - use super::*; use $crate::shared::*; + use super::*; + #[test] fn test_hash_produces_32_bytes() { assert_hash_produces_32_bytes::<$type>(); 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 2d26f9d62b..b5867c044c 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 @@ -275,7 +275,10 @@ async fn test_create_pdas_and_mint_auto() { initial_game_session.game_type, "Auto Game With Mint", "game_type should match" ); - assert_eq!(initial_game_session.end_time, None, "end_time should be None"); + assert_eq!( + initial_game_session.end_time, None, + "end_time should be None" + ); assert_eq!(initial_game_session.score, 0, "score should be 0"); // Store initial start_time for comparison after decompress @@ -449,13 +452,13 @@ async fn test_create_pdas_and_mint_auto() { // Build expected struct with compress_as overrides applied: // #[compress_as(start_time = 0, end_time = None, score = 0)] let expected_game_session = GameSession { - compression_info, // Runtime-specific, extracted from actual - session_id, // 222 - preserved - player: payer.pubkey(), // Preserved + compression_info, // Runtime-specific, extracted from actual + session_id, // 222 - preserved + player: payer.pubkey(), // Preserved game_type: "Auto Game With Mint".to_string(), // Preserved - start_time: 0, // compress_as override (was Clock timestamp) - end_time: None, // compress_as override - score: 0, // compress_as override + start_time: 0, // compress_as override (was Clock timestamp) + end_time: None, // compress_as override + score: 0, // compress_as override }; // Single assert comparing full struct @@ -464,3 +467,382 @@ async fn test_create_pdas_and_mint_auto() { "GameSession should match expected after decompress with compress_as overrides" ); } + +/// Test creating 2 mints in a single instruction. +/// Verifies multi-mint support in the RentFree macro. +#[tokio::test] +async fn test_create_two_mints() { + use csdk_anchor_full_derived_test::instruction_accounts::{ + CreateTwoMintsParams, MINT_SIGNER_A_SEED, MINT_SIGNER_B_SEED, + }; + use light_token_sdk::token::{ + find_mint_address as find_cmint_address, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, + }; + + let program_id = csdk_anchor_full_derived_test::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("csdk_anchor_full_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 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 PDAs for both mint signers + let (mint_signer_a_pda, mint_signer_a_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_A_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_b_pda, mint_signer_b_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_B_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDAs + let (cmint_a_pda, _) = find_cmint_address(&mint_signer_a_pda); + let (cmint_b_pda, _) = find_cmint_address(&mint_signer_b_pda); + + // Get proof for both mints + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_a_pda), + CreateAccountsProofInput::mint(mint_signer_b_pda), + ], + ) + .await + .unwrap(); + + // Debug: Check proof contents + println!( + "proof_result.create_accounts_proof.proof.0.is_some() = {:?}", + proof_result.create_accounts_proof.proof.0.is_some() + ); + println!( + "proof_result.remaining_accounts.len() = {:?}", + proof_result.remaining_accounts.len() + ); + + let accounts = csdk_anchor_full_derived_test::accounts::CreateTwoMints { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer_a: mint_signer_a_pda, + mint_signer_b: mint_signer_b_pda, + cmint_a: cmint_a_pda, + cmint_b: cmint_b_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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::CreateTwoMints { + params: CreateTwoMintsParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_a_bump, + mint_signer_b_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("CreateTwoMints should succeed"); + + // Verify both mints exist on-chain + let cmint_a_account = rpc + .get_account(cmint_a_pda) + .await + .unwrap() + .expect("Mint A should exist on-chain"); + let cmint_b_account = rpc + .get_account(cmint_b_pda) + .await + .unwrap() + .expect("Mint B should exist on-chain"); + + // Parse and verify mint data + use light_token_interface::state::Mint; + let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_a_account.data[..]) + .expect("Failed to deserialize Mint A"); + let mint_b: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_b_account.data[..]) + .expect("Failed to deserialize Mint B"); + + // Verify decimals match what was specified in #[light_mint] + assert_eq!(mint_a.base.decimals, 6, "Mint A should have 6 decimals"); + assert_eq!(mint_b.base.decimals, 9, "Mint B should have 9 decimals"); + + // Verify mint authorities + assert_eq!( + mint_a.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint A authority should be fee_payer" + ); + assert_eq!( + mint_b.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint B authority should be fee_payer" + ); + + // Verify compressed addresses registered + let address_tree_pubkey = rpc.get_address_tree_v2().tree; + + let mint_a_compressed_address = + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( + &mint_signer_a_pda, + &address_tree_pubkey, + ); + let compressed_mint_a = rpc + .get_compressed_account(mint_a_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!( + compressed_mint_a.address.unwrap(), + mint_a_compressed_address, + "Mint A compressed address should be registered" + ); + + let mint_b_compressed_address = + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( + &mint_signer_b_pda, + &address_tree_pubkey, + ); + let compressed_mint_b = rpc + .get_compressed_account(mint_b_compressed_address, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!( + compressed_mint_b.address.unwrap(), + mint_b_compressed_address, + "Mint B compressed address should be registered" + ); + + // Verify both compressed mint accounts have empty data (decompressed to on-chain) + assert!( + compressed_mint_a.data.as_ref().unwrap().data.is_empty(), + "Mint A compressed data should be empty (decompressed)" + ); + assert!( + compressed_mint_b.data.as_ref().unwrap().data.is_empty(), + "Mint B compressed data should be empty (decompressed)" + ); +} + +/// Test creating 4 mints in a single instruction. +/// Verifies multi-mint support in the RentFree macro scales beyond 2. +#[tokio::test] +async fn test_create_four_mints() { + use csdk_anchor_full_derived_test::instruction_accounts::{ + CreateFourMintsParams, MINT_SIGNER_A_SEED, MINT_SIGNER_B_SEED, MINT_SIGNER_C_SEED, + MINT_SIGNER_D_SEED, + }; + use light_token_sdk::token::{ + find_mint_address as find_cmint_address, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, + }; + + let program_id = csdk_anchor_full_derived_test::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("csdk_anchor_full_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 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 PDAs for all 4 mint signers + let (mint_signer_a_pda, mint_signer_a_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_A_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_b_pda, mint_signer_b_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_B_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_c_pda, mint_signer_c_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_C_SEED, authority.pubkey().as_ref()], + &program_id, + ); + let (mint_signer_d_pda, mint_signer_d_bump) = Pubkey::find_program_address( + &[MINT_SIGNER_D_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDAs + let (cmint_a_pda, _) = find_cmint_address(&mint_signer_a_pda); + let (cmint_b_pda, _) = find_cmint_address(&mint_signer_b_pda); + let (cmint_c_pda, _) = find_cmint_address(&mint_signer_c_pda); + let (cmint_d_pda, _) = find_cmint_address(&mint_signer_d_pda); + + // Get proof for all 4 mints + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![ + CreateAccountsProofInput::mint(mint_signer_a_pda), + CreateAccountsProofInput::mint(mint_signer_b_pda), + CreateAccountsProofInput::mint(mint_signer_c_pda), + CreateAccountsProofInput::mint(mint_signer_d_pda), + ], + ) + .await + .unwrap(); + + let accounts = csdk_anchor_full_derived_test::accounts::CreateFourMints { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer_a: mint_signer_a_pda, + mint_signer_b: mint_signer_b_pda, + mint_signer_c: mint_signer_c_pda, + mint_signer_d: mint_signer_d_pda, + cmint_a: cmint_a_pda, + cmint_b: cmint_b_pda, + cmint_c: cmint_c_pda, + cmint_d: cmint_d_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, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::CreateFourMints { + params: CreateFourMintsParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_a_bump, + mint_signer_b_bump, + mint_signer_c_bump, + mint_signer_d_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("CreateFourMints should succeed"); + + // Verify all 4 mints exist on-chain + use light_token_interface::state::Mint; + + let cmint_a_account = rpc + .get_account(cmint_a_pda) + .await + .unwrap() + .expect("Mint A should exist on-chain"); + let cmint_b_account = rpc + .get_account(cmint_b_pda) + .await + .unwrap() + .expect("Mint B should exist on-chain"); + let cmint_c_account = rpc + .get_account(cmint_c_pda) + .await + .unwrap() + .expect("Mint C should exist on-chain"); + let cmint_d_account = rpc + .get_account(cmint_d_pda) + .await + .unwrap() + .expect("Mint D should exist on-chain"); + + // Parse and verify mint data + let mint_a: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_a_account.data[..]) + .expect("Failed to deserialize Mint A"); + let mint_b: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_b_account.data[..]) + .expect("Failed to deserialize Mint B"); + let mint_c: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_c_account.data[..]) + .expect("Failed to deserialize Mint C"); + let mint_d: Mint = borsh::BorshDeserialize::deserialize(&mut &cmint_d_account.data[..]) + .expect("Failed to deserialize Mint D"); + + // Verify decimals match what was specified in #[light_mint] + assert_eq!(mint_a.base.decimals, 6, "Mint A should have 6 decimals"); + assert_eq!(mint_b.base.decimals, 8, "Mint B should have 8 decimals"); + assert_eq!(mint_c.base.decimals, 9, "Mint C should have 9 decimals"); + assert_eq!(mint_d.base.decimals, 12, "Mint D should have 12 decimals"); + + // Verify mint authorities + assert_eq!( + mint_a.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint A authority should be fee_payer" + ); + assert_eq!( + mint_b.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint B authority should be fee_payer" + ); + assert_eq!( + mint_c.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint C authority should be fee_payer" + ); + assert_eq!( + mint_d.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint D authority should be fee_payer" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs index dce2bac012..6ef5900474 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/integration_tests.rs @@ -230,7 +230,8 @@ async fn test_d6_account() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D6AccountRecordSeeds; - ctx.assert_lifecycle(&pda, D6AccountRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D6AccountRecordSeeds { owner }) + .await; } /// Tests D6Boxed: Box> type @@ -288,7 +289,8 @@ async fn test_d6_boxed() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D6BoxedRecordSeeds; - ctx.assert_lifecycle(&pda, D6BoxedRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D6BoxedRecordSeeds { owner }) + .await; } // ============================================================================= @@ -350,7 +352,8 @@ async fn test_d8_pda_only() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D8PdaOnlyRecordSeeds; - ctx.assert_lifecycle(&pda, D8PdaOnlyRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D8PdaOnlyRecordSeeds { owner }) + .await; } /// Tests D8MultiRentfree: Multiple #[rentfree] fields of same type @@ -459,11 +462,7 @@ async fn test_d8_multi_rentfree() { .await .unwrap(); ctx.rpc - .create_and_send_transaction( - &decompress_instructions, - &ctx.payer.pubkey(), - &[&ctx.payer], - ) + .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await .unwrap(); ctx.assert_onchain_exists(&pda1).await; @@ -492,11 +491,7 @@ async fn test_d8_multi_rentfree() { .await .unwrap(); ctx.rpc - .create_and_send_transaction( - &decompress_instructions, - &ctx.payer.pubkey(), - &[&ctx.payer], - ) + .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await .unwrap(); ctx.assert_onchain_exists(&pda2).await; @@ -600,11 +595,7 @@ async fn test_d8_all() { .await .unwrap(); ctx.rpc - .create_and_send_transaction( - &decompress_instructions, - &ctx.payer.pubkey(), - &[&ctx.payer], - ) + .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await .unwrap(); ctx.assert_onchain_exists(&pda_single).await; @@ -633,11 +624,7 @@ async fn test_d8_all() { .await .unwrap(); ctx.rpc - .create_and_send_transaction( - &decompress_instructions, - &ctx.payer.pubkey(), - &[&ctx.payer], - ) + .create_and_send_transaction(&decompress_instructions, &ctx.payer.pubkey(), &[&ctx.payer]) .await .unwrap(); ctx.assert_onchain_exists(&pda_multi).await; @@ -879,7 +866,8 @@ async fn test_d9_param() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9ParamRecordSeeds; - ctx.assert_lifecycle(&pda, D9ParamRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D9ParamRecordSeeds { owner }) + .await; } /// Tests D9ParamBytes: Param bytes seed expression (u64) @@ -940,7 +928,8 @@ async fn test_d9_param_bytes() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9ParamBytesRecordSeeds; - ctx.assert_lifecycle(&pda, D9ParamBytesRecordSeeds { id }).await; + ctx.assert_lifecycle(&pda, D9ParamBytesRecordSeeds { id }) + .await; } /// Tests D9Mixed: Mixed seed expression types @@ -1071,7 +1060,8 @@ async fn test_d7_payer() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7PayerRecordSeeds; - ctx.assert_lifecycle(&pda, D7PayerRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D7PayerRecordSeeds { owner }) + .await; } /// Tests D7Creator: "creator" field name variant @@ -1128,7 +1118,8 @@ async fn test_d7_creator() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D7CreatorRecordSeeds; - ctx.assert_lifecycle(&pda, D7CreatorRecordSeeds { owner }).await; + ctx.assert_lifecycle(&pda, D7CreatorRecordSeeds { owner }) + .await; } // ============================================================================= @@ -1192,7 +1183,8 @@ async fn test_d9_function_call() { // Full lifecycle: compression + decompression use csdk_anchor_full_derived_test::csdk_anchor_full_derived_test::D9FuncRecordSeeds; - ctx.assert_lifecycle(&pda, D9FuncRecordSeeds { key_a, key_b }).await; + ctx.assert_lifecycle(&pda, D9FuncRecordSeeds { key_a, key_b }) + .await; } /// Tests D9All: All 6 seed expression types @@ -1316,10 +1308,11 @@ async fn test_d9_all() { .get_account_info_interface(pda, &ctx.program_id) .await .unwrap(); - let program_owned_accounts = vec![ - RentFreeDecompressAccount::from_seeds(AccountInterface::from(&interface), seeds) - .unwrap(), - ]; + let program_owned_accounts = + vec![ + RentFreeDecompressAccount::from_seeds(AccountInterface::from(&interface), seeds) + .unwrap(), + ]; let decompress_instructions = create_load_accounts_instructions( &program_owned_accounts, &[], diff --git a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs index c2300e0076..680327ce0e 100644 --- a/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs +++ b/sdk-tests/sdk-token-test/tests/test_create_two_mints.rs @@ -89,16 +89,16 @@ async fn test_create_mints(n: usize) { .cpi_context .expect("CPI context account required"); - // Account layout (remaining_accounts): + // Account layout (remaining_accounts) must match SDK's create_mints expected order: // [0]: light_system_program // [1..N+1]: mint_signers (SIGNER) // [N+1..N+6]: system PDAs (cpi_authority, registered_program, compression_authority, compression_program, system_program) // [N+6]: cpi_context_account // [N+7]: output_queue - // [N+8]: address_tree - // [N+9]: compressible_config - // [N+10]: rent_sponsor - // [N+11]: state_merkle_tree + // [N+8]: state_merkle_tree + // [N+9]: address_tree (must be at index 1 in tree accounts for create_mint validation) + // [N+10]: compressible_config + // [N+11]: rent_sponsor // [N+12..2N+12]: mint_pdas // [2N+12]: compressed_token_program (for CPI) let mut accounts = vec![ @@ -117,11 +117,11 @@ async fn test_create_mints(n: usize) { AccountMeta::new_readonly(system_accounts.account_compression_program, false), AccountMeta::new_readonly(system_accounts.system_program, false), AccountMeta::new(cpi_context_pubkey, false), - AccountMeta::new(state_tree_info.queue, false), - AccountMeta::new(address_tree_info.tree, false), - AccountMeta::new_readonly(config_pda().into(), false), - AccountMeta::new(rent_sponsor_pda().into(), false), - AccountMeta::new(state_tree_info.tree, false), + AccountMeta::new(state_tree_info.queue, false), // output_queue at [N+7] + AccountMeta::new(state_tree_info.tree, false), // state_merkle_tree at [N+8] + AccountMeta::new(address_tree_info.tree, false), // address_tree at [N+9] + AccountMeta::new_readonly(config_pda(), false), + AccountMeta::new(rent_sponsor_pda(), false), ]); for (mint_pda, _) in &mint_pdas { @@ -149,7 +149,7 @@ async fn test_create_mints(n: usize) { .get_account(*mint_pda) .await .expect("Failed to get mint account") - .expect(&format!("Mint PDA {} should exist after decompress", i + 1)); + .unwrap_or_else(|| panic!("Mint PDA {} should exist after decompress", i + 1)); assert!( !mint_account.data.is_empty(),