From 8fd073fb103ec5a9b7d275625779429b0c59918c Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 17:10:39 +0000 Subject: [PATCH 1/7] refactor rentfree to builder pattern --- .../macros/src/rentfree/accounts/builder.rs | 395 +++++++++++++ .../macros/src/rentfree/accounts/derive.rs | 55 ++ .../src/rentfree/accounts/light_mint.rs | 44 +- sdk-libs/macros/src/rentfree/accounts/mod.rs | 13 +- .../macros/src/rentfree/accounts/parse.rs | 135 +++-- sdk-libs/macros/src/rentfree/accounts/pda.rs | 528 +++++------------- 6 files changed, 721 insertions(+), 449 deletions(-) create mode 100644 sdk-libs/macros/src/rentfree/accounts/builder.rs create mode 100644 sdk-libs/macros/src/rentfree/accounts/derive.rs diff --git a/sdk-libs/macros/src/rentfree/accounts/builder.rs b/sdk-libs/macros/src/rentfree/accounts/builder.rs new file mode 100644 index 0000000000..3b85b2fcc7 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/accounts/builder.rs @@ -0,0 +1,395 @@ +//! Builder for RentFree derive macro code generation. +//! +//! Encapsulates parsed struct data and resolved infrastructure fields, +//! providing methods for validation, querying, and code generation. + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::DeriveInput; + +use super::{ + light_mint::{generate_mint_action_invocation, MintActionConfig}, + parse::{InfraFields, ParsedRentFreeStruct}, + pda::generate_pda_compress_blocks, +}; + +/// Resolve optional field name to TokenStream, using default if None. +fn resolve_field_name(field: &Option, default: &str) -> TokenStream { + field.as_ref().map(|f| quote! { #f }).unwrap_or_else(|| { + let ident = format_ident!("{}", default); + quote! { #ident } + }) +} + +/// Resolved infrastructure field names as TokenStreams. +struct ResolvedInfraFields { + fee_payer: TokenStream, + compression_config: TokenStream, + ctoken_config: TokenStream, + ctoken_rent_sponsor: TokenStream, + light_token_program: TokenStream, + ctoken_cpi_authority: TokenStream, +} + +impl ResolvedInfraFields { + fn from_infra(infra: &InfraFields) -> Self { + Self { + fee_payer: resolve_field_name(&infra.fee_payer, "fee_payer"), + compression_config: resolve_field_name(&infra.compression_config, "compression_config"), + ctoken_config: resolve_field_name(&infra.ctoken_config, "ctoken_compressible_config"), + ctoken_rent_sponsor: resolve_field_name( + &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", + ), + } + } +} + +/// Builder for RentFree derive macro code generation. +/// +/// Encapsulates parsed struct data and resolved infrastructure fields, +/// providing methods for validation, querying, and code generation. +pub(super) struct RentFreeBuilder { + parsed: ParsedRentFreeStruct, + infra: ResolvedInfraFields, +} + +impl RentFreeBuilder { + /// Parse a DeriveInput and construct the builder. + pub fn parse(input: &DeriveInput) -> Result { + let parsed = super::parse::parse_rentfree_struct(input)?; + let infra = ResolvedInfraFields::from_infra(&parsed.infra_fields); + Ok(Self { parsed, infra }) + } + + /// Validate constraints (e.g., account count < 255). + pub fn validate(&self) -> Result<(), syn::Error> { + let total = self.parsed.rentfree_fields.len() + self.parsed.light_mint_fields.len(); + if total > 255 { + return Err(syn::Error::new_spanned( + &self.parsed.struct_name, + format!( + "Too many compression fields ({} PDAs + {} mints = {} total, maximum 255). \ + Light Protocol uses u8 for account indices.", + self.parsed.rentfree_fields.len(), + self.parsed.light_mint_fields.len(), + total + ), + )); + } + Ok(()) + } + + /// Query: any #[rentfree] fields? + pub fn has_pdas(&self) -> bool { + !self.parsed.rentfree_fields.is_empty() + } + + /// Query: any #[light_mint] fields? + pub fn has_mints(&self) -> bool { + !self.parsed.light_mint_fields.is_empty() + } + + /// Query: #[instruction(...)] present? + pub fn has_instruction_args(&self) -> bool { + self.parsed.instruction_args.is_some() + } + + /// Generate no-op trait impls (for backwards compatibility). + pub fn generate_noop_impls(&self) -> Result { + let struct_name = &self.parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); + + Ok(quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + ) -> std::result::Result { + Ok(false) + } + } + + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, ()> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + _params: &(), + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + Ok(()) + } + } + }) + } + + /// 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 + pub fn generate_pre_init_pdas_and_mints(&self) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&self.parsed.rentfree_fields); + let rentfree_count = self.parsed.rentfree_fields.len() as u8; + let pda_count = self.parsed.rentfree_fields.len(); + + // Get instruction param ident + let params_ident = &self + .parsed + .instruction_args + .as_ref() + .unwrap() + .first() + .unwrap() + .name; + + // 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; + + // Infra field references + let fee_payer = &self.infra.fee_payer; + let compression_config = &self.infra.compression_config; + let ctoken_config = &self.infra.ctoken_config; + let ctoken_rent_sponsor = &self.infra.ctoken_rent_sponsor; + let light_token_program = &self.infra.light_token_program; + let ctoken_cpi_authority = &self.infra.ctoken_cpi_authority; + + // Generate mint action invocation with CPI context + let mint_invocation = generate_mint_action_invocation(&MintActionConfig { + mint, + params_ident, + fee_payer, + ctoken_config, + ctoken_rent_sponsor, + light_token_program, + ctoken_cpi_authority, + cpi_context: Some((quote! { #first_pda_output_tree }, mint_assigned_index)), + }); + + quote! { + // Build CPI accounts WITH CPI context for batching + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( + &self.#fee_payer, + _remaining, + light_sdk_types::cpi_accounts::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree PDA accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Step 1: Write PDAs to CPI context + let cpi_context_account = cpi_accounts.cpi_context()?; + let cpi_context_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { + fee_payer: cpi_accounts.fee_payer(), + authority: cpi_accounts.authority()?, + cpi_context: cpi_context_account, + cpi_signer: crate::LIGHT_CPI_SIGNER, + }; + + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .write_to_cpi_context_first() + .invoke_write_to_cpi_context_first(cpi_context_accounts)?; + + // Step 2: Build and invoke mint_action with decompress + CPI context + #mint_invocation + + Ok(true) + } + } + + /// Generate LightPreInit body for mints-only (no PDAs): + /// Invoke mint_action with decompress directly + /// After this, CMint is "hot" and usable in instruction body + pub fn generate_pre_init_mints_only(&self) -> TokenStream { + // Get instruction param ident + let params_ident = &self + .parsed + .instruction_args + .as_ref() + .unwrap() + .first() + .unwrap() + .name; + + // Infra field references + let fee_payer = &self.infra.fee_payer; + let ctoken_config = &self.infra.ctoken_config; + let ctoken_rent_sponsor = &self.infra.ctoken_rent_sponsor; + let light_token_program = &self.infra.light_token_program; + let ctoken_cpi_authority = &self.infra.ctoken_cpi_authority; + + // 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 + let mint_invocation = generate_mint_action_invocation(&MintActionConfig { + mint, + params_ident, + fee_payer, + ctoken_config, + ctoken_rent_sponsor, + light_token_program, + ctoken_cpi_authority, + cpi_context: None, + }); + + quote! { + // Build CPI accounts (no CPI context needed for mints-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Build and invoke mint_action with decompress + #mint_invocation + + Ok(true) + } + } + + /// Generate LightPreInit body for PDAs only (no mints) + /// After this, compressed addresses are registered + pub fn generate_pre_init_pdas_only(&self) -> TokenStream { + let (compress_blocks, new_addr_idents) = + generate_pda_compress_blocks(&self.parsed.rentfree_fields); + let rentfree_count = self.parsed.rentfree_fields.len() as u8; + + // Get instruction param ident + let params_ident = &self + .parsed + .instruction_args + .as_ref() + .unwrap() + .first() + .unwrap() + .name; + + // Infra field references + let fee_payer = &self.infra.fee_payer; + let compression_config = &self.infra.compression_config; + + quote! { + // Build CPI accounts (no CPI context needed for PDAs-only) + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.#fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.#compression_config, + &crate::ID + )?; + + // Collect compressed infos for all rentfree accounts + let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); + #(#compress_blocks)* + + // Execute Light System Program CPI directly with proof + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + #params_ident.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[#(#new_addr_idents),*]) + .with_account_infos(&all_compressed_infos) + .invoke(cpi_accounts)?; + + Ok(true) + } + } + + /// Generate LightPreInit trait implementation. + pub fn generate_pre_init_impl(&self, body: TokenStream) -> TokenStream { + let struct_name = &self.parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); + + let first_arg = self + .parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .unwrap(); + + let params_type = &first_arg.ty; + let params_ident = &first_arg.name; + + quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + ) -> std::result::Result { + use anchor_lang::ToAccountInfo; + #body + } + } + } + } + + /// Generate LightFinalize trait implementation. + pub fn generate_finalize_impl(&self, body: TokenStream) -> TokenStream { + let struct_name = &self.parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); + + let first_arg = self + .parsed + .instruction_args + .as_ref() + .and_then(|args| args.first()) + .unwrap(); + + let params_type = &first_arg.ty; + let params_ident = &first_arg.name; + + quote! { + #[automatically_derived] + impl #impl_generics light_sdk::compressible::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + #params_ident: &#params_type, + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + use anchor_lang::ToAccountInfo; + #body + } + } + } + } +} diff --git a/sdk-libs/macros/src/rentfree/accounts/derive.rs b/sdk-libs/macros/src/rentfree/accounts/derive.rs new file mode 100644 index 0000000000..0de5ce2c34 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/accounts/derive.rs @@ -0,0 +1,55 @@ +//! Orchestration layer for RentFree derive macro. +//! +//! This module coordinates code generation by combining: +//! - PDA block generation from `pda.rs` +//! - Mint action invocation from `light_mint.rs` +//! - Parsing results from `parse.rs` +//! +//! Design for mints: +//! - At mint init, we CREATE + DECOMPRESS atomically +//! - After init, the CMint should always be in decompressed/"hot" state +//! +//! Flow for PDAs + mints: +//! 1. Pre-init: ALL compression logic executes here +//! a. Write PDAs to CPI context +//! b. Invoke mint_action with decompress + CPI context +//! c. CMint is now "hot" and usable +//! 2. Instruction body: Can use hot CMint (mintTo, transfers, etc.) +//! 3. Finalize: No-op (all work done in pre_init) + +use proc_macro2::TokenStream; +use quote::quote; +use syn::DeriveInput; + +use super::builder::RentFreeBuilder; + +/// Main orchestration - shows the high-level flow clearly. +pub(super) fn derive_rentfree(input: &DeriveInput) -> Result { + let builder = RentFreeBuilder::parse(input)?; + builder.validate()?; + + // No instruction args = no-op impls (backwards compatibility) + if !builder.has_instruction_args() { + return builder.generate_noop_impls(); + } + + // Generate pre_init body based on what fields we have + let pre_init = if builder.has_pdas() && builder.has_mints() { + builder.generate_pre_init_pdas_and_mints() + } else if builder.has_mints() { + builder.generate_pre_init_mints_only() + } else if builder.has_pdas() { + builder.generate_pre_init_pdas_only() + } else { + quote! { Ok(false) } + }; + + // Generate trait implementations + let pre_init_impl = builder.generate_pre_init_impl(pre_init); + let finalize_impl = builder.generate_finalize_impl(quote! { Ok(()) }); + + Ok(quote! { + #pre_init_impl + #finalize_impl + }) +} diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index 1f45f12677..3def68f52e 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -163,6 +163,40 @@ fn generate_write_top_up_tokens(write_top_up: &Option) -> TokenStream { } } +/// Builder for mint field expression generation. +/// +/// Encapsulates the generation of TokenStreams for optional mint field attributes +/// like signer_seeds, freeze_authority, rent_payment, and write_top_up. +pub(super) struct MintExprBuilder<'a> { + field: &'a LightMintField, +} + +impl<'a> MintExprBuilder<'a> { + pub fn new(field: &'a LightMintField) -> Self { + Self { field } + } + + /// Generate signer seeds expression (explicit or empty default). + pub fn signer_seeds(&self) -> TokenStream { + generate_signer_seeds_tokens(&self.field.signer_seeds) + } + + /// Generate freeze authority expression (Some or None). + pub fn freeze_authority(&self) -> TokenStream { + generate_freeze_authority_tokens(&self.field.freeze_authority) + } + + /// Generate rent payment expression with default. + pub fn rent_payment(&self) -> TokenStream { + generate_rent_payment_tokens(&self.field.rent_payment) + } + + /// Generate write top-up expression with default. + pub fn write_top_up(&self) -> TokenStream { + generate_write_top_up_tokens(&self.field.write_top_up) + } +} + /// Configuration for mint_action CPI generation pub(super) struct MintActionConfig<'a> { pub mint: &'a LightMintField, @@ -196,10 +230,12 @@ pub(super) fn generate_mint_action_invocation(config: &MintActionConfig) -> Toke let decimals = &mint.decimals; let address_tree_info = &mint.address_tree_info; - let signer_seeds_tokens = generate_signer_seeds_tokens(&mint.signer_seeds); - let freeze_authority_tokens = generate_freeze_authority_tokens(&mint.freeze_authority); - let rent_payment_tokens = generate_rent_payment_tokens(&mint.rent_payment); - let write_top_up_tokens = generate_write_top_up_tokens(&mint.write_top_up); + // Use MintExprBuilder for optional field expressions + let expr_builder = MintExprBuilder::new(mint); + let signer_seeds_tokens = expr_builder.signer_seeds(); + let freeze_authority_tokens = expr_builder.freeze_authority(); + let rent_payment_tokens = expr_builder.rent_payment(); + let write_top_up_tokens = expr_builder.write_top_up(); // Queue access differs based on CPI context presence let queue_access = if cpi_context.is_some() { diff --git a/sdk-libs/macros/src/rentfree/accounts/mod.rs b/sdk-libs/macros/src/rentfree/accounts/mod.rs index c10cc6a7c4..8ca7039210 100644 --- a/sdk-libs/macros/src/rentfree/accounts/mod.rs +++ b/sdk-libs/macros/src/rentfree/accounts/mod.rs @@ -4,7 +4,15 @@ //! - `LightPreInit` trait implementation for pre-instruction compression setup //! - `LightFinalize` trait implementation for post-instruction cleanup //! - Supports rent-free PDAs, rent-free token accounts, and light mints +//! +//! Module structure: +//! - `parse.rs` - Parsing #[rentfree] and #[light_mint] attributes +//! - `pda.rs` - PDA block code generation +//! - `light_mint.rs` - Mint action invocation code generation +//! - `derive.rs` - Orchestration layer that wires everything together +mod builder; +mod derive; mod light_mint; mod parse; mod pda; @@ -13,8 +21,5 @@ use proc_macro2::TokenStream; use syn::DeriveInput; pub fn derive_rentfree(input: DeriveInput) -> Result { - let parsed = parse::parse_rentfree_struct(&input)?; - pda::generate_rentfree_impl(&parsed) + derive::derive_rentfree(&input) } - -// TODO: add a codegen file that puts the generated code together diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index cc3dfebe4a..395e3586d2 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -12,6 +12,71 @@ use super::light_mint::{parse_light_mint_attr, LightMintField}; // Import shared types from seed_extraction module pub(super) use crate::rentfree::traits::seed_extraction::extract_account_inner_type; +// ============================================================================ +// Infrastructure Field Classification +// ============================================================================ + +/// Classification of infrastructure fields by naming convention. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(super) enum InfraFieldType { + FeePayer, + CompressionConfig, + CTokenConfig, + CTokenRentSponsor, + CTokenProgram, + CTokenCpiAuthority, +} + +/// Classifier for infrastructure fields by naming convention. +pub(super) struct InfraFieldClassifier; + +impl InfraFieldClassifier { + /// Classify a field name into its infrastructure type, if any. + #[inline] + pub fn classify(name: &str) -> Option { + match name { + "fee_payer" | "payer" | "creator" => Some(InfraFieldType::FeePayer), + "compression_config" => Some(InfraFieldType::CompressionConfig), + "ctoken_compressible_config" | "ctoken_config" | "light_token_config_account" => { + Some(InfraFieldType::CTokenConfig) + } + "ctoken_rent_sponsor" | "light_token_rent_sponsor" => { + Some(InfraFieldType::CTokenRentSponsor) + } + "ctoken_program" | "light_token_program" => Some(InfraFieldType::CTokenProgram), + "ctoken_cpi_authority" + | "light_token_program_cpi_authority" + | "compress_token_program_cpi_authority" => Some(InfraFieldType::CTokenCpiAuthority), + _ => None, + } + } +} + +/// Collected infrastructure field identifiers. +#[derive(Default)] +pub(super) struct InfraFields { + pub fee_payer: Option, + pub compression_config: Option, + pub ctoken_config: Option, + pub ctoken_rent_sponsor: Option, + pub ctoken_program: Option, + pub ctoken_cpi_authority: Option, +} + +impl InfraFields { + /// Set an infrastructure field by type. + pub fn set(&mut self, field_type: InfraFieldType, ident: Ident) { + match field_type { + InfraFieldType::FeePayer => self.fee_payer = Some(ident), + InfraFieldType::CompressionConfig => self.compression_config = Some(ident), + InfraFieldType::CTokenConfig => self.ctoken_config = Some(ident), + InfraFieldType::CTokenRentSponsor => self.ctoken_rent_sponsor = Some(ident), + InfraFieldType::CTokenProgram => self.ctoken_program = Some(ident), + InfraFieldType::CTokenCpiAuthority => self.ctoken_cpi_authority = Some(ident), + } + } +} + // ============================================================================ // darling support for parsing Expr from attributes // ============================================================================ @@ -40,16 +105,8 @@ pub(super) struct ParsedRentFreeStruct { pub rentfree_fields: Vec, pub light_mint_fields: Vec, pub instruction_args: Option>, - pub fee_payer_field: Option, - pub compression_config_field: Option, - /// CToken compressible config account (for decompress mint) - pub ctoken_config_field: Option, - /// CToken rent sponsor account (for decompress mint) - pub ctoken_rent_sponsor_field: Option, - /// CToken program account (for decompress mint CPI) - pub ctoken_program_field: Option, - /// CToken CPI authority PDA (for decompress mint CPI) - pub ctoken_cpi_authority_field: Option, + /// Infrastructure fields detected by naming convention. + pub infra_fields: InfraFields, } /// A field marked with #[rentfree(...)] @@ -130,12 +187,7 @@ pub(super) fn parse_rentfree_struct(input: &DeriveInput) -> Result Result { - fee_payer_field = Some(field_ident.clone()); - } - "compression_config" => { - compression_config_field = Some(field_ident.clone()); - } - "ctoken_compressible_config" | "ctoken_config" | "light_token_config_account" => { - ctoken_config_field = Some(field_ident.clone()); - } - "ctoken_rent_sponsor" | "light_token_rent_sponsor" => { - ctoken_rent_sponsor_field = Some(field_ident.clone()); - } - "ctoken_program" | "light_token_program" => { - ctoken_program_field = Some(field_ident.clone()); - } - "ctoken_cpi_authority" - | "light_token_program_cpi_authority" - | "compress_token_program_cpi_authority" => { - ctoken_cpi_authority_field = Some(field_ident.clone()); - } - _ => {} + // Track infrastructure fields by naming convention using the classifier. + // See InfraFieldClassifier for supported field names. + if let Some(field_type) = InfraFieldClassifier::classify(&field_name) { + infra_fields.set(field_type, field_ident.clone()); } // Track if this field already has a compression attribute @@ -262,11 +278,6 @@ pub(super) fn parse_rentfree_struct(input: &DeriveInput) -> Result, default: &str) -> TokenStream { - field.as_ref().map(|f| quote! { #f }).unwrap_or_else(|| { - let ident = format_ident!("{}", default); - quote! { #ident } - }) +use syn::Ident; + +use super::parse::RentFreeField; + +/// Generated identifier names for a PDA field. +pub(super) struct PdaIdents { + pub idx: u8, + pub new_addr_params: Ident, + pub compressed_infos: Ident, + pub address: Ident, + pub account_info: Ident, + pub account_key: Ident, + pub account_data: Ident, } -/// Generate both trait implementations. -/// -/// Returns `Err` if the parsed struct has inconsistent state (e.g., params type without ident). -pub(super) fn generate_rentfree_impl( - parsed: &ParsedRentFreeStruct, -) -> Result { - let struct_name = &parsed.struct_name; - let (impl_generics, ty_generics, where_clause) = parsed.generics.split_for_impl(); - - // Validation: Ensure combined PDA + mint count fits in u8 (Light Protocol uses u8 for account indices) - let total_accounts = parsed.rentfree_fields.len() + parsed.light_mint_fields.len(); - if total_accounts > 255 { - return Err(syn::Error::new_spanned( - struct_name, - format!( - "Too many compression fields ({} PDAs + {} mints = {} total, maximum 255). \ - Light Protocol uses u8 for account indices.", - parsed.rentfree_fields.len(), - parsed.light_mint_fields.len(), - total_accounts - ), - )); - } - - // Extract first instruction arg or generate no-op impls - let first_arg = match parsed - .instruction_args - .as_ref() - .and_then(|args| args.first()) - { - Some(arg) => arg, - None => { - // No instruction args - generate no-op impls. - // Keep these for backwards compatibility with structs that derive RentFree - // without compression fields or instruction params. - return Ok(quote! { - #[automatically_derived] - impl #impl_generics light_sdk::compressible::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { - fn light_pre_init( - &mut self, - _remaining: &[solana_account_info::AccountInfo<'info>], - _params: &(), - ) -> std::result::Result { - Ok(false) - } - } - - #[automatically_derived] - impl #impl_generics light_sdk::compressible::LightFinalize<'info, ()> for #struct_name #ty_generics #where_clause { - fn light_finalize( - &mut self, - _remaining: &[solana_account_info::AccountInfo<'info>], - _params: &(), - _has_pre_init: bool, - ) -> std::result::Result<(), light_sdk::error::LightSdkError> { - Ok(()) - } - } - }); - } - }; - - let params_type = &first_arg.ty; - let params_ident = &first_arg.name; - - let has_pdas = !parsed.rentfree_fields.is_empty(); - let has_mints = !parsed.light_mint_fields.is_empty(); - - // Resolve field names with defaults - let fee_payer = resolve_field_name(&parsed.fee_payer_field, "fee_payer"); - let compression_config = - resolve_field_name(&parsed.compression_config_field, "compression_config"); - let ctoken_config = - resolve_field_name(&parsed.ctoken_config_field, "ctoken_compressible_config"); - let ctoken_rent_sponsor = - resolve_field_name(&parsed.ctoken_rent_sponsor_field, "ctoken_rent_sponsor"); - let light_token_program = - resolve_field_name(&parsed.ctoken_program_field, "light_token_program"); - let ctoken_cpi_authority = - resolve_field_name(&parsed.ctoken_cpi_authority_field, "ctoken_cpi_authority"); - - // Generate LightPreInit impl based on what we have - // ALL compression logic runs in pre_init so instruction body can use hot state - let pre_init_body = if has_pdas && has_mints { - // PDAs + mints: Write PDAs to CPI context, then invoke mint_action with decompress - generate_pre_init_pdas_and_mints( - parsed, - params_ident, - &fee_payer, - &compression_config, - &ctoken_config, - &ctoken_rent_sponsor, - &light_token_program, - &ctoken_cpi_authority, - ) - } else if has_mints { - // Mints only: Invoke mint_action with decompress (no CPI context) - generate_pre_init_mints_only( - parsed, - params_ident, - &fee_payer, - &ctoken_config, - &ctoken_rent_sponsor, - &light_token_program, - &ctoken_cpi_authority, - ) - } else if has_pdas { - // PDAs only: Direct invoke (no CPI context needed) - generate_pre_init_pdas_only(parsed, params_ident, &fee_payer, &compression_config) - } else { - quote! { Ok(false) } - }; - - // LightFinalize: No-op (all work done in pre_init) - let finalize_body = quote! { Ok(()) }; - - Ok(quote! { - #[automatically_derived] - impl #impl_generics light_sdk::compressible::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { - fn light_pre_init( - &mut self, - _remaining: &[solana_account_info::AccountInfo<'info>], - #params_ident: &#params_type, - ) -> std::result::Result { - use anchor_lang::ToAccountInfo; - #pre_init_body - } - } - - #[automatically_derived] - impl #impl_generics light_sdk::compressible::LightFinalize<'info, #params_type> for #struct_name #ty_generics #where_clause { - fn light_finalize( - &mut self, - _remaining: &[solana_account_info::AccountInfo<'info>], - #params_ident: &#params_type, - _has_pre_init: bool, - ) -> std::result::Result<(), light_sdk::error::LightSdkError> { - use anchor_lang::ToAccountInfo; - #finalize_body - } +impl PdaIdents { + pub fn new(idx: usize) -> Self { + Self { + idx: idx as u8, + new_addr_params: format_ident!("__new_addr_params_{}", idx), + compressed_infos: format_ident!("__compressed_infos_{}", idx), + address: format_ident!("__address_{}", idx), + account_info: format_ident!("__account_info_{}", idx), + account_key: format_ident!("__account_key_{}", idx), + account_data: format_ident!("__account_data_{}", idx), } - }) -} - -/// Generate LightPreInit body for PDAs + mints: -/// 1. Write PDAs to CPI context -/// 2. Invoke mint_action with decompress + CPI context -/// After this, Mint is "hot" and usable in instruction body -#[allow(clippy::too_many_arguments)] -fn generate_pre_init_pdas_and_mints( - parsed: &ParsedRentFreeStruct, - params_ident: &syn::Ident, - fee_payer: &TokenStream, - compression_config: &TokenStream, - ctoken_config: &TokenStream, - ctoken_rent_sponsor: &TokenStream, - light_token_program: &TokenStream, - ctoken_cpi_authority: &TokenStream, -) -> TokenStream { - let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&parsed.rentfree_fields); - let rentfree_count = parsed.rentfree_fields.len() as u8; - let pda_count = parsed.rentfree_fields.len(); - - // Get the first PDA's output tree index (for the state tree output queue) - let first_pda_output_tree = &parsed.rentfree_fields[0].output_tree; - - // 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 = &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 = generate_mint_action_invocation(&MintActionConfig { - mint, - params_ident, - fee_payer, - ctoken_config, - ctoken_rent_sponsor, - light_token_program, - ctoken_cpi_authority, - cpi_context: Some((quote! { #first_pda_output_tree }, mint_assigned_index)), - }); - - quote! { - // Build CPI accounts WITH CPI context for batching - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new_with_config( - &self.#fee_payer, - _remaining, - light_sdk_types::cpi_accounts::CpiAccountsConfig::new_with_cpi_context(crate::LIGHT_CPI_SIGNER), - ); - - // Load compression config - let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( - &self.#compression_config, - &crate::ID - )?; - - // Collect compressed infos for all rentfree PDA accounts - let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); - #(#compress_blocks)* - - // Step 1: Write PDAs to CPI context - let cpi_context_account = cpi_accounts.cpi_context()?; - let cpi_context_accounts = light_sdk_types::cpi_context_write::CpiContextWriteAccounts { - fee_payer: cpi_accounts.fee_payer(), - authority: cpi_accounts.authority()?, - cpi_context: cpi_context_account, - cpi_signer: crate::LIGHT_CPI_SIGNER, - }; - - use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; - light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( - crate::LIGHT_CPI_SIGNER, - #params_ident.create_accounts_proof.proof.clone() - ) - .with_new_addresses(&[#(#new_addr_idents),*]) - .with_account_infos(&all_compressed_infos) - .write_to_cpi_context_first() - .invoke_write_to_cpi_context_first(cpi_context_accounts)?; - - // Step 2: Build and invoke mint_action with decompress + CPI context - #mint_invocation - - Ok(true) } } -/// Generate LightPreInit body for mints-only (no PDAs): -/// Invoke mint_action with decompress directly -/// After this, CMint is "hot" and usable in instruction body -#[allow(clippy::too_many_arguments)] -fn generate_pre_init_mints_only( - parsed: &ParsedRentFreeStruct, - params_ident: &syn::Ident, - fee_payer: &TokenStream, - ctoken_config: &TokenStream, - ctoken_rent_sponsor: &TokenStream, - light_token_program: &TokenStream, - ctoken_cpi_authority: &TokenStream, -) -> TokenStream { - // 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 = &parsed.light_mint_fields[0]; - - // Generate mint action invocation without CPI context - let mint_invocation = generate_mint_action_invocation(&MintActionConfig { - mint, - params_ident, - fee_payer, - ctoken_config, - ctoken_rent_sponsor, - light_token_program, - ctoken_cpi_authority, - cpi_context: None, - }); - - quote! { - // Build CPI accounts (no CPI context needed for mints-only) - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( - &self.#fee_payer, - _remaining, - crate::LIGHT_CPI_SIGNER, - ); - - // Build and invoke mint_action with decompress - #mint_invocation - - Ok(true) - } +/// Builder for PDA compression block code generation. +pub(super) struct PdaBlockBuilder<'a> { + field: &'a RentFreeField, + idents: PdaIdents, } -/// Generate LightPreInit body for PDAs only (no mints) -/// After this, compressed addresses are registered -fn generate_pre_init_pdas_only( - parsed: &ParsedRentFreeStruct, - params_ident: &syn::Ident, - fee_payer: &TokenStream, - compression_config: &TokenStream, -) -> TokenStream { - let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&parsed.rentfree_fields); - let rentfree_count = parsed.rentfree_fields.len() as u8; - - quote! { - // Build CPI accounts (no CPI context needed for PDAs-only) - let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( - &self.#fee_payer, - _remaining, - crate::LIGHT_CPI_SIGNER, - ); - - // Load compression config - let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( - &self.#compression_config, - &crate::ID - )?; - - // Collect compressed infos for all rentfree accounts - let mut all_compressed_infos = Vec::with_capacity(#rentfree_count as usize); - #(#compress_blocks)* - - // Execute Light System Program CPI directly with proof - use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; - light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( - crate::LIGHT_CPI_SIGNER, - #params_ident.create_accounts_proof.proof.clone() - ) - .with_new_addresses(&[#(#new_addr_idents),*]) - .with_account_infos(&all_compressed_infos) - .invoke(cpi_accounts)?; - - Ok(true) +impl<'a> PdaBlockBuilder<'a> { + pub fn new(field: &'a RentFreeField, idx: usize) -> Self { + Self { + field, + idents: PdaIdents::new(idx), + } } -} - -/// Generate compression blocks for PDA fields -fn generate_pda_compress_blocks(fields: &[RentFreeField]) -> (Vec, Vec) { - let mut blocks = Vec::new(); - let mut addr_idents = Vec::new(); - for (idx, field) in fields.iter().enumerate() { - let idx_lit = idx as u8; - let ident = &field.ident; - let addr_tree_info = &field.address_tree_info; - let output_tree = &field.output_tree; - let inner_type = &field.inner_type; - - let new_addr_params_ident = format_ident!("__new_addr_params_{}", idx); - let compressed_infos_ident = format_ident!("__compressed_infos_{}", idx); - let address_ident = format_ident!("__address_{}", idx); - let account_info_ident = format_ident!("__account_info_{}", idx); - let account_key_ident = format_ident!("__account_key_{}", idx); - let account_data_ident = format_ident!("__account_data_{}", idx); + /// Returns the identifier used for new address params (for collecting in array). + pub fn new_addr_ident(&self) -> TokenStream { + let ident = &self.idents.new_addr_params; + quote! { #ident } + } - // Generate correct deref pattern: ** for Box>, * for Account - let deref_expr = if field.is_boxed { - quote! { &mut **self.#ident } - } else { - quote! { &mut *self.#ident } - }; + /// Generate account extraction (get account info and key bytes). + fn account_extraction(&self) -> TokenStream { + let ident = &self.field.ident; + let account_info = &self.idents.account_info; + let account_key = &self.idents.account_key; - addr_idents.push(quote! { #new_addr_params_ident }); + quote! { + let #account_info = self.#ident.to_account_info(); + let #account_key = #account_info.key.to_bytes(); + } + } - blocks.push(quote! { - // Get account info early before any mutable borrows - let #account_info_ident = self.#ident.to_account_info(); - let #account_key_ident = #account_info_ident.key.to_bytes(); + /// Generate new address params struct. + fn new_addr_params(&self) -> TokenStream { + let addr_tree_info = &self.field.address_tree_info; + let new_addr_params = &self.idents.new_addr_params; + let account_key = &self.idents.account_key; + let idx = self.idents.idx; - let #new_addr_params_ident = { + quote! { + let #new_addr_params = { let tree_info = &#addr_tree_info; light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked { - seed: #account_key_ident, + seed: #account_key, address_merkle_tree_account_index: tree_info.address_merkle_tree_pubkey_index, address_queue_account_index: tree_info.address_queue_pubkey_index, address_merkle_tree_root_index: tree_info.root_index, assigned_to_account: true, - assigned_account_index: #idx_lit, + assigned_account_index: #idx, } }; + } + } - // Derive the compressed address - let #address_ident = light_compressed_account::address::derive_address( - &#new_addr_params_ident.seed, + /// Generate address derivation from seed and merkle tree. + fn address_derivation(&self) -> TokenStream { + let address = &self.idents.address; + let new_addr_params = &self.idents.new_addr_params; + + quote! { + let #address = light_compressed_account::address::derive_address( + &#new_addr_params.seed, &cpi_accounts - .get_tree_account_info(#new_addr_params_ident.address_merkle_tree_account_index as usize)? + .get_tree_account_info(#new_addr_params.address_merkle_tree_account_index as usize)? .key() .to_bytes(), &crate::ID.to_bytes(), ); + } + } - // Get mutable reference to inner account data - let #account_data_ident = #deref_expr; + /// Generate mutable reference to account data (handles Box vs Account). + fn account_data_extraction(&self) -> TokenStream { + let ident = &self.field.ident; + let account_data = &self.idents.account_data; - let #compressed_infos_ident = light_sdk::compressible::prepare_compressed_account_on_init::<#inner_type>( - &#account_info_ident, - #account_data_ident, + let deref_expr = if self.field.is_boxed { + quote! { &mut **self.#ident } + } else { + quote! { &mut *self.#ident } + }; + + quote! { + let #account_data = #deref_expr; + } + } + + /// Generate compression info preparation and collection. + fn compression_info(&self) -> TokenStream { + let inner_type = &self.field.inner_type; + let output_tree = &self.field.output_tree; + let account_info = &self.idents.account_info; + let account_data = &self.idents.account_data; + let address = &self.idents.address; + let new_addr_params = &self.idents.new_addr_params; + let compressed_infos = &self.idents.compressed_infos; + + quote! { + let #compressed_infos = light_sdk::compressible::prepare_compressed_account_on_init::<#inner_type>( + &#account_info, + #account_data, &compression_config_data, - #address_ident, - #new_addr_params_ident, + #address, + #new_addr_params, #output_tree, &cpi_accounts, &compression_config_data.address_space, false, // at init, we do not compress_and_close the pda, we just "register" the empty compressed account with the derived address. )?; - all_compressed_infos.push(#compressed_infos_ident); - }); + all_compressed_infos.push(#compressed_infos); + } + } + + /// Build the complete compression block for this PDA field. + pub fn build(&self) -> TokenStream { + let account_extraction = self.account_extraction(); + let new_addr_params = self.new_addr_params(); + let address_derivation = self.address_derivation(); + let account_data_extraction = self.account_data_extraction(); + let compression_info = self.compression_info(); + + quote! { + // Get account info early before any mutable borrows + #account_extraction + #new_addr_params + // Derive the compressed address + #address_derivation + // Get mutable reference to inner account data + #account_data_extraction + #compression_info + } + } +} + +/// Generate compression blocks for PDA fields using PdaBlockBuilder. +/// +/// Returns a tuple of: +/// - Vector of TokenStreams for compression blocks +/// - Vector of TokenStreams for new address parameter identifiers +pub(super) fn generate_pda_compress_blocks( + fields: &[RentFreeField], +) -> (Vec, Vec) { + let mut blocks = Vec::with_capacity(fields.len()); + let mut addr_idents = Vec::with_capacity(fields.len()); + + for (idx, field) in fields.iter().enumerate() { + let builder = PdaBlockBuilder::new(field, idx); + addr_idents.push(builder.new_addr_ident()); + blocks.push(builder.build()); } (blocks, addr_idents) From 8b0f96161f17a5e1f504a6397efeaebd3e3038c9 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 17:30:49 +0000 Subject: [PATCH 2/7] add docs --- sdk-libs/macros/CLAUDE.md | 94 ++++ sdk-libs/macros/docs/CLAUDE.md | 56 +++ sdk-libs/macros/docs/rentfree.md | 580 +++++++++++++++++++++++ sdk-libs/macros/docs/rentfree_program.md | 578 ++++++++++++++++++++++ 4 files changed, 1308 insertions(+) create mode 100644 sdk-libs/macros/CLAUDE.md create mode 100644 sdk-libs/macros/docs/CLAUDE.md create mode 100644 sdk-libs/macros/docs/rentfree.md create mode 100644 sdk-libs/macros/docs/rentfree_program.md diff --git a/sdk-libs/macros/CLAUDE.md b/sdk-libs/macros/CLAUDE.md new file mode 100644 index 0000000000..abdf8b9ec8 --- /dev/null +++ b/sdk-libs/macros/CLAUDE.md @@ -0,0 +1,94 @@ +# light-sdk-macros + +Procedural macros for Light Protocol's rent-free compression system. + +## Crate Overview + +This crate provides macros that enable rent-free compressed accounts on Solana with minimal boilerplate. + +**Package**: `light-sdk-macros` +**Location**: `sdk-libs/macros/` + +## Main Macros + +| Macro | Type | Purpose | +|-------|------|---------| +| `#[derive(RentFree)]` | Derive | Generates `LightPreInit`/`LightFinalize` for Accounts structs | +| `#[rentfree_program]` | Attribute | Program-level auto-discovery and instruction generation | +| `#[derive(LightCompressible)]` | Derive | Combined traits for compressible account data | +| `#[derive(Compressible)]` | Derive | Compression traits (HasCompressionInfo, CompressAs, Size) | +| `#[derive(CompressiblePack)]` | Derive | Pack/Unpack with Pubkey-to-index compression | + +## Documentation + +Detailed macro documentation is in the `docs/` directory: + +- **`docs/CLAUDE.md`** - Documentation structure guide +- **`docs/rentfree.md`** - `#[derive(RentFree)]` and trait derives +- **`docs/rentfree_program.md`** - `#[rentfree_program]` attribute macro + +## Source Structure + +``` +src/ +├── lib.rs # Macro entry points +├── rentfree/ # RentFree macro system +│ ├── accounts/ # #[derive(RentFree)] for Accounts structs +│ ├── program/ # #[rentfree_program] attribute macro +│ ├── traits/ # Trait derive macros +│ └── shared_utils.rs # Common utilities +└── hasher/ # LightHasherSha derive macro +``` + +## Usage Example + +```rust +use light_sdk_macros::{rentfree_program, RentFree, LightCompressible}; + +// State account with compression support +#[derive(Default, Debug, InitSpace, LightCompressible)] +#[account] +pub struct UserRecord { + pub owner: Pubkey, + pub score: u64, + pub compression_info: Option, +} + +// Accounts struct with rent-free field +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct Create<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + #[account(init, payer = fee_payer, space = 8 + UserRecord::INIT_SPACE, seeds = [b"user", params.owner.as_ref()], bump)] + #[rentfree] + pub user_record: Account<'info, UserRecord>, +} + +// Program with auto-wrapped instructions +#[rentfree_program] +#[program] +pub mod my_program { + pub fn create(ctx: Context, params: CreateParams) -> Result<()> { + // Business logic - compression handled automatically + ctx.accounts.user_record.owner = params.owner; + Ok(()) + } +} +``` + +## Requirements + +Programs using these macros must define: +- `LIGHT_CPI_SIGNER: Pubkey` - CPI signer constant +- `ID` - Program ID (from `declare_id!`) + +## Testing + +```bash +cargo test -p light-sdk-macros +``` + +Integration tests are in `sdk-tests/`: +- `csdk-anchor-full-derived-test` - Full macro integration test diff --git a/sdk-libs/macros/docs/CLAUDE.md b/sdk-libs/macros/docs/CLAUDE.md new file mode 100644 index 0000000000..c1a84a3397 --- /dev/null +++ b/sdk-libs/macros/docs/CLAUDE.md @@ -0,0 +1,56 @@ +# Documentation Structure + +## Overview + +Documentation for the rentfree macro system in `light-sdk-macros`. These macros enable rent-free compressed accounts on Solana with minimal boilerplate. + +## Structure + +| File | Description | +|------|-------------| +| **`CLAUDE.md`** | This file - documentation structure guide | +| **`../CLAUDE.md`** | Main entry point for sdk-libs/macros | +| **`rentfree.md`** | `#[derive(RentFree)]` macro and trait derives | +| **`rentfree_program.md`** | `#[rentfree_program]` attribute macro | + +## Navigation Tips + +### Starting Points + +- **Building account structs**: Start with `rentfree.md` for the accounts-level derive macro that marks fields for compression +- **Program-level integration**: Use `rentfree_program.md` for program-level auto-discovery and instruction generation + +### Macro Hierarchy + +``` +#[rentfree_program] <- Program-level (rentfree_program.md) + | + +-- Discovers #[derive(RentFree)] structs + | + +-- Generates: + - RentFreeAccountVariant enum + - Seeds structs + - Compress/Decompress instructions + - Config instructions + +#[derive(RentFree)] <- Account-level (rentfree.md) + | + +-- Generates LightPreInit + LightFinalize impls + | + +-- Uses trait derives: + - HasCompressionInfo + - Compressible + - Pack/Unpack + - LightCompressible +``` + +## Related Source Code + +``` +sdk-libs/macros/src/rentfree/ +├── 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/rentfree.md b/sdk-libs/macros/docs/rentfree.md new file mode 100644 index 0000000000..daaa0ee421 --- /dev/null +++ b/sdk-libs/macros/docs/rentfree.md @@ -0,0 +1,580 @@ +# RentFree Derive Macro and Trait Derives + +## 1. Overview + +The `#[derive(RentFree)]` macro and associated trait derives enable rent-free compressed accounts on Solana with minimal boilerplate. These macros generate code for: + +- Pre-instruction compression setup (`LightPreInit` trait) +- Post-instruction cleanup (`LightFinalize` trait) +- Account data serialization and hashing +- Pubkey compression to u8 indices + +### 1.1 Module Structure + +``` +sdk-libs/macros/src/rentfree/ +|-- mod.rs # Module exports +|-- shared_utils.rs # Common utilities (constant detection, identifier extraction) +| +|-- accounts/ # #[derive(RentFree)] for Accounts structs +| |-- mod.rs # Module entry point +| |-- derive.rs # Orchestration layer +| |-- builder.rs # Code generation builder +| |-- parse.rs # Attribute parsing with darling +| |-- pda.rs # PDA block code generation +| +-- light_mint.rs # Mint action CPI generation +| ++-- traits/ # Trait derive macros for data structs + |-- mod.rs # Module entry point + |-- traits.rs # HasCompressionInfo, Compressible, CompressAs, Size + |-- pack_unpack.rs # Pack/Unpack traits with Packed struct generation + |-- light_compressible.rs # Combined LightCompressible derive + |-- seed_extraction.rs # Anchor seed extraction from #[account(...)] + |-- decompress_context.rs # Decompression context utilities + +-- utils.rs # Shared utilities (field extraction, type checks) +``` + +--- + +## 2. `#[derive(RentFree)]` Derive Macro + +### 2.1 Purpose + +Generates `LightPreInit` and `LightFinalize` trait implementations for Anchor Accounts structs. These traits enable automatic compression of PDA accounts and mint creation during instruction execution. + +**Source**: `sdk-libs/macros/src/rentfree/accounts/derive.rs` + +### 2.2 Supported Attributes + +#### `#[rentfree]` - Mark PDA Fields for Compression + +Applied to `Account<'info, T>` or `Box>` fields. + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateAccounts<'info> { + #[account( + init, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user", params.owner.as_ref()], + bump + )] + #[rentfree] // Uses default address_tree_info and output_tree from params + pub user_record: Account<'info, UserRecord>, +} +``` + +**Optional arguments**: +- `address_tree_info` - Expression for address tree info (default: `params.create_accounts_proof.address_tree_info`) +- `output_tree` - Expression for output tree index (default: `params.create_accounts_proof.output_state_tree_index`) + +```rust +#[rentfree( + address_tree_info = custom_tree_info, + output_tree = custom_output_index +)] +pub user_record: Account<'info, UserRecord>, +``` + +#### `#[light_mint(...)]` - Mark Mint Fields + +Creates a compressed mint with automatic decompression. + +```rust +#[light_mint( + mint_signer = mint_signer, // AccountInfo that seeds the mint PDA (required) + authority = authority, // Mint authority (required) + decimals = 9, // Token decimals (required) + freeze_authority = freeze_auth, // Optional freeze authority + signer_seeds = &[b"mint", &[bump]], // Optional PDA signer seeds + rent_payment = 2, // Rent payment epochs (default: 2) + write_top_up = 0 // Write top-up lamports (default: 0) +)] +pub cmint: Account<'info, CMint>, +``` + +#### `#[instruction(...)]` - Specify Instruction Parameters (Required) + +Must be present on the struct when using `#[rentfree]` or `#[light_mint]`. + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateAccounts<'info> { ... } +``` + +### 2.3 Infrastructure Field Detection + +Infrastructure fields are auto-detected by naming convention. No attribute required. + +| Field Type | Accepted Names | +|------------|----------------| +| Fee Payer | `fee_payer`, `payer`, `creator` | +| Compression Config | `compression_config` | +| 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` | + +**Source**: `sdk-libs/macros/src/rentfree/accounts/parse.rs` (lines 30-53) + +### 2.4 Code Generation Flow + +``` +1. Parse + |-- parse_rentfree_struct() extracts: + | - Struct name and generics + | - #[rentfree] fields -> RentFreeField + | - #[light_mint] fields -> LightMintField + | - #[instruction] args + | - Infrastructure fields by naming convention + | +2. Validate + |-- Total fields <= 255 (u8 index limit) + |-- #[instruction] required when #[rentfree] or #[light_mint] present + | +3. Generate pre_init Body + |-- PDAs + Mints: generate_pre_init_pdas_and_mints() + | - Write PDAs to CPI context + | - Invoke mint_action with decompress + CPI context + |-- Mints only: generate_pre_init_mints_only() + |-- PDAs only: generate_pre_init_pdas_only() + |-- Neither: Ok(false) + | +4. Wrap in Trait Impls + |-- LightPreInit<'info, ParamsType> + +-- LightFinalize<'info, ParamsType> +``` + +**Source**: `sdk-libs/macros/src/rentfree/accounts/derive.rs` + +### 2.5 Generated Code Example + +**Input**: + +```rust +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct CreateAccounts<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub compression_config: Account<'info, CompressibleConfig>, + + #[account( + init, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, + seeds = [b"user", params.owner.as_ref()], + bump + )] + #[rentfree] + pub user_record: Account<'info, UserRecord>, +} +``` + +**Output** (simplified): + +```rust +#[automatically_derived] +impl<'info> light_sdk::compressible::LightPreInit<'info, CreateParams> for CreateAccounts<'info> { + fn light_pre_init( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + params: &CreateParams, + ) -> std::result::Result { + use anchor_lang::ToAccountInfo; + + // Build CPI accounts + let cpi_accounts = light_sdk::cpi::v2::CpiAccounts::new( + &self.fee_payer, + _remaining, + crate::LIGHT_CPI_SIGNER, + ); + + // Load compression config + let compression_config_data = light_sdk::compressible::CompressibleConfig::load_checked( + &self.compression_config, + &crate::ID + )?; + + // Collect compressed infos + let mut all_compressed_infos = Vec::with_capacity(1); + + // PDA 0: user_record + let __account_info_0 = self.user_record.to_account_info(); + let __account_key_0 = __account_info_0.key.to_bytes(); + let __new_addr_params_0 = { /* NewAddressParamsAssignedPacked */ }; + let __address_0 = light_compressed_account::address::derive_address(/* ... */); + let __account_data_0 = &mut *self.user_record; + let __compressed_infos_0 = light_sdk::compressible::prepare_compressed_account_on_init::(/* ... */)?; + all_compressed_infos.push(__compressed_infos_0); + + // Execute Light System Program CPI + use light_sdk::cpi::{InvokeLightSystemProgram, LightCpiInstruction}; + light_sdk::cpi::v2::LightSystemProgramCpi::new_cpi( + crate::LIGHT_CPI_SIGNER, + params.create_accounts_proof.proof.clone() + ) + .with_new_addresses(&[__new_addr_params_0]) + .with_account_infos(&all_compressed_infos) + .invoke(cpi_accounts)?; + + Ok(true) + } +} + +#[automatically_derived] +impl<'info> light_sdk::compressible::LightFinalize<'info, CreateParams> for CreateAccounts<'info> { + fn light_finalize( + &mut self, + _remaining: &[solana_account_info::AccountInfo<'info>], + params: &CreateParams, + _has_pre_init: bool, + ) -> std::result::Result<(), light_sdk::error::LightSdkError> { + use anchor_lang::ToAccountInfo; + Ok(()) + } +} +``` + +--- + +## 3. Trait Derives (traits/) + +### 3.0 Trait Composition Overview + +The following diagram shows how the derive macros compose together to enable rent-free compressed accounts: + +``` + ACCOUNT STRUCT LEVEL + ==================== + + +--------------------+ + | #[derive(RentFree)]| <-- Applied to Anchor Accounts struct + +--------------------+ + | + | generates + v + +---------------------------+ + | LightPreInit + LightFinalize | + +---------------------------+ + | + | uses traits from + v + DATA STRUCT LEVEL + ================= + ++-------------------------------------------------------------------------+ +| #[derive(LightCompressible)] | +| (convenience macro - expands to all below) | ++-------------------------------------------------------------------------+ + | | | | + | expands to | expands to | expands to | expands to + v v v v ++----------------+ +------------------+ +--------------+ +-----------------+ +| LightHasherSha | | LightDiscriminator| | Compressible | | CompressiblePack| ++----------------+ +------------------+ +--------------+ +-----------------+ + | | | | + | generates | generates | generates | generates + v v v v ++----------------+ +------------------+ +--------------+ +-----------------+ +| - DataHasher | | - LightDiscriminator| | (see below)| | - Pack | +| - ToByteArray | | (8-byte unique ID) | | | | - Unpack | ++----------------+ +------------------+ +--------------+ | - Packed{Name} | + | | struct | + v +-----------------+ + +-----------------------------+ + | Compressible | + | (combined derive macro) | + +-----------------------------+ + | | | | + v v v v + +------------------+ +------------------+ + | HasCompressionInfo| | CompressAs | + +------------------+ +------------------+ + | - compression_info()| | - compress_as() | + | - compression_info_mut()| Creates compressed | + | - set_compression_info_none()| representation| + +------------------+ +------------------+ + | | + v v + +------------------+ +------------------+ + | Size | | CompressedInitSpace| + +------------------+ +------------------+ + | - size() | | - INIT_SPACE | + | Serialized size | | Compressed account| + +------------------+ +------------------+ + + + RELATIONSHIP SUMMARY + ==================== + + +-------------------------------------------------------------------+ + | USER'S PROGRAM CODE | + +-------------------------------------------------------------------+ + | | + | // Data struct - apply LightCompressible | + | #[derive(LightCompressible)] | + | #[account] | + | pub struct UserRecord { | + | pub owner: Pubkey, | + | pub score: u64, | + | pub compression_info: Option, <-- Required | + | } | + | | + | // Accounts struct - apply RentFree | + | #[derive(Accounts, RentFree)] | + | #[instruction(params: CreateParams)] | + | pub struct Create<'info> { | + | #[account(init, ...)] | + | #[rentfree] <-- Marks for compression | + | pub user_record: Account<'info, UserRecord>, | + | } | + | | + +-------------------------------------------------------------------+ + | + | At runtime, RentFree uses traits from + | LightCompressible to: + v + +-------------------------------------------------------------------+ + | 1. Hash account data (DataHasher, ToByteArray) | + | 2. Get discriminator (LightDiscriminator) | + | 3. Create compressed representation (CompressAs) | + | 4. Calculate sizes (Size, CompressedInitSpace) | + | 5. Pack Pubkeys to indices (Pack, Unpack) | + | 6. Access compression info (HasCompressionInfo) | + +-------------------------------------------------------------------+ +``` + +### 3.1 HasCompressionInfo + +Provides accessors for the `compression_info` field. + +**Source**: `sdk-libs/macros/src/rentfree/traits/traits.rs` (lines 69-88) + +**Requirements**: Struct must have `compression_info: Option` field. + +**Generated methods**: +- `compression_info(&self) -> &CompressionInfo` +- `compression_info_mut(&mut self) -> &mut CompressionInfo` +- `compression_info_mut_opt(&mut self) -> &mut Option` +- `set_compression_info_none(&mut self)` + +### 3.2 Compressible + +Combined derive that generates: +- `HasCompressionInfo` - Accessor for compression_info field +- `CompressAs` - Creates compressed representation +- `Size` - Calculates serialized size +- `CompressedInitSpace` - INIT_SPACE for compressed accounts + +**Source**: `sdk-libs/macros/src/rentfree/traits/traits.rs` (lines 233-272) + +**Optional attribute** `#[compress_as(field = expr, ...)]`: +- Override field values in compressed representation +- Useful for zeroing out fields that shouldn't be hashed + +```rust +#[derive(Compressible)] +#[compress_as(start_time = 0, cached_value = 0)] +pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, // Will be 0 in compressed form + pub cached_value: u64, // Will be 0 in compressed form + pub compression_info: Option, +} +``` + +**Auto-skipped fields**: +- `compression_info` (always handled specially) +- Fields with `#[skip]` attribute + +#### `#[skip]` - Exclude Fields from Compression + +Mark fields to exclude from `CompressAs` and `Size` calculations: + +```rust +#[derive(Compressible)] +pub struct CachedData { + pub id: u64, + #[skip] // Not included in compressed representation + pub cached_timestamp: u64, + pub compression_info: Option, +} +``` + +### 3.3 Pack/Unpack (CompressiblePack) + +Generates `Pack` and `Unpack` traits with a `Packed{StructName}` struct where Pubkey fields are compressed to u8 indices. + +**Source**: `sdk-libs/macros/src/rentfree/traits/pack_unpack.rs` + +**Input**: +```rust +#[derive(CompressiblePack)] +pub struct UserRecord { + pub owner: Pubkey, + pub authority: Pubkey, + pub score: u64, + pub compression_info: Option, +} +``` + +**Generated**: +```rust +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub owner: u8, // Pubkey -> u8 index + pub authority: u8, // Pubkey -> u8 index + pub score: u64, // Non-Pubkey unchanged + pub compression_info: Option, +} + +impl Pack for UserRecord { + type Packed = PackedUserRecord; + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed { + PackedUserRecord { + owner: remaining_accounts.insert_or_get(self.owner), + authority: remaining_accounts.insert_or_get(self.authority), + score: self.score, + compression_info: None, + } + } +} + +impl Unpack for PackedUserRecord { + type Unpacked = UserRecord; + fn unpack(&self, remaining_accounts: &[AccountInfo]) -> Result { + Ok(UserRecord { + owner: *remaining_accounts[self.owner as usize].key, + authority: *remaining_accounts[self.authority as usize].key, + score: self.score, + compression_info: None, + }) + } +} +``` + +**No Pubkey fields**: If struct has no Pubkey fields, generates identity implementations: +```rust +pub type PackedUserRecord = UserRecord; // Type alias +// Pack::pack returns self.clone() +// Unpack::unpack returns self.clone() +``` + +### 3.4 LightCompressible + +Convenience derive that combines all traits needed for a compressible account. + +**Source**: `sdk-libs/macros/src/rentfree/traits/light_compressible.rs` + +**Equivalent to**: +```rust +#[derive(LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +``` + +**Generated traits**: +- `DataHasher` + `ToByteArray` (SHA256 hashing via LightHasherSha) +- `LightDiscriminator` (unique 8-byte discriminator) +- `HasCompressionInfo` + `CompressAs` + `Size` + `CompressedInitSpace` (via Compressible) +- `Pack` + `Unpack` + `Packed{Name}` struct (via CompressiblePack) + +**Usage**: +```rust +#[derive(Default, Debug, InitSpace, LightCompressible)] +#[account] +pub struct UserRecord { + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub score: u64, + pub compression_info: Option, +} +``` + +**Notes**: +- `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) +- SHA256 hashes the entire struct, so no `#[hash]` attributes needed + +--- + +## 4. Source Code Structure + +``` +sdk-libs/macros/src/rentfree/ +| +|-- mod.rs +| Purpose: Module exports for rentfree macro system +| +|-- shared_utils.rs +| Purpose: Common utilities shared across modules +| Functions: +| - is_constant_identifier(ident: &str) -> bool +| - extract_terminal_ident(expr: &Expr, key_method_only: bool) -> Option +| - is_base_path(expr: &Expr, base: &str) -> bool +| +|-- accounts/ +| |-- mod.rs Entry point, exports derive_rentfree() +| |-- derive.rs Orchestration: parse -> validate -> generate +| |-- builder.rs RentFreeBuilder for code generation +| |-- parse.rs Attribute parsing with darling +| | - ParsedRentFreeStruct +| | - RentFreeField (#[rentfree] data) +| | - InfraFields (auto-detected infrastructure) +| | - InfraFieldClassifier (naming convention matching) +| |-- pda.rs PDA compression block generation +| | - PdaBlockBuilder +| | - generate_pda_compress_blocks() +| +-- light_mint.rs Mint action CPI generation +| - LightMintField (#[light_mint] data) +| - MintActionConfig +| - generate_mint_action_invocation() +| ++-- traits/ + |-- mod.rs Entry point for trait derives + |-- traits.rs Core traits + | - derive_has_compression_info() + | - derive_compress_as() + | - derive_compressible() [combined] + |-- pack_unpack.rs Pack/Unpack trait generation + | - derive_compressible_pack() + |-- light_compressible.rs Combined derive + | - derive_rentfree_account() [LightCompressible] + |-- seed_extraction.rs Anchor seed parsing + | - ClassifiedSeed enum + | - ExtractedSeedSpec, ExtractedTokenSpec + | - extract_anchor_seeds() + | - extract_account_inner_type() + |-- decompress_context.rs Decompression utilities + +-- utils.rs Shared utilities + - extract_fields_from_derive_input() + - is_copy_type(), is_pubkey_type() +``` + +--- + +## 5. Limitations + +### Field Limits +- **Maximum 255 fields**: Total `#[rentfree]` + `#[light_mint]` fields must be <= 255 (u8 index limit) +- **Single mint field**: Currently only the first `#[light_mint]` field is processed + +### Type Restrictions +- `#[rentfree]` only applies to `Account<'info, T>` or `Box>` fields +- Nested `Box>>` is not supported +- `#[rentfree]` and `#[light_mint]` are mutually exclusive on the same field + +### No-op Fallback +When no `#[instruction]` attribute is present, the macro generates no-op implementations for backwards compatibility with non-compressible Accounts structs. + +--- + +## 6. Related Documentation + +- **`sdk-libs/macros/docs/rentfree_program.md`** - Program-level `#[rentfree_program]` attribute macro +- **`sdk-libs/macros/README.md`** - Package overview +- **`sdk-libs/sdk/`** - Runtime SDK with `LightPreInit`, `LightFinalize` trait definitions diff --git a/sdk-libs/macros/docs/rentfree_program.md b/sdk-libs/macros/docs/rentfree_program.md new file mode 100644 index 0000000000..02462a9d95 --- /dev/null +++ b/sdk-libs/macros/docs/rentfree_program.md @@ -0,0 +1,578 @@ +# `#[rentfree_program]` Attribute Macro + +## 1. Overview + +The `#[rentfree_program]` attribute macro provides program-level auto-discovery and instruction wrapping for Light Protocol's rent-free compression system. When applied to an Anchor program module, it: + +1. **Discovers** all `#[rentfree]` and `#[rentfree_token]` fields in `#[derive(Accounts)]` structs across the crate +2. **Auto-wraps** instruction handlers with `light_pre_init`/`light_finalize` lifecycle hooks +3. **Generates** compression/decompression instructions, variant enums, seed structs, and client helper functions + +The macro reads external module files at compile time following Anchor's module resolution pattern, extracting seed information from `#[account(seeds = [...])]` attributes. + +**Location**: `sdk-libs/macros/src/rentfree/program/` + +## 2. Usage + +### Basic Application + +Apply `#[rentfree_program]` before `#[program]` on your Anchor program module: + +```rust +use light_sdk_macros::rentfree_program; + +#[rentfree_program] +#[program] +pub mod my_program { + pub mod instruction_accounts; // Macro reads this file + pub mod state; + + use instruction_accounts::*; + use state::*; + + // No #[light_instruction] needed - automatically wrapped! + pub fn create_user( + ctx: Context, + params: CreateUserParams, + ) -> Result<()> { + // Your business logic + ctx.accounts.user.owner = params.owner; + Ok(()) + } +} +``` + +### Required Attributes on Accounts Structs + +In your instruction accounts module, use `#[rentfree]` for PDA accounts and `#[rentfree_token(authority = [...])]` for token accounts: + +```rust +use anchor_lang::prelude::*; +use light_sdk_macros::RentFree; + +#[derive(Accounts, RentFree)] +#[instruction(params: CreateUserParams)] +pub struct CreateUser<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + #[account( + init, + payer = fee_payer, + space = 8 + UserRecord::INIT_SPACE, + seeds = [ + b"user_record", + authority.key().as_ref(), + params.owner.as_ref(), + params.category_id.to_le_bytes().as_ref() + ], + bump, + )] + #[rentfree] + pub user_record: Account<'info, UserRecord>, + + #[account( + mut, + seeds = [b"vault", cmint.key().as_ref()], + bump, + )] + #[rentfree_token(authority = [b"vault_authority"])] + pub vault: UncheckedAccount<'info>, + + pub system_program: Program<'info, System>, +} +``` + +### Seed Expression Support + +Seeds can reference: +- **Literals**: `b"seed"` or `"seed"` +- **Constants**: `MY_SEED` (uppercase identifiers resolved as `crate::MY_SEED`) +- **Context accounts**: `authority.key().as_ref()` -> extracted as `ctx.accounts.authority` +- **Instruction data**: `params.owner.as_ref()` or `params.category_id.to_le_bytes().as_ref()` +- **Function calls**: `max_key(&fee_payer.key(), &authority.key()).as_ref()` + +## 3. Generated Items + +### 3.1 RentFreeAccountVariant Enum + +A unified enum representing all compressible account types in both packed (serialized) and unpacked forms: + +```rust +#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] +pub enum RentFreeAccountVariant { + // For each #[rentfree] account type + UserRecord { data: UserRecord, authority: Pubkey }, + PackedUserRecord { data: PackedUserRecord, authority_idx: u8 }, + + GameSession { data: GameSession, fee_payer: Pubkey, authority: Pubkey }, + PackedGameSession { data: PackedGameSession, fee_payer_idx: u8, authority_idx: u8 }, + + // Token variants + PackedCTokenData(PackedCTokenData), + CTokenData(CTokenData), +} +``` + +The enum implements: +- `light_hasher::DataHasher` - for computing compressed account hashes +- `light_sdk::LightDiscriminator` - discriminator for account identification +- `light_sdk::compressible::HasCompressionInfo` - compression metadata access +- `light_sdk::compressible::Pack/Unpack` - serialization with account index packing + +### 3.2 Seeds Structs + +For each PDA type, a seeds struct and constructor are generated: + +```rust +#[derive(Clone, Debug)] +pub struct UserRecordSeeds { + pub authority: Pubkey, // from ctx.accounts.authority + pub owner: Pubkey, // from params.owner (data field) + pub category_id: u64, // from params.category_id (data field) +} + +impl RentFreeAccountVariant { + pub fn user_record( + account_data: &[u8], + seeds: UserRecordSeeds, + ) -> Result { + use anchor_lang::AnchorDeserialize; + let data = UserRecord::deserialize(&mut &account_data[..])?; + + // Verify data fields match seeds + if data.owner != seeds.owner { + return Err(RentFreeInstructionError::SeedMismatch.into()); + } + if data.category_id != seeds.category_id { + return Err(RentFreeInstructionError::SeedMismatch.into()); + } + + Ok(Self::UserRecord { + data, + authority: seeds.authority, + }) + } +} + +impl IntoVariant for UserRecordSeeds { + fn into_variant(self, data: &[u8]) -> Result { + RentFreeAccountVariant::user_record(data, self) + } +} +``` + +### 3.3 CtxSeeds Structs + +For PDA seed derivation during decompression, context seed structs hold resolved Pubkeys: + +```rust +#[derive(Default)] +pub struct UserRecordCtxSeeds { + pub authority: Pubkey, +} + +impl PdaSeedDerivation for UserRecord { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &Pubkey, + ctx_seeds: &UserRecordCtxSeeds, + _seed_params: &(), + ) -> Result<(Vec>, Pubkey), ProgramError> { + let seeds: &[&[u8]] = &[ + b"user_record", + ctx_seeds.authority.as_ref(), + self.owner.as_ref(), + self.category_id.to_le_bytes().as_ref(), + ]; + let (pda, bump) = Pubkey::find_program_address(seeds, program_id); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + // ... build seeds_vec with bump + Ok((seeds_vec, pda)) + } +} +``` + +### 3.4 Decompress Instruction + +The `decompress_accounts_idempotent` instruction recreates on-chain PDA accounts from compressed state: + +```rust +#[derive(Accounts)] +pub struct DecompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: anyone can pay + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info>, + /// CHECK: optional - only needed if decompressing tokens + #[account(mut)] + pub ctoken_rent_sponsor: Option>, + /// CHECK: + #[account(address = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] + pub light_token_program: Option>, + /// CHECK: + #[account(address = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] + pub ctoken_cpi_authority: Option>, + /// CHECK: Checked by SDK + pub ctoken_config: Option>, +} + +pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, +) -> Result<()> { + // Delegates to process_decompress_accounts_idempotent +} +``` + +### 3.5 Compress Instruction + +The `compress_accounts_idempotent` instruction compresses on-chain PDA accounts back to compressed state: + +```rust +#[derive(Accounts)] +pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub compression_authority: AccountInfo<'info>, +} + +pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, + proof: ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, +) -> Result<()> { + // Delegates to process_compress_accounts_idempotent +} +``` + +### 3.6 Config Instructions + +Configuration management instructions for the compression system: + +```rust +#[derive(Accounts)] +pub struct InitializeCompressionConfig<'info> { + #[account(mut)] + pub payer: Signer<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + pub program_data: AccountInfo<'info>, + pub authority: Signer<'info>, + pub system_program: Program<'info, System>, +} + +pub fn initialize_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, + write_top_up: u32, + rent_sponsor: Pubkey, + compression_authority: Pubkey, + rent_config: RentConfig, + address_space: Vec, +) -> Result<()>; + +#[derive(Accounts)] +pub struct UpdateCompressionConfig<'info> { + /// CHECK: Checked by SDK + #[account(mut)] + pub config: AccountInfo<'info>, + pub update_authority: Signer<'info>, +} + +pub fn update_compression_config<'info>( + ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, + new_rent_sponsor: Option, + new_compression_authority: Option, + new_rent_config: Option, + new_write_top_up: Option, + new_address_space: Option>, + new_update_authority: Option, +) -> Result<()>; +``` + +### 3.7 Client Seed Functions + +Helper functions for deriving PDAs on the client side: + +```rust +mod __client_seed_functions { + use super::*; + + pub fn get_user_record_seeds( + authority: &Pubkey, + owner: &Pubkey, + category_id: u64, + ) -> (Vec>, Pubkey) { + let mut seed_values = Vec::with_capacity(5); + seed_values.push(b"user_record".to_vec()); + seed_values.push(authority.as_ref().to_vec()); + seed_values.push(owner.as_ref().to_vec()); + seed_values.push(category_id.to_le_bytes().to_vec()); + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); + seed_values.push(vec![bump]); + (seed_values, pda) + } + + // For token accounts + pub fn get_vault_seeds(mint: &Pubkey) -> (Vec>, Pubkey) { ... } + pub fn get_vault_authority_seeds(_program_id: &Pubkey) -> (Vec>, Pubkey) { ... } +} + +pub use __client_seed_functions::*; +``` + +### 3.8 TokenAccountVariant Enum + +For `#[rentfree_token]` fields, packed/unpacked token variant enums are generated: + +```rust +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +pub enum TokenAccountVariant { + Vault { mint: Pubkey }, + UserAta { owner: Pubkey }, +} + +#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] +pub enum PackedTokenAccountVariant { + Vault { mint_idx: u8 }, + UserAta { owner_idx: u8 }, +} + +impl TokenSeedProvider for TokenAccountVariant { + fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { + match self { + TokenAccountVariant::Vault { mint } => { + let seeds: &[&[u8]] = &[b"vault", mint.as_ref()]; + // ... derive PDA + } + // ... + } + } + + fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { + // Returns authority PDA seeds for signing token operations + } +} +``` + +### 3.9 Error Codes + +```rust +#[error_code] +pub enum RentFreeInstructionError { + #[msg("Rent sponsor mismatch")] + InvalidRentSponsor, + #[msg("Missing seed account")] + MissingSeedAccount, + #[msg("Seed value does not match account data")] + SeedMismatch, + #[msg("Not implemented")] + CTokenDecompressionNotImplemented, + #[msg("Not implemented")] + PdaDecompressionNotImplemented, + #[msg("Not implemented")] + TokenCompressionNotImplemented, + #[msg("Not implemented")] + PdaCompressionNotImplemented, +} +``` + +## 4. Code Generation Flow + +``` + #[rentfree_program] + | + v + +-----------------------------+ + | rentfree_program_impl() | + | (instructions.rs:389) | + +-----------------------------+ + | + +-----------------+-----------------+ + | | + v v ++------------------+ +----------------------+ +| CrateContext | | extract_context_and_ | +| ::parse_from_ | | params() + wrap_ | +| manifest() | | function_with_ | +| (crate_context.rs)| | rentfree() | ++------------------+ | (parsing.rs) | + | +----------------------+ + v | ++------------------+ | +| structs_with_ | | +| derive("Accounts")| | ++------------------+ | + | | + v | ++------------------------+ | +| extract_from_accounts_ | | +| struct() | | +| (seed_extraction.rs) | | ++------------------------+ | + | | + v v ++--------------------------------------------------+ +| codegen() | +| (instructions.rs:37) | ++--------------------------------------------------+ + | + +---> validate_compressed_account_sizes() + | (compress.rs) + | + +---> compressed_account_variant_with_ctx_seeds() + | (variant_enum.rs) + | + +---> generate_ctoken_account_variant_enum() + | (variant_enum.rs) + | + +---> generate_decompress_*() + | (decompress.rs) + | + +---> generate_compress_*() + | (compress.rs) + | + +---> generate_pda_seed_provider_impls() + | (decompress.rs) + | + +---> generate_ctoken_seed_provider_implementation() + | (seed_codegen.rs) + | + +---> generate_client_seed_functions() + (seed_codegen.rs) +``` + +## 5. Source Code Structure + +``` +sdk-libs/macros/src/rentfree/program/ +|-- mod.rs # Module exports, main entry point rentfree_program_impl +|-- instructions.rs # Main orchestration: codegen(), rentfree_program_impl() +|-- parsing.rs # Core types (TokenSeedSpec, SeedElement, InstructionDataSpec) +| # Expression analysis, seed conversion, function wrapping +|-- compress.rs # CompressAccountsIdempotent generation +| # CompressContext trait impl, compress processor +|-- decompress.rs # DecompressAccountsIdempotent generation +| # DecompressContext trait impl, PDA seed provider impls +|-- variant_enum.rs # RentFreeAccountVariant enum generation +| # TokenAccountVariant/PackedTokenAccountVariant generation +| # Pack/Unpack trait implementations +|-- seed_codegen.rs # Client seed function generation +| # TokenSeedProvider implementation generation +|-- crate_context.rs # Anchor-style crate parsing (CrateContext, ParsedModule) +| # Module file discovery and parsing +|-- expr_traversal.rs # AST expression transformation (ctx.field -> ctx_seeds.field) +|-- seed_utils.rs # Seed expression conversion utilities +| # SeedConversionConfig, seed_element_to_ref_expr() +|-- visitors.rs # Visitor-based AST traversal (FieldExtractor) +| # ClientSeedInfo classification and code generation +``` + +### Related Files + +``` +sdk-libs/macros/src/rentfree/ +|-- traits/ +| |-- seed_extraction.rs # ClassifiedSeed enum, Anchor seed parsing +| | # extract_from_accounts_struct() +| |-- decompress_context.rs # DecompressContext trait impl generation +| |-- utils.rs # Shared utilities (is_pubkey_type, etc.) +|-- shared_utils.rs # Cross-module utilities (is_constant_identifier, etc.) +``` + +## 6. Key Implementation Details + +### Automatic Function Wrapping + +Functions using `#[rentfree]` Accounts structs are automatically wrapped with lifecycle hooks: + +```rust +// Original: +pub fn create_user(ctx: Context, params: Params) -> Result<()> { + ctx.accounts.user.owner = params.owner; + Ok(()) +} + +// Wrapped (generated): +pub fn create_user(ctx: Context, params: Params) -> Result<()> { + use light_sdk::compressible::{LightPreInit, LightFinalize}; + + // Phase 1: Pre-init (registers compressed addresses) + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, ¶ms)?; + + // Execute original handler + let __light_handler_result = (|| { + ctx.accounts.user.owner = params.owner; + Ok(()) + })(); + + // Phase 2: Finalize compression on success + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms, __has_pre_init)?; + } + + __light_handler_result +} +``` + +### Size Validation + +Compressed accounts are validated at compile time to not exceed 800 bytes: + +```rust +const _: () = { + const COMPRESSED_SIZE: usize = 8 + ::COMPRESSED_INIT_SPACE; + if COMPRESSED_SIZE > 800 { + panic!("Compressed account 'UserRecord' exceeds 800-byte compressible account size limit."); + } +}; +``` + +### Instruction Variants + +The macro supports three instruction variants based on field types: +- `PdaOnly`: Only `#[rentfree]` PDA fields +- `TokenOnly`: Only `#[rentfree_token]` token fields +- `Mixed`: Both PDA and token fields (most common) + +Currently, only `Mixed` variant is fully implemented. `PdaOnly` and `TokenOnly` will error at runtime. + +--- + +## 7. Limitations + +### Compressed Account Size +- Maximum compressed account size is **800 bytes** (discriminator + data) +- Accounts exceeding this limit will fail at compile time with a descriptive error + +### Instruction Variant Support +- `Mixed` (PDA + token fields): Fully implemented +- `PdaOnly`: Returns `unreachable!()` at runtime (not yet implemented) +- `TokenOnly`: Returns `unreachable!()` at runtime (not yet implemented) + +### Crate Discovery +- Requires `CARGO_MANIFEST_DIR` environment variable (set by cargo) +- Module files must follow Anchor's `pub mod name;` pattern for discovery +- Inline `mod name { }` blocks are not discovered + +### Token Authority Requirement +- `#[rentfree_token]` fields must specify `authority = [...]` seeds +- Authority is required for compression signing operations From 947da83977a4cc63b097d2959456c28a2723a5ea Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 18:09:21 +0000 Subject: [PATCH 3/7] refactor light mint, and docs --- sdk-libs/macros/CLAUDE.md | 2 +- sdk-libs/macros/docs/CLAUDE.md | 9 +- sdk-libs/macros/docs/rentfree.md | 2 +- sdk-libs/macros/docs/rentfree_program.md | 578 ------------------ .../docs/rentfree_program/architecture.md | 206 +++++++ .../macros/docs/rentfree_program/codegen.md | 165 +++++ .../macros/src/rentfree/accounts/builder.rs | 92 +-- .../src/rentfree/accounts/light_mint.rs | 395 ++++++------ .../macros/src/rentfree/accounts/parse.rs | 24 +- sdk-libs/macros/src/rentfree/shared_utils.rs | 25 + 10 files changed, 642 insertions(+), 856 deletions(-) delete mode 100644 sdk-libs/macros/docs/rentfree_program.md create mode 100644 sdk-libs/macros/docs/rentfree_program/architecture.md create mode 100644 sdk-libs/macros/docs/rentfree_program/codegen.md diff --git a/sdk-libs/macros/CLAUDE.md b/sdk-libs/macros/CLAUDE.md index abdf8b9ec8..dfb6f9724c 100644 --- a/sdk-libs/macros/CLAUDE.md +++ b/sdk-libs/macros/CLAUDE.md @@ -25,7 +25,7 @@ Detailed macro documentation is in the `docs/` directory: - **`docs/CLAUDE.md`** - Documentation structure guide - **`docs/rentfree.md`** - `#[derive(RentFree)]` and trait derives -- **`docs/rentfree_program.md`** - `#[rentfree_program]` attribute macro +- **`docs/rentfree_program/`** - `#[rentfree_program]` attribute macro (architecture.md + codegen.md) ## Source Structure diff --git a/sdk-libs/macros/docs/CLAUDE.md b/sdk-libs/macros/docs/CLAUDE.md index c1a84a3397..4b5d7b4910 100644 --- a/sdk-libs/macros/docs/CLAUDE.md +++ b/sdk-libs/macros/docs/CLAUDE.md @@ -11,19 +11,22 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros | **`CLAUDE.md`** | This file - documentation structure guide | | **`../CLAUDE.md`** | Main entry point for sdk-libs/macros | | **`rentfree.md`** | `#[derive(RentFree)]` macro and trait derives | -| **`rentfree_program.md`** | `#[rentfree_program]` attribute macro | +| **`rentfree_program/`** | `#[rentfree_program]` attribute macro | +| **`rentfree_program/architecture.md`** | Architecture overview, usage, generated items | +| **`rentfree_program/codegen.md`** | Technical implementation details (code generation) | ## Navigation Tips ### Starting Points - **Building account structs**: Start with `rentfree.md` for the accounts-level derive macro that marks fields for compression -- **Program-level integration**: Use `rentfree_program.md` for program-level auto-discovery and instruction generation +- **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 ### Macro Hierarchy ``` -#[rentfree_program] <- Program-level (rentfree_program.md) +#[rentfree_program] <- Program-level (rentfree_program/) | +-- Discovers #[derive(RentFree)] structs | diff --git a/sdk-libs/macros/docs/rentfree.md b/sdk-libs/macros/docs/rentfree.md index daaa0ee421..2d15f56c03 100644 --- a/sdk-libs/macros/docs/rentfree.md +++ b/sdk-libs/macros/docs/rentfree.md @@ -575,6 +575,6 @@ When no `#[instruction]` attribute is present, the macro generates no-op impleme ## 6. Related Documentation -- **`sdk-libs/macros/docs/rentfree_program.md`** - Program-level `#[rentfree_program]` attribute macro +- **`sdk-libs/macros/docs/rentfree_program/`** - Program-level `#[rentfree_program]` attribute macro (architecture.md + codegen.md) - **`sdk-libs/macros/README.md`** - Package overview - **`sdk-libs/sdk/`** - Runtime SDK with `LightPreInit`, `LightFinalize` trait definitions diff --git a/sdk-libs/macros/docs/rentfree_program.md b/sdk-libs/macros/docs/rentfree_program.md deleted file mode 100644 index 02462a9d95..0000000000 --- a/sdk-libs/macros/docs/rentfree_program.md +++ /dev/null @@ -1,578 +0,0 @@ -# `#[rentfree_program]` Attribute Macro - -## 1. Overview - -The `#[rentfree_program]` attribute macro provides program-level auto-discovery and instruction wrapping for Light Protocol's rent-free compression system. When applied to an Anchor program module, it: - -1. **Discovers** all `#[rentfree]` and `#[rentfree_token]` fields in `#[derive(Accounts)]` structs across the crate -2. **Auto-wraps** instruction handlers with `light_pre_init`/`light_finalize` lifecycle hooks -3. **Generates** compression/decompression instructions, variant enums, seed structs, and client helper functions - -The macro reads external module files at compile time following Anchor's module resolution pattern, extracting seed information from `#[account(seeds = [...])]` attributes. - -**Location**: `sdk-libs/macros/src/rentfree/program/` - -## 2. Usage - -### Basic Application - -Apply `#[rentfree_program]` before `#[program]` on your Anchor program module: - -```rust -use light_sdk_macros::rentfree_program; - -#[rentfree_program] -#[program] -pub mod my_program { - pub mod instruction_accounts; // Macro reads this file - pub mod state; - - use instruction_accounts::*; - use state::*; - - // No #[light_instruction] needed - automatically wrapped! - pub fn create_user( - ctx: Context, - params: CreateUserParams, - ) -> Result<()> { - // Your business logic - ctx.accounts.user.owner = params.owner; - Ok(()) - } -} -``` - -### Required Attributes on Accounts Structs - -In your instruction accounts module, use `#[rentfree]` for PDA accounts and `#[rentfree_token(authority = [...])]` for token accounts: - -```rust -use anchor_lang::prelude::*; -use light_sdk_macros::RentFree; - -#[derive(Accounts, RentFree)] -#[instruction(params: CreateUserParams)] -pub struct CreateUser<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - - pub authority: Signer<'info>, - - #[account( - init, - payer = fee_payer, - space = 8 + UserRecord::INIT_SPACE, - seeds = [ - b"user_record", - authority.key().as_ref(), - params.owner.as_ref(), - params.category_id.to_le_bytes().as_ref() - ], - bump, - )] - #[rentfree] - pub user_record: Account<'info, UserRecord>, - - #[account( - mut, - seeds = [b"vault", cmint.key().as_ref()], - bump, - )] - #[rentfree_token(authority = [b"vault_authority"])] - pub vault: UncheckedAccount<'info>, - - pub system_program: Program<'info, System>, -} -``` - -### Seed Expression Support - -Seeds can reference: -- **Literals**: `b"seed"` or `"seed"` -- **Constants**: `MY_SEED` (uppercase identifiers resolved as `crate::MY_SEED`) -- **Context accounts**: `authority.key().as_ref()` -> extracted as `ctx.accounts.authority` -- **Instruction data**: `params.owner.as_ref()` or `params.category_id.to_le_bytes().as_ref()` -- **Function calls**: `max_key(&fee_payer.key(), &authority.key()).as_ref()` - -## 3. Generated Items - -### 3.1 RentFreeAccountVariant Enum - -A unified enum representing all compressible account types in both packed (serialized) and unpacked forms: - -```rust -#[derive(Clone, Debug, AnchorSerialize, AnchorDeserialize)] -pub enum RentFreeAccountVariant { - // For each #[rentfree] account type - UserRecord { data: UserRecord, authority: Pubkey }, - PackedUserRecord { data: PackedUserRecord, authority_idx: u8 }, - - GameSession { data: GameSession, fee_payer: Pubkey, authority: Pubkey }, - PackedGameSession { data: PackedGameSession, fee_payer_idx: u8, authority_idx: u8 }, - - // Token variants - PackedCTokenData(PackedCTokenData), - CTokenData(CTokenData), -} -``` - -The enum implements: -- `light_hasher::DataHasher` - for computing compressed account hashes -- `light_sdk::LightDiscriminator` - discriminator for account identification -- `light_sdk::compressible::HasCompressionInfo` - compression metadata access -- `light_sdk::compressible::Pack/Unpack` - serialization with account index packing - -### 3.2 Seeds Structs - -For each PDA type, a seeds struct and constructor are generated: - -```rust -#[derive(Clone, Debug)] -pub struct UserRecordSeeds { - pub authority: Pubkey, // from ctx.accounts.authority - pub owner: Pubkey, // from params.owner (data field) - pub category_id: u64, // from params.category_id (data field) -} - -impl RentFreeAccountVariant { - pub fn user_record( - account_data: &[u8], - seeds: UserRecordSeeds, - ) -> Result { - use anchor_lang::AnchorDeserialize; - let data = UserRecord::deserialize(&mut &account_data[..])?; - - // Verify data fields match seeds - if data.owner != seeds.owner { - return Err(RentFreeInstructionError::SeedMismatch.into()); - } - if data.category_id != seeds.category_id { - return Err(RentFreeInstructionError::SeedMismatch.into()); - } - - Ok(Self::UserRecord { - data, - authority: seeds.authority, - }) - } -} - -impl IntoVariant for UserRecordSeeds { - fn into_variant(self, data: &[u8]) -> Result { - RentFreeAccountVariant::user_record(data, self) - } -} -``` - -### 3.3 CtxSeeds Structs - -For PDA seed derivation during decompression, context seed structs hold resolved Pubkeys: - -```rust -#[derive(Default)] -pub struct UserRecordCtxSeeds { - pub authority: Pubkey, -} - -impl PdaSeedDerivation for UserRecord { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &Pubkey, - ctx_seeds: &UserRecordCtxSeeds, - _seed_params: &(), - ) -> Result<(Vec>, Pubkey), ProgramError> { - let seeds: &[&[u8]] = &[ - b"user_record", - ctx_seeds.authority.as_ref(), - self.owner.as_ref(), - self.category_id.to_le_bytes().as_ref(), - ]; - let (pda, bump) = Pubkey::find_program_address(seeds, program_id); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - // ... build seeds_vec with bump - Ok((seeds_vec, pda)) - } -} -``` - -### 3.4 Decompress Instruction - -The `decompress_accounts_idempotent` instruction recreates on-chain PDA accounts from compressed state: - -```rust -#[derive(Accounts)] -pub struct DecompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: anyone can pay - #[account(mut)] - pub rent_sponsor: UncheckedAccount<'info>, - /// CHECK: optional - only needed if decompressing tokens - #[account(mut)] - pub ctoken_rent_sponsor: Option>, - /// CHECK: - #[account(address = pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub light_token_program: Option>, - /// CHECK: - #[account(address = pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option>, - /// CHECK: Checked by SDK - pub ctoken_config: Option>, -} - -pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - // Delegates to process_decompress_accounts_idempotent -} -``` - -### 3.5 Compress Instruction - -The `compress_accounts_idempotent` instruction compresses on-chain PDA accounts back to compressed state: - -```rust -#[derive(Accounts)] -pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, -} - -pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, - proof: ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, -) -> Result<()> { - // Delegates to process_compress_accounts_idempotent -} -``` - -### 3.6 Config Instructions - -Configuration management instructions for the compression system: - -```rust -#[derive(Accounts)] -pub struct InitializeCompressionConfig<'info> { - #[account(mut)] - pub payer: Signer<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - pub program_data: AccountInfo<'info>, - pub authority: Signer<'info>, - pub system_program: Program<'info, System>, -} - -pub fn initialize_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, InitializeCompressionConfig<'info>>, - write_top_up: u32, - rent_sponsor: Pubkey, - compression_authority: Pubkey, - rent_config: RentConfig, - address_space: Vec, -) -> Result<()>; - -#[derive(Accounts)] -pub struct UpdateCompressionConfig<'info> { - /// CHECK: Checked by SDK - #[account(mut)] - pub config: AccountInfo<'info>, - pub update_authority: Signer<'info>, -} - -pub fn update_compression_config<'info>( - ctx: Context<'_, '_, '_, 'info, UpdateCompressionConfig<'info>>, - new_rent_sponsor: Option, - new_compression_authority: Option, - new_rent_config: Option, - new_write_top_up: Option, - new_address_space: Option>, - new_update_authority: Option, -) -> Result<()>; -``` - -### 3.7 Client Seed Functions - -Helper functions for deriving PDAs on the client side: - -```rust -mod __client_seed_functions { - use super::*; - - pub fn get_user_record_seeds( - authority: &Pubkey, - owner: &Pubkey, - category_id: u64, - ) -> (Vec>, Pubkey) { - let mut seed_values = Vec::with_capacity(5); - seed_values.push(b"user_record".to_vec()); - seed_values.push(authority.as_ref().to_vec()); - seed_values.push(owner.as_ref().to_vec()); - seed_values.push(category_id.to_le_bytes().to_vec()); - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(vec![bump]); - (seed_values, pda) - } - - // For token accounts - pub fn get_vault_seeds(mint: &Pubkey) -> (Vec>, Pubkey) { ... } - pub fn get_vault_authority_seeds(_program_id: &Pubkey) -> (Vec>, Pubkey) { ... } -} - -pub use __client_seed_functions::*; -``` - -### 3.8 TokenAccountVariant Enum - -For `#[rentfree_token]` fields, packed/unpacked token variant enums are generated: - -```rust -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -pub enum TokenAccountVariant { - Vault { mint: Pubkey }, - UserAta { owner: Pubkey }, -} - -#[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] -pub enum PackedTokenAccountVariant { - Vault { mint_idx: u8 }, - UserAta { owner_idx: u8 }, -} - -impl TokenSeedProvider for TokenAccountVariant { - fn get_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { - match self { - TokenAccountVariant::Vault { mint } => { - let seeds: &[&[u8]] = &[b"vault", mint.as_ref()]; - // ... derive PDA - } - // ... - } - } - - fn get_authority_seeds(&self, program_id: &Pubkey) -> Result<(Vec>, Pubkey), ProgramError> { - // Returns authority PDA seeds for signing token operations - } -} -``` - -### 3.9 Error Codes - -```rust -#[error_code] -pub enum RentFreeInstructionError { - #[msg("Rent sponsor mismatch")] - InvalidRentSponsor, - #[msg("Missing seed account")] - MissingSeedAccount, - #[msg("Seed value does not match account data")] - SeedMismatch, - #[msg("Not implemented")] - CTokenDecompressionNotImplemented, - #[msg("Not implemented")] - PdaDecompressionNotImplemented, - #[msg("Not implemented")] - TokenCompressionNotImplemented, - #[msg("Not implemented")] - PdaCompressionNotImplemented, -} -``` - -## 4. Code Generation Flow - -``` - #[rentfree_program] - | - v - +-----------------------------+ - | rentfree_program_impl() | - | (instructions.rs:389) | - +-----------------------------+ - | - +-----------------+-----------------+ - | | - v v -+------------------+ +----------------------+ -| CrateContext | | extract_context_and_ | -| ::parse_from_ | | params() + wrap_ | -| manifest() | | function_with_ | -| (crate_context.rs)| | rentfree() | -+------------------+ | (parsing.rs) | - | +----------------------+ - v | -+------------------+ | -| structs_with_ | | -| derive("Accounts")| | -+------------------+ | - | | - v | -+------------------------+ | -| extract_from_accounts_ | | -| struct() | | -| (seed_extraction.rs) | | -+------------------------+ | - | | - v v -+--------------------------------------------------+ -| codegen() | -| (instructions.rs:37) | -+--------------------------------------------------+ - | - +---> validate_compressed_account_sizes() - | (compress.rs) - | - +---> compressed_account_variant_with_ctx_seeds() - | (variant_enum.rs) - | - +---> generate_ctoken_account_variant_enum() - | (variant_enum.rs) - | - +---> generate_decompress_*() - | (decompress.rs) - | - +---> generate_compress_*() - | (compress.rs) - | - +---> generate_pda_seed_provider_impls() - | (decompress.rs) - | - +---> generate_ctoken_seed_provider_implementation() - | (seed_codegen.rs) - | - +---> generate_client_seed_functions() - (seed_codegen.rs) -``` - -## 5. Source Code Structure - -``` -sdk-libs/macros/src/rentfree/program/ -|-- mod.rs # Module exports, main entry point rentfree_program_impl -|-- instructions.rs # Main orchestration: codegen(), rentfree_program_impl() -|-- parsing.rs # Core types (TokenSeedSpec, SeedElement, InstructionDataSpec) -| # Expression analysis, seed conversion, function wrapping -|-- compress.rs # CompressAccountsIdempotent generation -| # CompressContext trait impl, compress processor -|-- decompress.rs # DecompressAccountsIdempotent generation -| # DecompressContext trait impl, PDA seed provider impls -|-- variant_enum.rs # RentFreeAccountVariant enum generation -| # TokenAccountVariant/PackedTokenAccountVariant generation -| # Pack/Unpack trait implementations -|-- seed_codegen.rs # Client seed function generation -| # TokenSeedProvider implementation generation -|-- crate_context.rs # Anchor-style crate parsing (CrateContext, ParsedModule) -| # Module file discovery and parsing -|-- expr_traversal.rs # AST expression transformation (ctx.field -> ctx_seeds.field) -|-- seed_utils.rs # Seed expression conversion utilities -| # SeedConversionConfig, seed_element_to_ref_expr() -|-- visitors.rs # Visitor-based AST traversal (FieldExtractor) -| # ClientSeedInfo classification and code generation -``` - -### Related Files - -``` -sdk-libs/macros/src/rentfree/ -|-- traits/ -| |-- seed_extraction.rs # ClassifiedSeed enum, Anchor seed parsing -| | # extract_from_accounts_struct() -| |-- decompress_context.rs # DecompressContext trait impl generation -| |-- utils.rs # Shared utilities (is_pubkey_type, etc.) -|-- shared_utils.rs # Cross-module utilities (is_constant_identifier, etc.) -``` - -## 6. Key Implementation Details - -### Automatic Function Wrapping - -Functions using `#[rentfree]` Accounts structs are automatically wrapped with lifecycle hooks: - -```rust -// Original: -pub fn create_user(ctx: Context, params: Params) -> Result<()> { - ctx.accounts.user.owner = params.owner; - Ok(()) -} - -// Wrapped (generated): -pub fn create_user(ctx: Context, params: Params) -> Result<()> { - use light_sdk::compressible::{LightPreInit, LightFinalize}; - - // Phase 1: Pre-init (registers compressed addresses) - let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, ¶ms)?; - - // Execute original handler - let __light_handler_result = (|| { - ctx.accounts.user.owner = params.owner; - Ok(()) - })(); - - // Phase 2: Finalize compression on success - if __light_handler_result.is_ok() { - ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms, __has_pre_init)?; - } - - __light_handler_result -} -``` - -### Size Validation - -Compressed accounts are validated at compile time to not exceed 800 bytes: - -```rust -const _: () = { - const COMPRESSED_SIZE: usize = 8 + ::COMPRESSED_INIT_SPACE; - if COMPRESSED_SIZE > 800 { - panic!("Compressed account 'UserRecord' exceeds 800-byte compressible account size limit."); - } -}; -``` - -### Instruction Variants - -The macro supports three instruction variants based on field types: -- `PdaOnly`: Only `#[rentfree]` PDA fields -- `TokenOnly`: Only `#[rentfree_token]` token fields -- `Mixed`: Both PDA and token fields (most common) - -Currently, only `Mixed` variant is fully implemented. `PdaOnly` and `TokenOnly` will error at runtime. - ---- - -## 7. Limitations - -### Compressed Account Size -- Maximum compressed account size is **800 bytes** (discriminator + data) -- Accounts exceeding this limit will fail at compile time with a descriptive error - -### Instruction Variant Support -- `Mixed` (PDA + token fields): Fully implemented -- `PdaOnly`: Returns `unreachable!()` at runtime (not yet implemented) -- `TokenOnly`: Returns `unreachable!()` at runtime (not yet implemented) - -### Crate Discovery -- Requires `CARGO_MANIFEST_DIR` environment variable (set by cargo) -- Module files must follow Anchor's `pub mod name;` pattern for discovery -- Inline `mod name { }` blocks are not discovered - -### Token Authority Requirement -- `#[rentfree_token]` fields must specify `authority = [...]` seeds -- Authority is required for compression signing operations diff --git a/sdk-libs/macros/docs/rentfree_program/architecture.md b/sdk-libs/macros/docs/rentfree_program/architecture.md new file mode 100644 index 0000000000..9bed62d7e3 --- /dev/null +++ b/sdk-libs/macros/docs/rentfree_program/architecture.md @@ -0,0 +1,206 @@ +# `#[rentfree_program]` Attribute Macro + +## 1. Overview + +The `#[rentfree_program]` attribute macro provides program-level auto-discovery and instruction wrapping for Light Protocol's rent-free compression system. It eliminates boilerplate by automatically generating compression infrastructure from your existing Anchor code. + +**Location**: `sdk-libs/macros/src/rentfree/program/` + +## 2. Required Macros + +| Location | Macro | Purpose | +|----------|-------|---------| +| Program module | `#[rentfree_program]` | Discovers fields, generates instructions, wraps handlers | +| Accounts struct | `#[derive(RentFree)]` | Generates `LightPreInit`/`LightFinalize` trait impls | +| Account field | `#[rentfree]` | Marks PDA for compression | +| Account field | `#[rentfree_token(authority=[...])]` | Marks token account for compression | +| State struct | `#[derive(LightCompressible)]` | Generates compression traits + `Packed{Type}` | +| State struct | `compression_info: Option` | Required field for compression metadata | + +## 3. How It Works + +### 3.1 High-Level Flow + +``` ++------------------+ +------------------+ +------------------+ +| User Code | --> | Macro at | --> | Generated | +| | | Compile Time | | Code | ++------------------+ +------------------+ +------------------+ +| - Program module | | 1. Parse crate | | - Variant enums | +| - Accounts | | 2. Find #[rent- | | - Seeds structs | +| structs | | free] fields | | - Compress/ | +| - State structs | | 3. Extract seeds | | Decompress ix | +| | | 4. Generate code | | - Wrapped fns | ++------------------+ +------------------+ +------------------+ +``` + +### 3.2 Compile-Time Discovery + +The macro reads your crate at compile time to find compressible accounts: + +``` +#[rentfree_program] +#[program] +pub mod my_program { + pub mod accounts; <-- Macro follows this to accounts.rs + pub mod state; <-- And this to state.rs + ... +} + + | + v + ++----------------------------------------------------------+ +| DISCOVERY | ++----------------------------------------------------------+ +| | +| For each #[derive(Accounts)] struct: | +| | +| 1. Find #[rentfree] fields --> PDA accounts | +| 2. Find #[rentfree_token] fields --> Token accounts | +| 3. Parse #[account(seeds=[...])] --> Seed expressions | +| 4. Parse #[instruction(...)] --> Params type | +| | ++----------------------------------------------------------+ +``` + +### 3.3 Seed Classification + +Seeds from `#[account(seeds = [...])]` are classified by source: + +``` ++----------------------+---------------------------+------------------------+ +| Seed Expression | Classification | Used For | ++----------------------+---------------------------+------------------------+ +| b"literal" | Static bytes | PDA derivation | +| CONSTANT | crate::CONSTANT ref | PDA derivation | +| authority.key() | Context account (Pubkey) | Variant enum field | +| params.owner | Instruction data field | Seeds struct + verify | ++----------------------+---------------------------+------------------------+ +``` + +Context account seeds become fields in the variant enum. Instruction data seeds become fields in the Seeds struct and are verified against account data. + +### 3.4 Code Generation + +``` + GENERATED ARTIFACTS ++------------------------------------------------------------------+ +| | +| RentFreeAccountVariant TokenAccountVariant | +| +------------------------+ +------------------------+ | +| | UserRecord { data, .. }| | Vault { mint } | | +| | PackedUserRecord {...} | | PackedVault { mint_idx}| | +| +------------------------+ +------------------------+ | +| | | | +| v v | +| UserRecordSeeds get_vault_seeds() | +| UserRecordCtxSeeds get_vault_authority_seeds() | +| | ++------------------------------------------------------------------+ +| | +| INSTRUCTIONS | +| +--------------------+ +--------------------+ +--------------+| +| | decompress_ | | compress_ | | init/update_ || +| | accounts_ | | accounts_ | | compression_ || +| | idempotent | | idempotent | | config || +| +--------------------+ +--------------------+ +--------------+| +| | ++------------------------------------------------------------------+ +``` + +### 3.5 Instruction Wrapping + +Original instruction handlers are automatically wrapped with lifecycle hooks: + +``` +ORIGINAL WRAPPED (generated) ++---------------------------+ +----------------------------------+ +| pub fn create_user( | | pub fn create_user( | +| ctx: Context, | -> | ctx: Context, | +| params: Params | | params: Params | +| ) -> Result<()> { | | ) -> Result<()> { | +| // business logic | | // 1. light_pre_init | +| } | | // 2. business logic (closure) | ++---------------------------+ | // 3. light_finalize | + | } | + +----------------------------------+ +``` + +### 3.6 Runtime Flows + +**Create (Compression)** +``` +User calls create_user + | + v +light_pre_init: Register address in Merkle tree + | + v +Business logic: Set account fields + | + v +light_finalize: Complete compression via CPI + | + v +Account exists as compressed state + temporary PDA +``` + +**Decompress (Read/Modify)** +``` +Client fetches compressed account from indexer + | + v +Client calls decompress_accounts_idempotent + | + v +PDA recreated on-chain from compressed state + | + v +User interacts with standard Anchor account +``` + +**Re-Compress (Return to compressed)** +``` +Authority calls compress_accounts_idempotent + | + v +PDA closed, state written to Merkle tree + | + v +Rent returned to sponsor +``` + +## 4. Generated Items Summary + +| Item | Purpose | +|------|---------| +| `RentFreeAccountVariant` | Unified enum for all compressible account types (packed + unpacked) | +| `TokenAccountVariant` | Enum for token account types | +| `{Type}Seeds` | Client-side PDA derivation with seed values | +| `{Type}CtxSeeds` | Decompression context with resolved Pubkeys | +| `decompress_accounts_idempotent` | Recreate PDAs from compressed state | +| `compress_accounts_idempotent` | Compress PDAs back to Merkle tree | +| `initialize_compression_config` | Setup compression config PDA | +| `update_compression_config` | Modify compression config | +| `get_{type}_seeds()` | Client helper functions for PDA derivation | +| `RentFreeInstructionError` | Error codes for compression operations | + +## 5. Seed Expression Support + +Seeds in `#[account(seeds = [...])]` can reference: + +- **Literals**: `b"seed"` or `"seed"` +- **Constants**: `MY_SEED` (resolved as `crate::MY_SEED`) +- **Context accounts**: `authority.key().as_ref()` +- **Instruction data**: `params.owner.as_ref()` or `params.id.to_le_bytes().as_ref()` +- **Function calls**: `max_key(&a.key(), &b.key()).as_ref()` + +## 6. Limitations + +| Limitation | Details | +|------------|---------| +| Max size | 800 bytes per compressed account (compile-time check) | +| Module discovery | Requires `pub mod name;` pattern (not inline `mod name {}`) | +| Instruction variants | Only `Mixed` (PDA + token) fully implemented | +| Token authority | `#[rentfree_token]` requires `authority = [...]` seeds | diff --git a/sdk-libs/macros/docs/rentfree_program/codegen.md b/sdk-libs/macros/docs/rentfree_program/codegen.md new file mode 100644 index 0000000000..a2bf4002ee --- /dev/null +++ b/sdk-libs/macros/docs/rentfree_program/codegen.md @@ -0,0 +1,165 @@ +# `#[rentfree_program]` Code Generation + +Technical implementation details for the `#[rentfree_program]` attribute macro. + +## 1. Source Code Structure + +``` +sdk-libs/macros/src/rentfree/program/ +|-- mod.rs # Module exports, main entry point rentfree_program_impl +|-- instructions.rs # Main orchestration: codegen(), rentfree_program_impl() +|-- parsing.rs # Core types (TokenSeedSpec, SeedElement, InstructionDataSpec) +| # Expression analysis, seed conversion, function wrapping +|-- compress.rs # CompressAccountsIdempotent generation +| # CompressContext trait impl, compress processor +|-- decompress.rs # DecompressAccountsIdempotent generation +| # DecompressContext trait impl, PDA seed provider impls +|-- variant_enum.rs # RentFreeAccountVariant enum generation +| # TokenAccountVariant/PackedTokenAccountVariant generation +| # Pack/Unpack trait implementations +|-- seed_codegen.rs # Client seed function generation +| # TokenSeedProvider implementation generation +|-- crate_context.rs # Anchor-style crate parsing (CrateContext, ParsedModule) +| # Module file discovery and parsing +|-- expr_traversal.rs # AST expression transformation (ctx.field -> ctx_seeds.field) +|-- seed_utils.rs # Seed expression conversion utilities +| # SeedConversionConfig, seed_element_to_ref_expr() +|-- visitors.rs # Visitor-based AST traversal (FieldExtractor) +| # ClientSeedInfo classification and code generation +``` + +### Related Files + +``` +sdk-libs/macros/src/rentfree/ +|-- traits/ +| |-- seed_extraction.rs # ClassifiedSeed enum, Anchor seed parsing +| | # extract_from_accounts_struct() +| |-- decompress_context.rs # DecompressContext trait impl generation +| |-- utils.rs # Shared utilities (is_pubkey_type, etc.) +|-- shared_utils.rs # Cross-module utilities (is_constant_identifier, etc.) +``` + + +## 2. Code Generation Flow + +``` + #[rentfree_program] + | + v + +-----------------------------+ + | rentfree_program_impl() | + | (instructions.rs:389) | + +-----------------------------+ + | + +-----------------+-----------------+ + | | + v v ++------------------+ +----------------------+ +| CrateContext | | extract_context_and_ | +| ::parse_from_ | | params() + wrap_ | +| manifest() | | function_with_ | +| (crate_context.rs)| | rentfree() | ++------------------+ | (parsing.rs) | + | +----------------------+ + v | ++------------------+ | +| structs_with_ | | +| derive("Accounts")| | ++------------------+ | + | | + v | ++------------------------+ | +| extract_from_accounts_ | | +| struct() | | +| (seed_extraction.rs) | | ++------------------------+ | + | | + v v ++--------------------------------------------------+ +| codegen() | +| (instructions.rs:37) | ++--------------------------------------------------+ + | + +---> validate_compressed_account_sizes() + | (compress.rs) + | + +---> compressed_account_variant_with_ctx_seeds() + | (variant_enum.rs) + | + +---> generate_ctoken_account_variant_enum() + | (variant_enum.rs) + | + +---> generate_decompress_*() + | (decompress.rs) + | + +---> generate_compress_*() + | (compress.rs) + | + +---> generate_pda_seed_provider_impls() + | (decompress.rs) + | + +---> generate_ctoken_seed_provider_implementation() + | (seed_codegen.rs) + | + +---> generate_client_seed_functions() + (seed_codegen.rs) +``` + + +## 3. Key Implementation Details + +### Automatic Function Wrapping + +Functions using `#[rentfree]` Accounts structs are automatically wrapped with lifecycle hooks: + +```rust +// Original: +pub fn create_user(ctx: Context, params: Params) -> Result<()> { + ctx.accounts.user.owner = params.owner; + Ok(()) +} + +// Wrapped (generated): +pub fn create_user(ctx: Context, params: Params) -> Result<()> { + use light_sdk::compressible::{LightPreInit, LightFinalize}; + + // Phase 1: Pre-init (registers compressed addresses) + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, ¶ms)?; + + // Execute original handler + let __light_handler_result = (|| { + ctx.accounts.user.owner = params.owner; + Ok(()) + })(); + + // Phase 2: Finalize compression on success + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms, __has_pre_init)?; + } + + __light_handler_result +} +``` + +### Size Validation + +Compressed accounts are validated at compile time to not exceed 800 bytes: + +```rust +const _: () = { + const COMPRESSED_SIZE: usize = 8 + ::COMPRESSED_INIT_SPACE; + if COMPRESSED_SIZE > 800 { + panic!("Compressed account 'UserRecord' exceeds 800-byte compressible account size limit."); + } +}; +``` + +### Instruction Variants + +The macro supports three instruction variants based on field types: +- `PdaOnly`: Only `#[rentfree]` PDA fields +- `TokenOnly`: Only `#[rentfree_token]` token fields +- `Mixed`: Both PDA and token fields (most common) + +Currently, only `Mixed` variant is fully implemented. `PdaOnly` and `TokenOnly` will error at runtime. diff --git a/sdk-libs/macros/src/rentfree/accounts/builder.rs b/sdk-libs/macros/src/rentfree/accounts/builder.rs index 3b85b2fcc7..0c8d5b3184 100644 --- a/sdk-libs/macros/src/rentfree/accounts/builder.rs +++ b/sdk-libs/macros/src/rentfree/accounts/builder.rs @@ -4,66 +4,29 @@ //! providing methods for validation, querying, and code generation. use proc_macro2::TokenStream; -use quote::{format_ident, quote}; +use quote::quote; use syn::DeriveInput; use super::{ - light_mint::{generate_mint_action_invocation, MintActionConfig}, - parse::{InfraFields, ParsedRentFreeStruct}, + light_mint::{InfraRefs, LightMintBuilder}, + parse::ParsedRentFreeStruct, pda::generate_pda_compress_blocks, }; -/// Resolve optional field name to TokenStream, using default if None. -fn resolve_field_name(field: &Option, default: &str) -> TokenStream { - field.as_ref().map(|f| quote! { #f }).unwrap_or_else(|| { - let ident = format_ident!("{}", default); - quote! { #ident } - }) -} - -/// Resolved infrastructure field names as TokenStreams. -struct ResolvedInfraFields { - fee_payer: TokenStream, - compression_config: TokenStream, - ctoken_config: TokenStream, - ctoken_rent_sponsor: TokenStream, - light_token_program: TokenStream, - ctoken_cpi_authority: TokenStream, -} - -impl ResolvedInfraFields { - fn from_infra(infra: &InfraFields) -> Self { - Self { - fee_payer: resolve_field_name(&infra.fee_payer, "fee_payer"), - compression_config: resolve_field_name(&infra.compression_config, "compression_config"), - ctoken_config: resolve_field_name(&infra.ctoken_config, "ctoken_compressible_config"), - ctoken_rent_sponsor: resolve_field_name( - &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", - ), - } - } -} - /// Builder for RentFree derive macro code generation. /// /// Encapsulates parsed struct data and resolved infrastructure fields, /// providing methods for validation, querying, and code generation. pub(super) struct RentFreeBuilder { parsed: ParsedRentFreeStruct, - infra: ResolvedInfraFields, + infra: InfraRefs, } impl RentFreeBuilder { /// Parse a DeriveInput and construct the builder. pub fn parse(input: &DeriveInput) -> Result { let parsed = super::parse::parse_rentfree_struct(input)?; - let infra = ResolvedInfraFields::from_infra(&parsed.infra_fields); + let infra = InfraRefs::from_parsed(&parsed.infra_fields); Ok(Self { parsed, infra }) } @@ -162,25 +125,14 @@ impl RentFreeBuilder { // assigned_account_index for mint is after PDAs let mint_assigned_index = pda_count as u8; - // Infra field references + // 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_invocation(); + + // Infrastructure field references for quote! interpolation let fee_payer = &self.infra.fee_payer; let compression_config = &self.infra.compression_config; - let ctoken_config = &self.infra.ctoken_config; - let ctoken_rent_sponsor = &self.infra.ctoken_rent_sponsor; - let light_token_program = &self.infra.light_token_program; - let ctoken_cpi_authority = &self.infra.ctoken_cpi_authority; - - // Generate mint action invocation with CPI context - let mint_invocation = generate_mint_action_invocation(&MintActionConfig { - mint, - params_ident, - fee_payer, - ctoken_config, - ctoken_rent_sponsor, - light_token_program, - ctoken_cpi_authority, - cpi_context: Some((quote! { #first_pda_output_tree }, mint_assigned_index)), - }); quote! { // Build CPI accounts WITH CPI context for batching @@ -240,29 +192,17 @@ impl RentFreeBuilder { .unwrap() .name; - // Infra field references - let fee_payer = &self.infra.fee_payer; - let ctoken_config = &self.infra.ctoken_config; - let ctoken_rent_sponsor = &self.infra.ctoken_rent_sponsor; - let light_token_program = &self.infra.light_token_program; - let ctoken_cpi_authority = &self.infra.ctoken_cpi_authority; - // 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 - let mint_invocation = generate_mint_action_invocation(&MintActionConfig { - mint, - params_ident, - fee_payer, - ctoken_config, - ctoken_rent_sponsor, - light_token_program, - ctoken_cpi_authority, - cpi_context: None, - }); + let mint_invocation = LightMintBuilder::new(mint, 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) diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index 3def68f52e..62a19f78e9 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -3,34 +3,32 @@ //! This module handles: //! - Parsing of #[light_mint(...)] attributes using darling //! - Code generation for mint_action CPI invocations +//! +//! ## Parsed Attributes +//! +//! Required: `mint_signer`, `authority`, `decimals` +//! Optional: `address_tree_info`, `freeze_authority`, `signer_seeds`, `rent_payment`, `write_top_up` +//! +//! ## Code Generation +//! +//! Two cases for mint_action CPI: +//! - **With CPI context**: Batching mint creation with PDA compression +//! - **Without CPI context**: Mint-only instructions +//! +//! See `CpiContextParts` for what differs between these cases. use darling::FromMeta; use proc_macro2::TokenStream; -use quote::quote; +use quote::{format_ident, quote}; use syn::{Expr, Ident}; +use super::parse::InfraFields; +use crate::rentfree::shared_utils::MetaExpr; + // ============================================================================ // Parsing with darling // ============================================================================ -/// Wrapper for syn::Expr that implements darling's FromMeta trait. -/// Enables darling to parse arbitrary expressions in attributes like -/// `#[light_mint(mint_signer = self.authority)]`. -#[derive(Clone)] -struct MetaExpr(Expr); - -impl FromMeta for MetaExpr { - fn from_expr(expr: &Expr) -> darling::Result { - Ok(MetaExpr(expr.clone())) - } -} - -impl From for Expr { - fn from(meta: MetaExpr) -> Expr { - meta.0 - } -} - /// A field marked with #[light_mint(...)] pub(super) struct LightMintField { /// The field name where #[light_mint] is attached (CMint account) @@ -44,7 +42,7 @@ pub(super) struct LightMintField { /// Address tree info expression pub address_tree_info: Expr, /// Optional freeze authority - pub freeze_authority: Option, + pub freeze_authority: Option, /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) pub signer_seeds: Option, /// Rent payment epochs for decompression (default: 2) @@ -68,9 +66,9 @@ struct LightMintArgs { /// Address tree info expression (defaults to params.create_accounts_proof.address_tree_info) #[darling(default)] address_tree_info: Option, - /// Optional freeze authority + /// Optional freeze authority (field name, e.g., `freeze_authority = freeze_auth`) #[darling(default)] - freeze_authority: Option, + freeze_authority: Option, /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) #[darling(default)] signer_seeds: Option, @@ -105,7 +103,7 @@ pub(super) fn parse_light_mint_attr( authority: args.authority.into(), decimals: args.decimals.into(), address_tree_info, - freeze_authority: args.freeze_authority.map(Into::into), + freeze_authority: args.freeze_authority, signer_seeds: args.signer_seeds.map(Into::into), rent_payment: args.rent_payment.map(Into::into), write_top_up: args.write_top_up.map(Into::into), @@ -119,189 +117,234 @@ pub(super) fn parse_light_mint_attr( // Code Generation // ============================================================================ -/// Default rent payment period in epochs (how long to prepay rent for decompressed accounts). -const DEFAULT_RENT_PAYMENT_EPOCHS: u8 = 2; - -/// Default write top-up in lamports (additional lamports for write operations during decompression). -const DEFAULT_WRITE_TOP_UP_LAMPORTS: u32 = 0; - -/// Generate token stream for signer seeds (explicit or empty default) -fn generate_signer_seeds_tokens(signer_seeds: &Option) -> TokenStream { - if let Some(seeds) = signer_seeds { - quote! { #seeds } - } else { - quote! { &[] as &[&[u8]] } - } +/// Quote an optional expression, using default if None. +fn quote_option_or(opt: &Option, default: TokenStream) -> TokenStream { + opt.as_ref().map(|e| quote! { #e }).unwrap_or(default) } -/// Generate token stream for freeze authority expression -fn generate_freeze_authority_tokens(freeze_authority: &Option) -> TokenStream { - if let Some(freeze_auth) = freeze_authority { - quote! { Some(*self.#freeze_auth.to_account_info().key) } - } else { - quote! { None } - } +/// Resolve optional field name to TokenStream, using default if None. +fn resolve_field_name(field: &Option, default: &str) -> TokenStream { + field.as_ref().map(|f| quote! { #f }).unwrap_or_else(|| { + let ident = format_ident!("{}", default); + quote! { #ident } + }) } -/// Generate token stream for rent payment with default -fn generate_rent_payment_tokens(rent_payment: &Option) -> TokenStream { - if let Some(rent) = rent_payment { - quote! { #rent } - } else { - let default = DEFAULT_RENT_PAYMENT_EPOCHS; - quote! { #default } - } +/// Resolved infrastructure field names as TokenStreams. +/// +/// Single source of truth for infrastructure fields used across code generation. +pub(super) struct InfraRefs { + pub fee_payer: TokenStream, + pub compression_config: TokenStream, + pub ctoken_config: TokenStream, + pub ctoken_rent_sponsor: TokenStream, + pub light_token_program: TokenStream, + pub ctoken_cpi_authority: TokenStream, } -/// Generate token stream for write top-up with default -fn generate_write_top_up_tokens(write_top_up: &Option) -> TokenStream { - if let Some(top_up) = write_top_up { - quote! { #top_up } - } else { - let default = DEFAULT_WRITE_TOP_UP_LAMPORTS; - quote! { #default } +impl InfraRefs { + /// Construct from parsed InfraFields, applying defaults for missing fields. + pub fn from_parsed(infra: &InfraFields) -> Self { + Self { + fee_payer: resolve_field_name(&infra.fee_payer, "fee_payer"), + compression_config: resolve_field_name(&infra.compression_config, "compression_config"), + ctoken_config: resolve_field_name(&infra.ctoken_config, "ctoken_compressible_config"), + ctoken_rent_sponsor: resolve_field_name( + &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", + ), + } } } -/// Builder for mint field expression generation. +/// Parts of generated code that differ based on CPI context presence. +/// +/// - **With CPI context**: Used when batching mint creation with PDA compression. +/// The mint shares output tree with PDAs, uses assigned_account_index for ordering. /// -/// Encapsulates the generation of TokenStreams for optional mint field attributes -/// like signer_seeds, freeze_authority, rent_payment, and write_top_up. -pub(super) struct MintExprBuilder<'a> { - field: &'a LightMintField, +/// - **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<'a> MintExprBuilder<'a> { - pub fn new(field: &'a LightMintField) -> Self { - Self { field } +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 }, + }, + } } +} - /// Generate signer seeds expression (explicit or empty default). - pub fn signer_seeds(&self) -> TokenStream { - generate_signer_seeds_tokens(&self.field.signer_seeds) - } +/// Builder for mint code generation. +/// +/// Usage: +/// ```ignore +/// LightMintBuilder::new(mint, params_ident, &infra) +/// .with_cpi_context(quote! { #first_pda_output_tree }, mint_assigned_index) +/// .generate_invocation() +/// ``` +pub(super) struct LightMintBuilder<'a> { + mint: &'a LightMintField, + params_ident: &'a Ident, + infra: &'a InfraRefs, + cpi_context: Option<(TokenStream, u8)>, +} - /// Generate freeze authority expression (Some or None). - pub fn freeze_authority(&self) -> TokenStream { - generate_freeze_authority_tokens(&self.field.freeze_authority) +impl<'a> LightMintBuilder<'a> { + /// Create builder with required fields. + pub fn new(mint: &'a LightMintField, params_ident: &'a Ident, infra: &'a InfraRefs) -> Self { + Self { + mint, + params_ident, + infra, + cpi_context: None, + } } - /// Generate rent payment expression with default. - pub fn rent_payment(&self) -> TokenStream { - generate_rent_payment_tokens(&self.field.rent_payment) + /// 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)); + self } - /// Generate write top-up expression with default. - pub fn write_top_up(&self) -> TokenStream { - generate_write_top_up_tokens(&self.field.write_top_up) + /// Generate mint_action CPI invocation code. + pub fn generate_invocation(self) -> TokenStream { + generate_mint_invocation(&self) } } -/// Configuration for mint_action CPI generation -pub(super) struct MintActionConfig<'a> { - pub mint: &'a LightMintField, - pub params_ident: &'a syn::Ident, - pub fee_payer: &'a TokenStream, - pub ctoken_config: &'a TokenStream, - pub ctoken_rent_sponsor: &'a TokenStream, - pub light_token_program: &'a TokenStream, - pub ctoken_cpi_authority: &'a TokenStream, - /// CPI context config: (output_tree_index_expr, assigned_account_index) - /// None = no CPI context (mints-only case) - pub cpi_context: Option<(TokenStream, u8)>, -} - -/// Generate mint_action invocation with optional CPI context -pub(super) fn generate_mint_action_invocation(config: &MintActionConfig) -> TokenStream { - let MintActionConfig { - mint, - params_ident, - fee_payer, - ctoken_config, - ctoken_rent_sponsor, - light_token_program, - ctoken_cpi_authority, - cpi_context, - } = config; - +/// Generate mint_action invocation code. +/// +/// 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; + let params_ident = builder.params_ident; + let infra = &builder.infra; + + // 2. Generate optional field expressions + let signer_seeds = quote_option_or(&mint.signer_seeds, quote! { &[] as &[&[u8]] }); + 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; - // Use MintExprBuilder for optional field expressions - let expr_builder = MintExprBuilder::new(mint); - let signer_seeds_tokens = expr_builder.signer_seeds(); - let freeze_authority_tokens = expr_builder.freeze_authority(); - let rent_payment_tokens = expr_builder.rent_payment(); - let write_top_up_tokens = expr_builder.write_top_up(); - - // Queue access differs based on CPI context presence - let queue_access = if cpi_context.is_some() { - quote! { __output_tree_index as usize } - } else { - quote! { __tree_info.address_queue_pubkey_index as usize } - }; - - // CPI context setup block (empty if no CPI context) - let cpi_context_setup = if let Some((output_tree_expr, _)) = cpi_context { - quote! { - let __output_tree_index = #output_tree_expr; - } - } else { - quote! {} - }; - - // CPI context chain method (empty if no CPI context) - let cpi_context_chain = if let Some((output_tree_expr, assigned_idx)) = cpi_context { - 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: #output_tree_expr + 1, - in_queue_index: #output_tree_expr, - out_queue_index: #output_tree_expr, - token_out_queue_index: 0, - assigned_account_index: #assigned_idx, - read_only_address_trees: [0; 4], - }) - } - } else { - quote! {} - }; - - // CPI context on meta_config (only if CPI context present) - let meta_cpi_context = if cpi_context.is_some() { - quote! { meta_config.cpi_context = Some(*cpi_accounts.cpi_context()?.key); } - } else { - quote! {} - }; - - // Use `let mut` only when CPI context needs modification - let instruction_data_binding = if cpi_context.is_some() { - quote! { let mut instruction_data } - } else { - quote! { let instruction_data } - }; - + 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; + + // ------------------------------------------------------------------------- + // 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) + // #signer_seeds - PDA signer seeds (default: &[] as &[&[u8]]) + // + // 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_context_setup + #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 let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() .expect("proof is required for mint creation"); - let __freeze_authority: Option = #freeze_authority_tokens; + 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, @@ -317,17 +360,19 @@ pub(super) fn generate_mint_action_invocation(config: &MintActionConfig) -> Toke extensions: None, }; - #instruction_data_binding = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( + // Step 5: Build compressed instruction data with decompress config + #data_binding = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( __tree_info.root_index, __proof, compressed_mint_data, ) .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { - rent_payment: #rent_payment_tokens, - write_top_up: #write_top_up_tokens, + rent_payment: #rent_payment, + write_top_up: #write_top_up, }) - #cpi_context_chain; + #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, @@ -341,20 +386,23 @@ pub(super) fn generate_mint_action_invocation(config: &MintActionConfig) -> Toke *self.#ctoken_rent_sponsor.to_account_info().key, ); - #meta_cpi_context + #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()); @@ -365,12 +413,9 @@ pub(super) fn generate_mint_action_invocation(config: &MintActionConfig) -> Toke account_infos.push(self.#mint_signer.to_account_info()); account_infos.push(self.#fee_payer.to_account_info()); - let signer_seeds: &[&[u8]] = #signer_seeds_tokens; - if signer_seeds.is_empty() { - anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; - } else { - anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; - } + // Step 10: Invoke CPI with signer seeds + let signer_seeds: &[&[u8]] = #signer_seeds; + anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; } } } diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index 395e3586d2..d3c9357f0e 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -9,8 +9,9 @@ use syn::{ // Import LightMintField and parsing from light_mint module use super::light_mint::{parse_light_mint_attr, LightMintField}; -// Import shared types from seed_extraction module +// Import shared types pub(super) use crate::rentfree::traits::seed_extraction::extract_account_inner_type; +use crate::rentfree::shared_utils::MetaExpr; // ============================================================================ // Infrastructure Field Classification @@ -77,27 +78,6 @@ impl InfraFields { } } -// ============================================================================ -// darling support for parsing Expr from attributes -// ============================================================================ - -/// Wrapper for syn::Expr that implements darling's FromMeta trait. -/// Enables darling to parse arbitrary expressions in attributes. -#[derive(Clone)] -struct MetaExpr(Expr); - -impl FromMeta for MetaExpr { - fn from_expr(expr: &Expr) -> darling::Result { - Ok(MetaExpr(expr.clone())) - } -} - -impl From for Expr { - fn from(meta: MetaExpr) -> Expr { - meta.0 - } -} - /// Parsed representation of a struct with rentfree and light_mint fields. pub(super) struct ParsedRentFreeStruct { pub struct_name: Ident, diff --git a/sdk-libs/macros/src/rentfree/shared_utils.rs b/sdk-libs/macros/src/rentfree/shared_utils.rs index a9550b5adf..2489d65645 100644 --- a/sdk-libs/macros/src/rentfree/shared_utils.rs +++ b/sdk-libs/macros/src/rentfree/shared_utils.rs @@ -3,9 +3,34 @@ //! This module provides common utility functions used across multiple files: //! - Constant identifier detection (SCREAMING_SNAKE_CASE) //! - Expression identifier extraction +//! - MetaExpr for darling attribute parsing +use darling::FromMeta; use syn::{Expr, Ident}; +// ============================================================================ +// darling support for parsing Expr from attributes +// ============================================================================ + +/// Wrapper for syn::Expr that implements darling's FromMeta trait. +/// +/// Enables darling to parse arbitrary expressions in attributes like +/// `#[light_mint(mint_signer = self.authority)]`. +#[derive(Clone)] +pub struct MetaExpr(Expr); + +impl FromMeta for MetaExpr { + fn from_expr(expr: &Expr) -> darling::Result { + Ok(MetaExpr(expr.clone())) + } +} + +impl From for Expr { + fn from(meta: MetaExpr) -> Expr { + meta.0 + } +} + /// Check if an identifier string is a constant (SCREAMING_SNAKE_CASE). /// /// Returns true if the string is non-empty and all characters are uppercase letters, From 07ef51b27e911c1cf63c35229b7938c35c6a9e4b Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 20:08:39 +0000 Subject: [PATCH 4/7] stash tests --- .gitignore | 1 + sdk-libs/macros/docs/rentfree.md | 13 +- .../macros/docs/rentfree_program/codegen.md | 4 +- .../macros/src/rentfree/accounts/parse.rs | 3 +- sdk-libs/macros/src/rentfree/accounts/pda.rs | 5 +- .../macros/src/rentfree/program/compress.rs | 17 +- .../macros/src/rentfree/program/decompress.rs | 42 +- .../src/rentfree/program/instructions.rs | 53 +- .../macros/src/rentfree/program/parsing.rs | 6 + .../src/rentfree/program/seed_codegen.rs | 4 +- .../src/rentfree/program/variant_enum.rs | 162 +- sdk-libs/macros/src/rentfree/shared_utils.rs | 72 +- .../src/rentfree/traits/decompress_context.rs | 29 +- .../src/rentfree/traits/seed_extraction.rs | 27 +- sdk-libs/macros/tests/discriminator.rs | 16 - sdk-libs/macros/tests/hasher.rs | 1536 ----------------- sdk-libs/macros/tests/pda.rs | 77 - .../src/d5_markers.rs | 3 + .../src/instructions/d5_markers/mod.rs | 7 + .../instructions/d5_markers/rentfree_bare.rs | 44 + .../src/instructions/mod.rs | 10 + .../csdk-anchor-full-derived-test/src/lib.rs | 26 +- .../src/state/d1_field_types/all.rs | 36 + .../src/state/d1_field_types/arrays.rs | 18 + .../src/state/d1_field_types/mod.rs | 12 + .../src/state/d1_field_types/multi_pubkey.rs | 20 + .../src/state/d1_field_types/no_pubkey.rs | 19 + .../src/state/d1_field_types/non_copy.rs | 21 + .../state/d1_field_types/option_primitive.rs | 20 + .../src/state/d1_field_types/option_pubkey.rs | 20 + .../src/state/d1_field_types/single_pubkey.rs | 18 + .../src/state/d2_compress_as/absent.rs | 19 + .../src/state/d2_compress_as/all.rs | 28 + .../src/state/d2_compress_as/mod.rs | 9 + .../src/state/d2_compress_as/multiple.rs | 21 + .../src/state/d2_compress_as/option_none.rs | 20 + .../src/state/d2_compress_as/single.rs | 19 + .../src/state/d4_composition/all.rs | 33 + .../src/state/d4_composition/info_last.rs | 18 + .../src/state/d4_composition/large.rs | 26 + .../src/state/d4_composition/minimal.rs | 15 + .../src/state/d4_composition/mod.rs | 8 + .../src/{state.rs => state/mod.rs} | 9 + 43 files changed, 801 insertions(+), 1765 deletions(-) delete mode 100644 sdk-libs/macros/tests/discriminator.rs delete mode 100644 sdk-libs/macros/tests/hasher.rs delete mode 100644 sdk-libs/macros/tests/pda.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/d5_markers.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs rename sdk-tests/csdk-anchor-full-derived-test/src/{state.rs => state/mod.rs} (88%) diff --git a/.gitignore b/.gitignore index 48f4fbd9be..f2564480b7 100644 --- a/.gitignore +++ b/.gitignore @@ -95,3 +95,4 @@ output1.txt **/~/ expand.rs +output.rs diff --git a/sdk-libs/macros/docs/rentfree.md b/sdk-libs/macros/docs/rentfree.md index 2d15f56c03..5b449e92ce 100644 --- a/sdk-libs/macros/docs/rentfree.md +++ b/sdk-libs/macros/docs/rentfree.md @@ -67,7 +67,7 @@ pub struct CreateAccounts<'info> { ``` **Optional arguments**: -- `address_tree_info` - Expression for address tree info (default: `params.create_accounts_proof.address_tree_info`) +- `address_tree_info` - Expression of type `PackedAddressTreeInfo` containing packed tree indices (default: `params.create_accounts_proof.address_tree_info`). Note: If you have an `AddressTreeInfo` with Pubkeys, you must pack it client-side using `pack_address_tree_info()` before passing to the instruction. - `output_tree` - Expression for output tree index (default: `params.create_accounts_proof.output_state_tree_index`) ```rust @@ -512,7 +512,13 @@ sdk-libs/macros/src/rentfree/ | |-- shared_utils.rs | Purpose: Common utilities shared across modules +| Types: +| - MetaExpr - darling wrapper for parsing Expr from attributes | Functions: +| - qualify_type_with_crate(ty: &Type) -> Type - ensures crate:: prefix +| - make_packed_type(ty: &Type) -> Option - creates Packed{Type} path +| - make_packed_variant_name(variant_name: &Ident) -> Ident +| - ident_to_type(ident: &Ident) -> Type | - is_constant_identifier(ident: &str) -> bool | - extract_terminal_ident(expr: &Expr, key_method_only: bool) -> Option | - is_base_path(expr: &Expr, base: &str) -> bool @@ -531,8 +537,9 @@ sdk-libs/macros/src/rentfree/ | | - generate_pda_compress_blocks() | +-- light_mint.rs Mint action CPI generation | - LightMintField (#[light_mint] data) -| - MintActionConfig -| - generate_mint_action_invocation() +| - InfraRefs - resolved infrastructure field references +| - LightMintBuilder - builder pattern for mint CPI generation +| - CpiContextParts - encapsulates CPI context branching logic | +-- traits/ |-- mod.rs Entry point for trait derives diff --git a/sdk-libs/macros/docs/rentfree_program/codegen.md b/sdk-libs/macros/docs/rentfree_program/codegen.md index a2bf4002ee..b7e782e9f6 100644 --- a/sdk-libs/macros/docs/rentfree_program/codegen.md +++ b/sdk-libs/macros/docs/rentfree_program/codegen.md @@ -49,7 +49,7 @@ sdk-libs/macros/src/rentfree/ v +-----------------------------+ | rentfree_program_impl() | - | (instructions.rs:389) | + | (instructions.rs:405) | +-----------------------------+ | +-----------------+-----------------+ @@ -78,7 +78,7 @@ sdk-libs/macros/src/rentfree/ v v +--------------------------------------------------+ | codegen() | -| (instructions.rs:37) | +| (instructions.rs:38) | +--------------------------------------------------+ | +---> validate_compressed_account_sizes() diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index d3c9357f0e..faa31aa648 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -93,7 +93,8 @@ pub(super) struct ParsedRentFreeStruct { pub(super) struct RentFreeField { pub ident: Ident, /// The inner type T from Account<'info, T> or Box> - pub inner_type: Ident, + /// Preserves the full type path (e.g., crate::state::UserRecord). + pub inner_type: Type, pub address_tree_info: Expr, pub output_tree: Expr, /// True if the field is Box>, false if Account diff --git a/sdk-libs/macros/src/rentfree/accounts/pda.rs b/sdk-libs/macros/src/rentfree/accounts/pda.rs index ecae458430..80fb37a10d 100644 --- a/sdk-libs/macros/src/rentfree/accounts/pda.rs +++ b/sdk-libs/macros/src/rentfree/accounts/pda.rs @@ -79,7 +79,10 @@ impl<'a> PdaBlockBuilder<'a> { quote! { let #new_addr_params = { - let tree_info = &#addr_tree_info; + // Explicit type annotation ensures clear error if wrong type is provided. + // Must be PackedAddressTreeInfo (with indices), not AddressTreeInfo (with Pubkeys). + // If you have AddressTreeInfo, pack it client-side using pack_address_tree_info(). + let tree_info: &light_sdk_types::instruction::PackedAddressTreeInfo = &#addr_tree_info; light_compressed_account::instruction_data::data::NewAddressParamsAssignedPacked { seed: #account_key, address_merkle_tree_account_index: tree_info.address_merkle_tree_pubkey_index, diff --git a/sdk-libs/macros/src/rentfree/program/compress.rs b/sdk-libs/macros/src/rentfree/program/compress.rs index 02c180c5b4..7df3703315 100644 --- a/sdk-libs/macros/src/rentfree/program/compress.rs +++ b/sdk-libs/macros/src/rentfree/program/compress.rs @@ -2,18 +2,21 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Ident, Result}; +use syn::{Result, Type}; use super::parsing::InstructionVariant; +use crate::rentfree::shared_utils::qualify_type_with_crate; // ============================================================================= // COMPRESS CONTEXT IMPL // ============================================================================= -pub fn generate_compress_context_impl(account_types: Vec) -> Result { +pub fn generate_compress_context_impl(account_types: Vec) -> Result { let lifetime: syn::Lifetime = syn::parse_quote!('info); - let compress_arms: Vec<_> = account_types.iter().map(|name| { + let compress_arms: Vec<_> = account_types.iter().map(|account_type| { + // Qualify with crate:: to ensure it's accessible from generated code + let name = qualify_type_with_crate(account_type); quote! { d if d == #name::LIGHT_DISCRIMINATOR => { drop(data); @@ -175,14 +178,16 @@ pub fn generate_compress_accounts_struct(variant: InstructionVariant) -> Result< // ============================================================================= #[inline(never)] -pub fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result { +pub fn validate_compressed_account_sizes(account_types: &[Type]) -> Result { let size_checks: Vec<_> = account_types.iter().map(|account_type| { + // Qualify with crate:: to ensure it's accessible from generated code + let qualified_type = qualify_type_with_crate(account_type); quote! { const _: () = { - const COMPRESSED_SIZE: usize = 8 + <#account_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; + const COMPRESSED_SIZE: usize = 8 + <#qualified_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; if COMPRESSED_SIZE > 800 { panic!(concat!( - "Compressed account '", stringify!(#account_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" + "Compressed account '", stringify!(#qualified_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" )); } }; diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index e39768a6c0..c07b337e7b 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -4,6 +4,8 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; +use crate::rentfree::shared_utils::qualify_type_with_crate; + use super::{ expr_traversal::transform_expr_for_ctx_seeds, parsing::{InstructionVariant, SeedElement, TokenSeedSpec}, @@ -228,36 +230,41 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( #[inline(never)] pub fn generate_pda_seed_provider_impls( - account_types: &[Ident], + account_types: &[syn::Type], pda_ctx_seeds: &[PdaCtxSeedInfo], pda_seeds: &Option>, ) -> Result> { let pda_seed_specs = pda_seeds.as_ref().ok_or_else(|| { - super::parsing::macro_error!( - account_types - .first() - .cloned() - .unwrap_or_else(|| syn::Ident::new("unknown", proc_macro2::Span::call_site())), - "No seed specifications provided" - ) + // Use first account type for error span, or create a dummy span + let span_source = account_types + .first() + .map(|t| quote::quote!(#t)) + .unwrap_or_else(|| quote::quote!(unknown)); + super::parsing::macro_error!(span_source, "No seed specifications provided") })?; - let mut results = Vec::with_capacity(account_types.len()); + let mut results = Vec::with_capacity(pda_ctx_seeds.len()); - for (name, ctx_info) in account_types.iter().zip(pda_ctx_seeds.iter()) { - let name_str = name.to_string(); + // Iterate over pda_ctx_seeds which has both variant_name and inner_type + for ctx_info in pda_ctx_seeds.iter() { + // Match spec by variant_name (field name based) + let variant_str = ctx_info.variant_name.to_string(); let spec = pda_seed_specs .iter() - .find(|s| s.variant == name_str) + .find(|s| s.variant.to_string() == variant_str) .ok_or_else(|| { super::parsing::macro_error!( - name, - "No seed specification for account type '{}'", - name_str + &ctx_info.variant_name, + "No seed specification for variant '{}'", + variant_str ) })?; - let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); + // Use variant_name for struct naming (e.g., RecordCtxSeeds) + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", ctx_info.variant_name); + // Use inner_type for the impl (e.g., impl ... for crate::SinglePubkeyRecord) + // Qualify with crate:: to ensure it's accessible from generated code + let inner_type = qualify_type_with_crate(&ctx_info.inner_type); let ctx_fields = &ctx_info.ctx_seed_fields; let ctx_fields_decl: Vec<_> = ctx_fields .iter() @@ -283,10 +290,11 @@ pub fn generate_pda_seed_provider_impls( let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, ctx_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 #name { + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #inner_type { fn derive_pda_seeds_with_accounts( &self, program_id: &solana_pubkey::Pubkey, diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 704635cc50..cf76cef80a 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Item, ItemMod, Result}; +use syn::{Item, ItemMod, Result, Type}; // Re-export types from parsing for external use pub use super::parsing::{ @@ -26,6 +26,7 @@ use super::{ }, variant_enum::PdaCtxSeedInfo, }; +use crate::rentfree::shared_utils::{ident_to_type, qualify_type_with_crate}; use crate::utils::to_snake_case; // ============================================================================= @@ -36,7 +37,7 @@ use crate::utils::to_snake_case; #[inline(never)] fn codegen( module: &mut ItemMod, - account_types: Vec, + account_types: Vec, pda_seeds: Option>, token_seeds: Option>, instruction_data: Vec, @@ -44,6 +45,14 @@ fn codegen( let size_validation_checks = validate_compressed_account_sizes(&account_types)?; let content = module.content.as_mut().unwrap(); + + // Insert anchor_lang::prelude::* import at the beginning of the module + // This ensures Accounts, Signer, AccountInfo, Result, error_code etc. are in scope + // for the generated code (structs, enums, functions). + let anchor_import: syn::Item = syn::parse_quote! { + use anchor_lang::prelude::*; + }; + content.1.insert(0, anchor_import); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { super::variant_enum::generate_ctoken_account_variant_enum(token_seed_specs)? @@ -73,15 +82,18 @@ fn codegen( .iter() .map(|spec| { let ctx_fields = extract_ctx_seed_fields(&spec.seeds); - PdaCtxSeedInfo::new(spec.variant.clone(), ctx_fields) + // Use inner_type if available (from #[rentfree] fields), otherwise fall back to variant as type + let inner_type = spec + .inner_type + .clone() + .unwrap_or_else(|| ident_to_type(&spec.variant)); + PdaCtxSeedInfo::new(spec.variant.clone(), inner_type, ctx_fields) }) .collect() }) .unwrap_or_default(); - let account_type_refs: Vec<&Ident> = account_types.iter().collect(); let enum_and_traits = super::variant_enum::compressed_account_variant_with_ctx_seeds( - &account_type_refs, &pda_ctx_seeds, )?; @@ -102,9 +114,12 @@ fn codegen( .iter() .zip(pda_ctx_seeds.iter()) .map(|(spec, ctx_info)| { - let type_name = &spec.variant; - let seeds_struct_name = format_ident!("{}Seeds", type_name); - let constructor_name = format_ident!("{}", to_snake_case(&type_name.to_string())); + // Use variant_name for naming (struct, constructor, enum variant) + let variant_name = &ctx_info.variant_name; + // Use inner_type for deserialization - qualify with crate:: for accessibility + let inner_type = qualify_type_with_crate(&ctx_info.inner_type); + 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 ctx_field_decls: Vec<_> = ctx_fields.iter().map(|field| { quote! { pub #field: solana_pubkey::Pubkey } @@ -135,11 +150,13 @@ fn codegen( seeds: #seeds_struct_name, ) -> std::result::Result { use anchor_lang::AnchorDeserialize; - let data = #type_name::deserialize(&mut &account_data[..])?; + // Deserialize using inner_type + let data = #inner_type::deserialize(&mut &account_data[..])?; #(#data_verifications)* - std::result::Result::Ok(Self::#type_name { + // Use variant_name for the enum variant + std::result::Result::Ok(Self::#variant_name { data, #(#ctx_fields: seeds.#ctx_fields,)* }) @@ -293,7 +310,6 @@ fn codegen( }; let client_functions = super::seed_codegen::generate_client_seed_functions( - &account_types, &pda_seeds, &token_seeds, &instruction_data, @@ -439,11 +455,20 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< } // Convert extracted specs to the format expected by codegen + // Deduplicate based on variant_name (field name) - field names must be globally unique let mut found_pda_seeds: Vec = Vec::new(); let mut found_data_fields: Vec = Vec::new(); - let mut account_types: Vec = Vec::new(); + let mut account_types: Vec = Vec::new(); + let mut seen_variants: std::collections::HashSet = std::collections::HashSet::new(); for pda in &pda_specs { + // Deduplicate based on variant_name (derived from field name) + // If same field name is used in multiple instruction structs, only add once + let variant_str = pda.variant_name.to_string(); + if !seen_variants.insert(variant_str) { + continue; // Skip duplicate field names + } + account_types.push(pda.inner_type.clone()); let seed_elements = convert_classified_to_seed_elements(&pda.seeds); @@ -465,11 +490,14 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< } found_pda_seeds.push(TokenSeedSpec { + // Use variant_name (from field name) for enum variant naming variant: pda.variant_name.clone(), _eq: syn::parse_quote!(=), is_token: Some(false), seeds: seed_elements, authority: None, + // Store inner_type for type references (deserialization, trait bounds) + inner_type: Some(pda.inner_type.clone()), }); } @@ -488,6 +516,7 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< is_token: Some(true), seeds: seed_elements, authority: authority_elements, + inner_type: None, // Token specs don't have inner type }); } diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index 6100fa3d95..2a3cfe033e 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -53,11 +53,16 @@ pub enum InstructionVariant { #[derive(Clone)] pub struct TokenSeedSpec { + /// The variant name (derived from field name, used for enum variant naming) pub variant: Ident, pub _eq: Token![=], pub is_token: Option, pub seeds: Punctuated, pub authority: Option>, + /// The inner type (e.g., crate::state::SinglePubkeyRecord - used for type references) + /// Preserves the full type path for code generation. + /// Only set for PDAs extracted from #[rentfree] fields; None for parsed specs + pub inner_type: Option, } impl Parse for TokenSeedSpec { @@ -141,6 +146,7 @@ impl Parse for TokenSeedSpec { is_token, seeds, authority, + inner_type: None, // Set by caller for #[rentfree] fields }) } } diff --git a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs index 121a2e73f6..10dc4451d2 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs @@ -4,7 +4,7 @@ use std::collections::HashSet; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Result}; +use syn::Result; use super::{ instructions::{InstructionDataSpec, TokenSeedSpec}, @@ -109,7 +109,6 @@ pub fn generate_ctoken_seed_provider_implementation( #[inline(never)] pub fn generate_client_seed_functions( - _account_types: &[Ident], pda_seeds: &Option>, token_seeds: &Option>, instruction_data: &[InstructionDataSpec], @@ -165,6 +164,7 @@ pub fn generate_client_seed_functions( is_token: spec.is_token, seeds: syn::punctuated::Punctuated::new(), authority: None, + inner_type: spec.inner_type.clone(), }; for auth_seed in authority_seeds { diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index be3d109e90..18268b4286 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -1,48 +1,56 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Ident, Result}; +use syn::{Ident, Result, Type}; use super::parsing::{SeedElement, TokenSeedSpec}; +use crate::rentfree::shared_utils::{ + make_packed_type, make_packed_variant_name, qualify_type_with_crate, +}; /// Info about ctx.* seeds for a PDA type #[derive(Clone, Debug)] pub struct PdaCtxSeedInfo { - pub type_name: Ident, + /// The variant name (derived from field name, e.g., "Record" from field "record") + pub variant_name: Ident, + /// The inner type (e.g., crate::state::SinglePubkeyRecord - preserves full path) + pub inner_type: Type, /// Field names from ctx.accounts.XXX references in seeds pub ctx_seed_fields: Vec, } impl PdaCtxSeedInfo { - pub fn new(type_name: Ident, ctx_seed_fields: Vec) -> Self { + pub fn new(variant_name: Ident, inner_type: Type, ctx_seed_fields: Vec) -> Self { Self { - type_name, + variant_name, + inner_type, ctx_seed_fields, } } } -/// Enhanced function that generates variants with ctx.* seed fields +/// Enhanced function that generates variants with ctx.* seed fields. +/// Now uses variant_name for enum variant naming and inner_type for type references. pub fn compressed_account_variant_with_ctx_seeds( - account_types: &[&Ident], pda_ctx_seeds: &[PdaCtxSeedInfo], ) -> Result { - if account_types.is_empty() { + if pda_ctx_seeds.is_empty() { return Err(syn::Error::new( proc_macro2::Span::call_site(), "At least one account type must be specified", )); } - // Build a map from type name to ctx seed fields - let ctx_seeds_map: std::collections::HashMap = pda_ctx_seeds - .iter() - .map(|info| (info.type_name.to_string(), info.ctx_seed_fields.as_slice())) - .collect(); - // Phase 2: Generate struct variants with ctx.* seed fields - let account_variants = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + // 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; + // Qualify inner_type with crate:: to ensure it's accessible from generated code + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = make_packed_variant_name(variant_name); + // Create packed type (also qualified with crate::) + 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; // Unpacked variant: Pubkey fields for ctx.* seeds // Note: Use bare Pubkey which is in scope via `use anchor_lang::prelude::*` @@ -57,8 +65,8 @@ pub fn compressed_account_variant_with_ctx_seeds( }); quote! { - #name { data: #name, #(#unpacked_ctx_fields,)* }, - #packed_name { data: #packed_name, #(#packed_ctx_fields,)* }, + #variant_name { data: #inner_type, #(#unpacked_ctx_fields,)* }, + #packed_variant_name { data: #packed_inner_type, #(#packed_ctx_fields,)* }, } }); @@ -73,27 +81,28 @@ pub fn compressed_account_variant_with_ctx_seeds( } }; - let first_type = account_types[0]; - let first_ctx_fields = ctx_seeds_map - .get(&first_type.to_string()) - .copied() - .unwrap_or(&[]); + let first = &pda_ctx_seeds[0]; + 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_default_ctx_fields = first_ctx_fields.iter().map(|field| { quote! { #field: Pubkey::default() } }); let default_impl = quote! { impl Default for RentFreeAccountVariant { fn default() -> Self { - Self::#first_type { data: #first_type::default(), #(#first_default_ctx_fields,)* } + Self::#first_variant { data: #first_type::default(), #(#first_default_ctx_fields,)* } } } }; - let hash_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let hash_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_hasher::DataHasher>::hash::(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_hasher::DataHasher>::hash::(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); @@ -116,35 +125,43 @@ pub fn compressed_account_variant_with_ctx_seeds( } }; - let compression_info_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let compression_info_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); - let compression_info_mut_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let compression_info_mut_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); - let compression_info_mut_opt_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let compression_info_mut_opt_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::compression_info_mut_opt(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); - let set_compression_info_none_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let set_compression_info_none_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::compressible::HasCompressionInfo>::set_compression_info_none(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); @@ -184,11 +201,13 @@ pub fn compressed_account_variant_with_ctx_seeds( } }; - let size_match_arms = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); + let size_match_arms = 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); quote! { - RentFreeAccountVariant::#name { data, .. } => <#name as light_sdk::account::Size>::size(data), - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => <#inner_type as light_sdk::account::Size>::size(data), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), } }); @@ -205,16 +224,18 @@ pub fn compressed_account_variant_with_ctx_seeds( }; // Phase 2: Pack/Unpack with ctx seed fields - let pack_match_arms: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + 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; if ctx_fields.is_empty() { // No ctx seeds - simple pack quote! { - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), - RentFreeAccountVariant::#name { data, .. } => RentFreeAccountVariant::#packed_name { - data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, .. } => RentFreeAccountVariant::#packed_variant_name { + data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts), }, } } else { @@ -228,11 +249,11 @@ pub fn compressed_account_variant_with_ctx_seeds( }).collect(); quote! { - RentFreeAccountVariant::#packed_name { .. } => unreachable!(), - RentFreeAccountVariant::#name { data, #(#field_names,)* .. } => { + RentFreeAccountVariant::#packed_variant_name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { data, #(#field_names,)* .. } => { #(#pack_ctx_seeds)* - RentFreeAccountVariant::#packed_name { - data: <#name as light_sdk::compressible::Pack>::pack(data, remaining_accounts), + RentFreeAccountVariant::#packed_variant_name { + data: <#inner_type as light_sdk::compressible::Pack>::pack(data, remaining_accounts), #(#idx_field_names,)* } }, @@ -257,17 +278,22 @@ pub fn compressed_account_variant_with_ctx_seeds( } }; - let unpack_match_arms: Vec<_> = account_types.iter().map(|name| { - let packed_name = format_ident!("Packed{}", name); - let ctx_fields = ctx_seeds_map.get(&name.to_string()).copied().unwrap_or(&[]); + let unpack_match_arms: Vec<_> = pda_ctx_seeds.iter().map(|info| { + let variant_name = &info.variant_name; + let inner_type = &info.inner_type; + let packed_variant_name = make_packed_variant_name(variant_name); + // Create packed type preserving full path (e.g., crate::module::PackedMyRecord) + 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; if ctx_fields.is_empty() { // No ctx seeds - simple unpack quote! { - RentFreeAccountVariant::#packed_name { data, .. } => Ok(RentFreeAccountVariant::#name { - data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + RentFreeAccountVariant::#packed_variant_name { data, .. } => Ok(RentFreeAccountVariant::#variant_name { + data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, }), - RentFreeAccountVariant::#name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { .. } => unreachable!(), } } else { // Has ctx seeds - unpack data and resolve ctx seed pubkeys from indices @@ -284,14 +310,14 @@ pub fn compressed_account_variant_with_ctx_seeds( }).collect(); quote! { - RentFreeAccountVariant::#packed_name { data, #(#idx_field_names,)* .. } => { + RentFreeAccountVariant::#packed_variant_name { data, #(#idx_field_names,)* .. } => { #(#unpack_ctx_seeds)* - Ok(RentFreeAccountVariant::#name { - data: <#packed_name as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, + Ok(RentFreeAccountVariant::#variant_name { + data: <#packed_inner_type as light_sdk::compressible::Unpack>::unpack(data, remaining_accounts)?, #(#field_names,)* }) }, - RentFreeAccountVariant::#name { .. } => unreachable!(), + RentFreeAccountVariant::#variant_name { .. } => unreachable!(), } } }).collect(); diff --git a/sdk-libs/macros/src/rentfree/shared_utils.rs b/sdk-libs/macros/src/rentfree/shared_utils.rs index 2489d65645..91dde17a28 100644 --- a/sdk-libs/macros/src/rentfree/shared_utils.rs +++ b/sdk-libs/macros/src/rentfree/shared_utils.rs @@ -6,7 +6,77 @@ //! - MetaExpr for darling attribute parsing use darling::FromMeta; -use syn::{Expr, Ident}; +use quote::format_ident; +use syn::{Expr, Ident, Type}; + +// ============================================================================ +// Type path helpers for preserving full type paths in code generation +// ============================================================================ + +/// Ensures a type path is fully qualified with `crate::` prefix. +/// For types that are already qualified (crate::, super::, self::, or absolute ::), +/// returns them unchanged. For bare types like `MyRecord`, returns `crate::MyRecord`. +/// +/// This ensures generated code can reference types regardless of what imports +/// are in scope at the generation site. +pub fn qualify_type_with_crate(ty: &Type) -> Type { + if let Type::Path(type_path) = ty { + // Check if already qualified + if let Some(first_seg) = type_path.path.segments.first() { + let first_str = first_seg.ident.to_string(); + // Already qualified with crate, super, self, or starts with :: + if first_str == "crate" || first_str == "super" || first_str == "self" { + return ty.clone(); + } + } + // Check for absolute path (starts with ::) + if type_path.path.leading_colon.is_some() { + return ty.clone(); + } + + // Prepend crate:: to the path + let mut qualified_path = type_path.clone(); + let crate_segment: syn::PathSegment = syn::parse_quote!(crate); + qualified_path.path.segments.insert(0, crate_segment); + Type::Path(qualified_path) + } else { + ty.clone() + } +} + +/// Creates a packed type path from an original type. +/// For `crate::module::MyRecord` returns `crate::module::PackedMyRecord` +/// For `MyRecord` returns `crate::PackedMyRecord` (qualified and packed) +/// +/// First qualifies the type with `crate::`, then prepends "Packed" to the terminal type name. +pub fn make_packed_type(ty: &Type) -> Option { + // First qualify the type + let qualified = qualify_type_with_crate(ty); + + if let Type::Path(type_path) = &qualified { + let mut packed_path = type_path.clone(); + if let Some(last_seg) = packed_path.path.segments.last_mut() { + let packed_name = format_ident!("Packed{}", last_seg.ident); + last_seg.ident = packed_name; + } + Some(Type::Path(packed_path)) + } else { + None + } +} + +/// Creates a packed variant name (Ident) from a variant name. +/// For `Record` returns `PackedRecord` +pub fn make_packed_variant_name(variant_name: &Ident) -> Ident { + format_ident!("Packed{}", variant_name) +} + +/// Creates a simple type from an identifier (for cases where we only have variant name). +/// Converts `MyRecord` Ident to `MyRecord` Type. +pub fn ident_to_type(ident: &Ident) -> Type { + let path: syn::Path = ident.clone().into(); + Type::Path(syn::TypePath { qself: None, path }) +} // ============================================================================ // darling support for parsing Expr from attributes diff --git a/sdk-libs/macros/src/rentfree/traits/decompress_context.rs b/sdk-libs/macros/src/rentfree/traits/decompress_context.rs index 44690279f5..e7680cb71e 100644 --- a/sdk-libs/macros/src/rentfree/traits/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/traits/decompress_context.rs @@ -6,6 +6,9 @@ use syn::{Ident, Result}; // Re-export from variant_enum for convenience pub use crate::rentfree::program::variant_enum::PdaCtxSeedInfo; +use crate::rentfree::shared_utils::{ + make_packed_type, make_packed_variant_name, qualify_type_with_crate, +}; pub fn generate_decompress_context_trait_impl( pda_ctx_seeds: Vec, @@ -16,9 +19,17 @@ pub fn generate_decompress_context_trait_impl( let pda_match_arms: Vec<_> = pda_ctx_seeds .iter() .map(|info| { - let pda_type = &info.type_name; - let packed_name = format_ident!("Packed{}", pda_type); - let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", pda_type); + // Use variant_name for enum variant matching + let variant_name = &info.variant_name; + // Use inner_type for type references (generics, trait bounds) + // Qualify with crate:: to ensure it's accessible from generated code + let inner_type = qualify_type_with_crate(&info.inner_type); + let packed_variant_name = make_packed_variant_name(variant_name); + // Create packed type (also qualified with crate::) + let packed_inner_type = make_packed_type(&info.inner_type) + .expect("inner_type should be a valid type path"); + // 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; // Generate pattern to extract idx fields from packed variant let idx_field_patterns: Vec<_> = ctx_fields.iter().map(|field| { @@ -46,9 +57,9 @@ pub fn generate_decompress_context_trait_impl( }; if ctx_fields.is_empty() { quote! { - RentFreeAccountVariant::#packed_name { data: packed, .. } => { + RentFreeAccountVariant::#packed_variant_name { data: packed, .. } => { #ctx_seeds_construction - match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + match light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( &*self.rent_sponsor, cpi_accounts, address_space, @@ -66,16 +77,16 @@ pub fn generate_decompress_context_trait_impl( std::result::Result::Err(e) => return std::result::Result::Err(e), } } - RentFreeAccountVariant::#pda_type { .. } => { + RentFreeAccountVariant::#variant_name { .. } => { unreachable!("Unpacked variants should not be present during decompression"); } } } else { quote! { - RentFreeAccountVariant::#packed_name { data: packed, #(#idx_field_patterns,)* .. } => { + RentFreeAccountVariant::#packed_variant_name { data: packed, #(#idx_field_patterns,)* .. } => { #(#resolve_ctx_seeds)* #ctx_seeds_construction - match light_sdk::compressible::handle_packed_pda_variant::<#pda_type, #packed_name, _, _>( + match light_sdk::compressible::handle_packed_pda_variant::<#inner_type, #packed_inner_type, _, _>( &*self.rent_sponsor, cpi_accounts, address_space, @@ -93,7 +104,7 @@ pub fn generate_decompress_context_trait_impl( std::result::Result::Err(e) => return std::result::Result::Err(e), } } - RentFreeAccountVariant::#pda_type { .. } => { + RentFreeAccountVariant::#variant_name { .. } => { unreachable!("Unpacked variants should not be present during decompression"); } } diff --git a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs index 143c7958e7..104b055a50 100644 --- a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs @@ -37,9 +37,13 @@ pub enum ClassifiedSeed { #[derive(Clone, Debug)] pub struct ExtractedSeedSpec { /// The variant name derived from field_name (snake_case -> CamelCase) + /// Note: Currently unused as we use inner_type for seed spec correlation, + /// but kept for potential future use cases (e.g., custom variant naming). + #[allow(dead_code)] pub variant_name: Ident, - /// The inner type (e.g., UserRecord from Account<'info, UserRecord>) - pub inner_type: Ident, + /// The inner type (e.g., crate::state::UserRecord from Account<'info, UserRecord>) + /// Preserves the full type path for code generation. + pub inner_type: Type, /// Classified seeds from #[account(seeds = [...])] pub seeds: Vec, } @@ -300,7 +304,10 @@ fn parse_rentfree_token_list(tokens: &proc_macro2::TokenStream) -> syn::Result, Box>, /// AccountLoader<'info, T>, or InterfaceAccount<'info, T> -pub fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { +/// +/// Returns the full type path (e.g., `crate::module::MyRecord`) to preserve +/// module qualification for code generation. +pub fn extract_account_inner_type(ty: &Type) -> Option<(bool, Type)> { match ty { Type::Path(type_path) => { let segment = type_path.path.segments.last()?; @@ -311,11 +318,15 @@ pub fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { // Extract T from Account<'info, T> if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { for arg in &args.args { - if let syn::GenericArgument::Type(Type::Path(inner_path)) = arg { - if let Some(inner_seg) = inner_path.path.segments.last() { - // Skip lifetime 'info - if inner_seg.ident != "info" { - return Some((false, inner_seg.ident.clone())); + if let syn::GenericArgument::Type(inner_ty) = arg { + // Skip lifetime 'info by checking if this is a path type + if let Type::Path(inner_path) = inner_ty { + if let Some(inner_seg) = inner_path.path.segments.last() { + // Skip lifetime 'info TODO: add a helper that is generalized to strip lifetimes or check whether a crate already has this + if inner_seg.ident != "info" { + // Return the full type, preserving the path + return Some((false, inner_ty.clone())); + } } } } diff --git a/sdk-libs/macros/tests/discriminator.rs b/sdk-libs/macros/tests/discriminator.rs deleted file mode 100644 index a7c70fdfec..0000000000 --- a/sdk-libs/macros/tests/discriminator.rs +++ /dev/null @@ -1,16 +0,0 @@ -use light_account_checks::discriminator::Discriminator as LightDiscriminator; -use light_sdk_macros::LightDiscriminator; - -#[test] -fn test_anchor_discriminator() { - #[cfg(feature = "anchor-discriminator")] - let protocol_config_discriminator = &[96, 176, 239, 146, 1, 254, 99, 146]; - #[cfg(not(feature = "anchor-discriminator"))] - let protocol_config_discriminator = &[254, 235, 147, 47, 205, 77, 97, 201]; - #[derive(LightDiscriminator)] - pub struct ProtocolConfigPda {} - assert_eq!( - protocol_config_discriminator, - &ProtocolConfigPda::LIGHT_DISCRIMINATOR - ); -} diff --git a/sdk-libs/macros/tests/hasher.rs b/sdk-libs/macros/tests/hasher.rs deleted file mode 100644 index 1154569be6..0000000000 --- a/sdk-libs/macros/tests/hasher.rs +++ /dev/null @@ -1,1536 +0,0 @@ -use std::{cell::RefCell, marker::PhantomData, rc::Rc}; - -use borsh::{BorshDeserialize, BorshSerialize}; -use light_compressed_account::hash_to_bn254_field_size_be; -use light_hasher::{to_byte_array::ToByteArray, DataHasher, Hasher, Poseidon, Sha256}; -use light_sdk_macros::LightHasher; -use solana_pubkey::Pubkey; - -#[derive(LightHasher, Clone)] -pub struct MyAccount { - pub a: bool, - pub b: u64, - pub c: MyNestedStruct, - #[hash] - pub d: [u8; 32], - pub f: Option, -} - -#[derive(LightHasher, Clone)] -pub struct TruncateVec { - #[hash] - pub d: Vec, -} - -#[derive(LightHasher, Clone)] -pub struct MyNestedStruct { - pub a: i32, - pub b: u32, - #[hash] - pub c: String, -} - -#[derive(Clone)] -pub struct MyNestedNonHashableStruct { - pub a: PhantomData<()>, - pub b: Rc>, -} - -#[test] -fn test_simple_hash() { - let account = MyAccount { - a: true, - b: 42, - c: MyNestedStruct { - a: 100, - b: 200, - c: "test".to_string(), - }, - d: [1u8; 32], - f: Some(10), - }; - - // Simply test that hashing works - let result = account.hash::(); - assert!(result.is_ok()); - - // Test ToByteArray and to_byte_arrays - let bytes = account.to_byte_array(); - assert!(bytes.is_ok()); -} -// #[cfg(test)] -// mod tests { - -/// LightHasher Tests -/// -/// 1. Basic Hashing (Success): -/// - test_byte_representation: assert_eq! nested struct hash matches manual hash -/// - test_zero_values: assert_eq! zero-value field hash matches manual hash -/// -/// 2. Attribute Behavior: -/// a. HashToFieldSize (Success): -/// - test_array_truncation: assert_ne! between different array hashes -/// - test_truncation_longer_array: assert_ne! between different long string hashes -/// - test_multiple_truncates: assert_ne! between multiple truncated field hashes -/// - test_nested_with_truncate: assert_eq! nested + truncated field hash matches manual hash -/// -/// b. Nested (Success): -/// - test_recursive_nesting: assert_eq! recursive nested struct hash matches manual hash -/// - test_nested_option: assert_eq! Option hash matches manual hash -/// - test_nested_field_count: assert!(is_ok()) with 12 nested fields -/// -/// 3. Error Cases (Failure): -/// - test_empty_struct: assert!(is_err()) on empty struct -/// - test_poseidon_width_limits: assert!(is_err()) with >12 fields -/// - test_max_array_length: assert!(is_err()) on array exceeding max size -/// - test_option_array_error: assert!(is_err()) on Option<[u8;32]> without truncate -/// -/// 4. Option Handling (Success): -/// - test_option_hashing_with_reference_values: assert_eq! against reference hashes -/// - test_basic_option_variants: assert_eq! basic type hashes match manual hash -/// - test_truncated_option_variants: assert_eq! truncated Option hash matches manual hash -/// - test_nested_option_variants: assert_eq! nested Option hash matches manual hash -/// - test_mixed_option_combinations: assert_eq! combined Option hash matches manual hash -/// - test_nested_struct_with_options: assert_eq! nested struct with options hash matches manual hash -/// -/// 5. Option Uniqueness (Success): -/// - test_option_value_uniqueness: assert_ne! between None/Some(0)/Some(1) hashes -/// - test_field_order_uniqueness: assert_ne! between different field orders -/// - test_truncated_option_uniqueness: assert_ne! between None/Some truncated hashes -/// -/// 6. Byte Representation (Success): -/// - test_truncate_byte_representation: assert_eq! truncated bytes match expected -/// - test_byte_representation_combinations: assert_eq! bytes match expected -/// -mod fixtures { - use super::*; - - pub fn create_nested_struct() -> MyNestedStruct { - MyNestedStruct { - a: i32::MIN, - b: u32::MAX, - c: "wao".to_string(), - } - } - - pub fn create_account(f: Option) -> MyAccount { - MyAccount { - a: true, - b: u64::MAX, - c: create_nested_struct(), - d: [u8::MAX; 32], - f, - } - } - - pub fn create_zero_nested() -> MyNestedStruct { - MyNestedStruct { - a: 0, - b: 0, - c: "".to_string(), - } - } -} - -mod basic_hashing { - use super::{fixtures::*, *}; - - #[test] - fn test_byte_representation() { - let nested_struct = create_nested_struct(); - let account = create_account(Some(42)); - - let manual_nested_bytes: Vec> = vec![ - nested_struct.a.to_be_bytes().to_vec(), - nested_struct.b.to_be_bytes().to_vec(), - light_compressed_account::hash_to_bn254_field_size_be( - nested_struct.c.try_to_vec().unwrap().as_slice(), - ) - .to_vec(), - ]; - - let nested_bytes: Vec<&[u8]> = manual_nested_bytes.iter().map(|v| v.as_slice()).collect(); - let manual_nested_hash = Poseidon::hashv(&nested_bytes).unwrap(); - - let nested_reference_hash = [ - 23, 168, 151, 171, 174, 194, 211, 73, 247, 130, 121, 180, 3, 103, 77, 84, 93, 124, 57, - 96, 100, 128, 168, 101, 212, 191, 249, 93, 115, 219, 37, 22, - ]; - let nested_hash_result = nested_struct.hash::().unwrap(); - - assert_eq!(nested_hash_result, manual_nested_hash); - assert_eq!(manual_nested_hash, nested_reference_hash); - assert_eq!(nested_hash_result, manual_nested_hash); - - let manual_account_bytes: Vec> = vec![ - vec![u8::from(account.a)], - account.b.to_be_bytes().to_vec(), - account.c.hash::().unwrap().to_vec(), - light_compressed_account::hash_to_bn254_field_size_be(&account.d).to_vec(), - { - let mut bytes = vec![0; 32]; - bytes[24..].copy_from_slice(&account.f.unwrap().to_be_bytes()); - bytes[23] = 1; // Suffix with 1 for Some - bytes - }, - ]; - - let account_bytes: Vec<&[u8]> = manual_account_bytes.iter().map(|v| v.as_slice()).collect(); - let manual_account_hash = Poseidon::hashv(&account_bytes).unwrap(); - - let account_hash_result = account.hash::().unwrap(); - - assert_eq!(account_hash_result, manual_account_hash); - } - - #[test] - fn test_zero_values() { - let nested = create_zero_nested(); - - let zero_account = MyAccount { - a: false, - b: 0, - c: nested, - d: [0; 32], - f: Some(0), - }; - - let manual_account_bytes = [ - [0u8; 32], - [0u8; 32], - zero_account.c.hash::().unwrap(), - light_compressed_account::hash_to_bn254_field_size_be(&zero_account.d), - { - let mut bytes = [0u8; 32]; - bytes[24..].copy_from_slice(&zero_account.f.unwrap().to_be_bytes()); - bytes[23] = 1; // Suffix with 1 for Some - bytes - }, - ]; - let account_bytes: Vec<&[u8]> = manual_account_bytes.iter().map(|v| v.as_slice()).collect(); - let manual_account_hash = Poseidon::hashv(&account_bytes).unwrap(); - let hash = zero_account.hash::().unwrap(); - assert_eq!(hash, manual_account_hash); - - let expected_hash = [ - 47, 62, 70, 12, 78, 227, 140, 201, 110, 213, 91, 205, 99, 218, 61, 163, 117, 26, 219, - 39, 235, 30, 172, 183, 161, 112, 98, 182, 145, 132, 9, 227, - ]; - assert_eq!(hash, expected_hash); - } -} - -mod attribute_behavior { - use super::{fixtures::*, *}; - - mod truncate { - use super::*; - - #[test] - fn test_array_truncation() { - #[derive(LightHasher)] - struct TruncatedStruct { - #[hash] - data: [u8; 32], - } - - let ones = TruncatedStruct { data: [1u8; 32] }; - let twos = TruncatedStruct { data: [2u8; 32] }; - let mixed = TruncatedStruct { - data: { - let mut data = [1u8; 32]; - data[0] = 2u8; - data - }, - }; - - let ones_hash = ones.hash::().unwrap(); - let twos_hash = twos.hash::().unwrap(); - let mixed_hash = mixed.hash::().unwrap(); - - assert_ne!(ones_hash, twos_hash); - assert_ne!(ones_hash, mixed_hash); - assert_ne!(twos_hash, mixed_hash); - } - - #[test] - fn test_truncation_longer_array() { - #[derive(LightHasher)] - struct LongTruncatedStruct { - #[hash] - data: String, - } - - let large_data = "a".repeat(64); - let truncated = LongTruncatedStruct { - data: large_data.clone(), - }; - - let mut modified_data = large_data.clone(); - modified_data.push('b'); - let truncated2 = LongTruncatedStruct { - data: modified_data, - }; - - let hash1 = truncated.hash::().unwrap(); - let hash2 = truncated2.hash::().unwrap(); - - assert_ne!(hash1, hash2); - } - - #[test] - fn test_multiple_truncates() { - #[derive(LightHasher)] - struct MultiTruncate { - #[hash] - data1: String, - #[hash] - data2: String, - } - - let test_struct = MultiTruncate { - data1: "a".repeat(64), - data2: "b".repeat(64), - }; - - let hash1 = test_struct.hash::().unwrap(); - - let test_struct2 = MultiTruncate { - data1: "a".repeat(65), - data2: "b".repeat(65), - }; - - let hash2 = test_struct2.hash::().unwrap(); - assert_ne!( - hash1, hash2, - "Different data should produce different hashes" - ); - } - - #[test] - fn test_nested_with_truncate() { - #[derive(LightHasher)] - struct NestedTruncate { - inner: MyNestedStruct, - #[hash] - data: String, - } - - let nested = create_nested_struct(); - let test_struct = NestedTruncate { - inner: nested, - data: "test".to_string(), - }; - - let manual_hash = Poseidon::hashv(&[ - &test_struct.inner.hash::().unwrap(), - &light_compressed_account::hash_to_bn254_field_size_be( - test_struct.data.try_to_vec().unwrap().as_slice(), - ), - ]) - .unwrap(); - - let hash = test_struct.hash::().unwrap(); - - // Updated reference hash for BE bytes - let reference_hash = [ - 23, 51, 46, 64, 164, 108, 180, 43, 103, 108, 36, 17, 191, 231, 210, 28, 178, 114, - 188, 37, 143, 15, 165, 109, 154, 241, 33, 210, 172, 108, 10, 33, - ]; - - assert_eq!(hash, manual_hash); - assert_eq!(hash, reference_hash); - } - } - - mod nested { - use super::*; - - #[test] - fn test_recursive_nesting() { - let nested_struct = create_nested_struct(); - - #[derive(LightHasher)] - struct TestNestedStruct { - one: MyNestedStruct, - - two: MyNestedStruct, - } - - let test_nested_struct = TestNestedStruct { - one: nested_struct, - two: create_nested_struct(), - }; - - let manual_hash = Poseidon::hashv(&[ - &test_nested_struct.one.hash::().unwrap(), - &test_nested_struct.two.hash::().unwrap(), - ]) - .unwrap(); - - assert_eq!(test_nested_struct.hash::().unwrap(), manual_hash); - } - - #[test] - fn test_nested_option() { - #[derive(LightHasher)] - struct NestedOption { - opt: Option, - } - - let with_some = NestedOption { - opt: Some(create_nested_struct()), - }; - let with_none = NestedOption { opt: None }; - - let some_bytes = - [ - Poseidon::hash( - &with_some.opt.as_ref().unwrap().hash::().unwrap()[..], - ) - .unwrap(), - ]; - let none_bytes = [[0u8; 32]]; - - assert_eq!(with_some.to_byte_array().unwrap(), some_bytes[0]); - println!("1"); - assert_eq!(with_none.to_byte_array().unwrap(), none_bytes[0]); - println!("1"); - - let some_hash = with_some.hash::().unwrap(); - let none_hash = with_none.hash::().unwrap(); - - assert_ne!(some_hash, none_hash); - } - - #[test] - fn test_nested_field_count() { - #[derive(LightHasher)] - struct InnerMaxFields { - f1: u64, - f2: u64, - f3: u64, - f4: u64, - f5: u64, - f6: u64, - f7: u64, - f8: u64, - f9: u64, - f10: u64, - f11: u64, - f12: u64, - } - - #[derive(LightHasher)] - struct OuterWithNested { - inner: InnerMaxFields, - other: u64, - } - - let inner = InnerMaxFields { - f1: 1, - f2: 2, - f3: 3, - f4: 4, - f5: 5, - f6: 6, - f7: 7, - f8: 8, - f9: 9, - f10: 10, - f11: 11, - f12: 12, - }; - - let outer = OuterWithNested { inner, other: 13 }; - - assert!(outer.hash::().is_ok()); - } - } -} - -#[test] -fn test_empty_struct() { - #[derive(LightHasher)] - struct EmptyStruct {} - - let empty = EmptyStruct {}; - let result = empty.hash::(); - - assert!(result.is_err(), "Empty struct should fail to hash"); -} - -#[test] -fn test_poseidon_width_limits() { - #[derive(LightHasher)] - struct MaxFields { - f1: u64, - f2: u64, - f3: u64, - f4: u64, - f5: u64, - f6: u64, - f7: u64, - f8: u64, - f9: u64, - f10: u64, - f11: u64, - f12: u64, - } - - let max_fields = MaxFields { - f1: 1, - f2: 2, - f3: 3, - f4: 4, - f5: 5, - f6: 6, - f7: 7, - f8: 8, - f9: 9, - f10: 10, - f11: 11, - f12: 12, - }; - - assert!(max_fields.hash::().is_ok()); - let expected_hash = Poseidon::hashv(&[ - 1u64.to_be_bytes().as_ref(), - 2u64.to_be_bytes().as_ref(), - 3u64.to_be_bytes().as_ref(), - 4u64.to_be_bytes().as_ref(), - 5u64.to_be_bytes().as_ref(), - 6u64.to_be_bytes().as_ref(), - 7u64.to_be_bytes().as_ref(), - 8u64.to_be_bytes().as_ref(), - 9u64.to_be_bytes().as_ref(), - 10u64.to_be_bytes().as_ref(), - 11u64.to_be_bytes().as_ref(), - 12u64.to_be_bytes().as_ref(), - ]) - .unwrap(); - assert_eq!(max_fields.hash::().unwrap(), expected_hash); - - // Doesn't compile because it has too many fields. - // #[derive(LightHasher)] - // struct TooManyFields { - // f1: u64, - // f2: u64, - // f3: u64, - // f4: u64, - // f5: u64, - // f6: u64, - // f7: u64, - // f8: u64, - // f9: u64, - // f10: u64, - // f11: u64, - // f12: u64, - // f13: u64, - // } -} - -/// Byte arrays over length 31 bytes need to be truncated or a custom ToByteArray impl. -#[test] -fn test_32_array_length() { - #[derive(LightHasher)] - struct OversizedArray { - #[hash] - data: [u8; 32], - } - - let test_struct = OversizedArray { data: [255u8; 32] }; - let expected_result = - Poseidon::hash(&hash_to_bn254_field_size_be(test_struct.data.as_slice())).unwrap(); - let result = test_struct.hash::().unwrap(); - assert_eq!(result, expected_result); -} - -/// doesn't compile without truncate -#[test] -fn test_option_array() { - #[derive(LightHasher)] - struct OptionArray { - #[hash] - data: Option<[u8; 32]>, - } - - let test_struct = OptionArray { - data: Some([0u8; 32]), - }; - - let result = test_struct.hash::().unwrap(); - assert_ne!(result, [0u8; 32]); - let expected_result = Poseidon::hash(&hash_to_bn254_field_size_be(&[0u8; 32][..])).unwrap(); - assert_eq!(result, expected_result); -} - -mod option_handling { - use super::{fixtures::*, *}; - - #[test] - fn test_option_hashing_with_reference_values() { - let account_none = create_account(None); - let none_hash = account_none.hash::().unwrap(); - - let account_some = create_account(Some(0)); - let some_hash = account_some.hash::().unwrap(); - - // Verify that None and Some(0) have different hashes - assert_ne!( - none_hash, some_hash, - "None and Some(0) should have different hashes" - ); - } - - #[test] - fn test_basic_option_variants() { - #[allow(dead_code)] - #[derive(LightHasher)] - struct BasicOptions { - small: Option, - large: Option, - #[hash] - empty_str: Option, - } - - let test_struct = BasicOptions { - small: Some(42), - large: Some(u64::MAX), - empty_str: Some("".to_string()), - }; - - let none_struct = BasicOptions { - small: None, - large: None, - empty_str: None, - }; - - let manual_bytes = [ - { - let mut bytes = [0u8; 32]; - bytes[28..].copy_from_slice(&42u32.to_be_bytes()); - bytes[27] = 1; // Prefix with 1 for Some - bytes - }, - { - let mut bytes = [0u8; 32]; - bytes[24..].copy_from_slice(&u64::MAX.to_be_bytes()); - bytes[23] = 1; // Prefix with 1 for Some - bytes - }, - light_compressed_account::hash_to_bn254_field_size_be( - "".try_to_vec().unwrap().as_slice(), - ), - ]; - - assert_eq!(test_struct.hash::(), test_struct.to_byte_array()); - let expected_hash = Poseidon::hashv( - &manual_bytes - .iter() - .map(|x| x.as_slice()) - .collect::>(), - ) - .unwrap(); - assert_eq!(test_struct.hash::().unwrap(), expected_hash); - let test_hash = test_struct.hash::(); - assert!(test_hash.is_ok()); - let none_hash = none_struct.hash::().unwrap(); - - // Verify that None and Some produce different hashes - assert_ne!( - test_hash.unwrap(), - none_hash, - "None and Some should have different hashes" - ); - } - - #[test] - fn test_truncated_option_variants() { - #[derive(LightHasher)] - struct TruncatedOptions { - #[hash] - empty_str: Option, - #[hash] - short_str: Option, - #[hash] - long_str: Option, - #[hash] - large_array: Option<[u8; 64]>, - } - - let test_struct = TruncatedOptions { - empty_str: Some("".to_string()), - short_str: Some("test".to_string()), - long_str: Some("a".repeat(100)), - large_array: Some([42u8; 64]), - }; - - let none_struct = TruncatedOptions { - empty_str: None, - short_str: None, - long_str: None, - large_array: None, - }; - - let manual_some_bytes = [ - light_compressed_account::hash_to_bn254_field_size_be( - "".try_to_vec().unwrap().as_slice(), - ), - light_compressed_account::hash_to_bn254_field_size_be( - "test".try_to_vec().unwrap().as_slice(), - ), - light_compressed_account::hash_to_bn254_field_size_be( - "a".repeat(100).try_to_vec().unwrap().as_slice(), - ), - light_compressed_account::hash_to_bn254_field_size_be( - &test_struct.large_array.unwrap(), - ), - ]; - - let test_hash = test_struct.hash::().unwrap(); - let none_hash = none_struct.hash::().unwrap(); - let expeceted_some_hash = Poseidon::hashv( - &manual_some_bytes - .iter() - .map(|x| x.as_slice()) - .collect::>(), - ) - .unwrap(); - let expected_none_hash = - Poseidon::hashv(&[&[0; 32], &[0; 32], &[0; 32], &[0; 32]]).unwrap(); - assert_eq!(test_hash, expeceted_some_hash); - assert_eq!(none_hash, expected_none_hash); - // Updated reference hash for BE bytes - assert_eq!( - test_hash, - [ - 26, 206, 86, 217, 69, 163, 110, 158, 101, 48, 167, 203, 138, 17, 126, 43, 203, 82, - 148, 165, 167, 144, 44, 120, 82, 49, 202, 62, 109, 206, 237, 190 - ] - ); - // Updated reference hash for BE bytes - assert_eq!( - none_hash, - [ - 5, 50, 253, 67, 110, 25, 199, 14, 81, 32, 150, 148, 217, 194, 21, 37, 9, 55, 146, - 27, 139, 121, 6, 4, 136, 193, 32, 109, 183, 62, 153, 70 - ] - ); - } - - #[test] - fn test_nested_option_variants() { - #[derive(LightHasher)] - struct NestedOptions { - empty_struct: Option, - full_struct: Option, - } - - let empty_nested = create_zero_nested(); - let full_nested = create_nested_struct(); - - let test_struct = NestedOptions { - empty_struct: Some(empty_nested), - full_struct: Some(full_nested), - }; - - let none_struct = NestedOptions { - empty_struct: None, - full_struct: None, - }; - - let manual_bytes = [ - Poseidon::hash( - &test_struct - .empty_struct - .as_ref() - .unwrap() - .hash::() - .unwrap(), - ) - .unwrap(), - Poseidon::hash( - &test_struct - .full_struct - .as_ref() - .unwrap() - .hash::() - .unwrap(), - ) - .unwrap(), - ]; - - let expected_hash = - Poseidon::hashv(&manual_bytes.iter().map(|x| x.as_ref()).collect::>()).unwrap(); - assert_eq!(test_struct.hash::().unwrap(), expected_hash); - // Updated reference hash for BE bytes - assert_eq!( - test_struct.hash::().unwrap(), - [ - 38, 207, 53, 149, 51, 139, 156, 60, 155, 207, 232, 222, 177, 238, 31, 130, 136, - 224, 210, 74, 144, 46, 141, 195, 34, 135, 83, 198, 233, 159, 168, 143 - ] - ); - // Updated reference hash for BE bytes - assert_eq!( - none_struct.hash::().unwrap(), - [ - 32, 152, 245, 251, 158, 35, 158, 171, 60, 234, 195, 242, 123, 129, 228, 129, 220, - 49, 36, 213, 95, 254, 213, 35, 168, 57, 238, 132, 70, 182, 72, 100 - ] - ); - } - - #[test] - fn test_mixed_option_combinations() { - #[derive(LightHasher)] - struct MixedOptions { - basic: Option, - #[hash] - truncated_small: Option, - #[hash] - truncated_large: Option<[u8; 64]>, - - nested_empty: Option, - - nested_full: Option, - } - - let test_struct = MixedOptions { - basic: Some(42), - truncated_small: Some("test".to_string()), - truncated_large: Some([42u8; 64]), - nested_empty: Some(MyNestedStruct { - a: 0, - b: 0, - c: "".to_string(), - }), - nested_full: Some(create_nested_struct()), - }; - - let partial_struct = MixedOptions { - basic: Some(42), - truncated_small: None, - truncated_large: Some([42u8; 64]), - nested_empty: None, - nested_full: Some(create_nested_struct()), - }; - - let none_struct = MixedOptions { - basic: None, - truncated_small: None, - truncated_large: None, - nested_empty: None, - nested_full: None, - }; - - let manual_bytes = [ - { - let mut bytes = [0u8; 32]; - bytes[28..].copy_from_slice(&42u32.to_be_bytes()); - bytes[27] = 1; - bytes - }, - light_compressed_account::hash_to_bn254_field_size_be( - "test".try_to_vec().unwrap().as_slice(), - ), - light_compressed_account::hash_to_bn254_field_size_be(&[42u8; 64][..]), - Poseidon::hash( - &test_struct - .nested_empty - .as_ref() - .unwrap() - .hash::() - .unwrap(), - ) - .unwrap(), - Poseidon::hash( - &test_struct - .nested_full - .as_ref() - .unwrap() - .hash::() - .unwrap(), - ) - .unwrap(), - ]; - - let expected_hash = - Poseidon::hashv(&manual_bytes.iter().map(|x| x.as_ref()).collect::>()).unwrap(); - assert_eq!(test_struct.hash::().unwrap(), expected_hash); - assert_eq!( - test_struct.hash::().unwrap(), - [ - 11, 157, 253, 114, 25, 23, 79, 182, 68, 25, 62, 21, 54, 17, 133, 132, 46, 211, 241, - 153, 207, 76, 61, 164, 177, 148, 208, 53, 50, 179, 26, 213 - ] - ); - // Updated reference hash for BE bytes - assert_eq!( - partial_struct.hash::().unwrap(), - [ - 37, 131, 136, 26, 175, 106, 143, 121, 184, 59, 76, 126, 15, 134, 111, 55, 194, 38, - 166, 191, 109, 79, 125, 48, 141, 129, 166, 234, 210, 243, 93, 144 - ] - ); - // Updated reference hash for BE bytes - assert_eq!( - none_struct.hash::().unwrap(), - [ - 32, 102, 190, 65, 190, 190, 108, 175, 126, 7, 147, 96, 171, 225, 79, 191, 145, 24, - 198, 46, 171, 196, 46, 47, 231, 94, 52, 43, 22, 10, 149, 188 - ] - ); - } - - #[test] - fn test_nested_struct_with_options() { - #[derive(LightHasher)] - struct InnerWithOptions { - basic: Option, - #[hash] - truncated: Option, - } - - #[derive(LightHasher)] - struct OuterStruct { - inner: InnerWithOptions, - basic: Option, - } - - let test_struct = OuterStruct { - inner: InnerWithOptions { - basic: Some(42), - truncated: Some("test".to_string()), - }, - basic: Some(u64::MAX), - }; - - let none_struct = OuterStruct { - inner: InnerWithOptions { - basic: None, - truncated: None, - }, - basic: None, - }; - - let manual_bytes = [test_struct.inner.hash::().unwrap(), { - let mut bytes = [0u8; 32]; - bytes[24..].copy_from_slice(&u64::MAX.to_be_bytes()); - bytes[23] = 1; - bytes - }]; - - let expected_hash = Poseidon::hashv( - manual_bytes - .iter() - .map(|x| x.as_slice()) - .collect::>() - .as_slice(), - ) - .unwrap(); - assert_eq!(test_struct.hash::().unwrap(), expected_hash); - assert_eq!( - test_struct.hash::().unwrap(), - [ - 12, 235, 222, 198, 73, 228, 229, 31, 235, 53, 206, 115, 238, 91, 183, 135, 185, - 105, 2, 255, 171, 222, 207, 6, 189, 151, 58, 172, 28, 183, 57, 92 - ] - ); - // Updated reference hash for BE bytes - assert_eq!( - none_struct.hash::().unwrap(), - [ - 23, 83, 82, 87, 94, 164, 86, 13, 119, 230, 225, 21, 182, 59, 41, 174, 42, 2, 191, - 189, 157, 234, 195, 122, 103, 142, 82, 137, 231, 49, 77, 106 - ] - ); - } -} - -mod option_uniqueness { - use super::*; - // TODO: split into multi tests to ensure ne is attributable - #[test] - fn test_option_value_uniqueness() { - #[derive(LightHasher)] - struct OptionTest { - a: Option, - b: Option, - #[hash] - c: Option, - - d: Option, - } - - // Test None vs Some(0) produce different hashes - let none_struct = OptionTest { - a: None, - b: None, - c: None, - d: None, - }; - - let zero_struct = OptionTest { - a: Some(0), - b: Some(0), - c: Some("".to_string()), - d: Some(MyNestedStruct { - a: 0, - b: 0, - c: "".to_string(), - }), - }; - - assert_ne!( - none_struct.hash::().unwrap(), - zero_struct.hash::().unwrap(), - "None should hash differently than Some(0)" - ); - - // Test different Some values produce different hashes - let one_struct = OptionTest { - a: Some(1), - b: Some(1), - c: Some("a".to_string()), - d: Some(MyNestedStruct { - a: 1, - b: 1, - c: "a".to_string(), - }), - }; - - assert_ne!( - zero_struct.hash::().unwrap(), - one_struct.hash::().unwrap(), - "Different Some values should hash differently" - ); - - // Test partial Some/None combinations - let partial_struct = OptionTest { - a: Some(1), - b: None, - c: Some("a".to_string()), - d: None, - }; - - assert_ne!( - none_struct.hash::().unwrap(), - partial_struct.hash::().unwrap(), - "Partial Some/None should hash differently than all None" - ); - assert_ne!( - one_struct.hash::().unwrap(), - partial_struct.hash::().unwrap(), - "Partial Some/None should hash differently than all Some" - ); - } - - #[test] - fn test_field_order_uniqueness() { - // Test that field order matters for options - #[derive(LightHasher)] - struct OrderTestA { - first: Option, - second: Option, - } - - #[derive(LightHasher)] - struct OrderTestB { - first: Option, - second: Option, - } - - let test_a = OrderTestA { - first: Some(1), - second: Some(2), - }; - - let test_b = OrderTestB { - first: Some(2), - second: Some(1), - }; - - assert_ne!( - test_a.hash::().unwrap(), - test_b.hash::().unwrap(), - "Different field order should produce different hashes" - ); - - // Test nested option field order - #[derive(LightHasher)] - struct NestedOrderTestA { - first: Option, - second: Option, - } - - #[derive(LightHasher)] - struct NestedOrderTestB { - first: Option, - - second: Option, - } - - let nested_a = NestedOrderTestA { - first: Some(MyNestedStruct { - a: 1, - b: 2, - c: "test".to_string(), - }), - second: Some(42), - }; - - let nested_b = NestedOrderTestB { - first: Some(42), - second: Some(MyNestedStruct { - a: 1, - b: 2, - c: "test".to_string(), - }), - }; - - assert_ne!( - nested_a.hash::().unwrap(), - nested_b.hash::().unwrap(), - "Different nested field order should produce different hashes" - ); - } - - #[test] - fn test_truncated_option_uniqueness() { - #[derive(LightHasher)] - struct TruncateTest { - #[hash] - a: Option, - #[hash] - b: Option<[u8; 64]>, - } - - // Test truncated None vs empty - let none_struct = TruncateTest { a: None, b: None }; - - let empty_struct = TruncateTest { - a: Some("".to_string()), - b: Some([0u8; 64]), - }; - - assert_ne!( - none_struct.hash::().unwrap(), - empty_struct.hash::().unwrap(), - "Truncated None should hash differently than empty values" - ); - - // Test truncated different values - let value_struct = TruncateTest { - a: Some("test".to_string()), - b: Some([1u8; 64]), - }; - - assert_ne!( - empty_struct.hash::().unwrap(), - value_struct.hash::().unwrap(), - "Different truncated values should hash differently" - ); - - // Test truncated long values - let long_struct = TruncateTest { - a: Some("a".repeat(100)), - b: Some([2u8; 64]), - }; - - assert_ne!( - value_struct.hash::().unwrap(), - long_struct.hash::().unwrap(), - "Different length truncated values should hash differently" - ); - } -} - -#[test] -fn test_solana_program_pubkey() { - // Pubkey field - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: Pubkey, - } - let pubkey_struct = PubkeyStruct { - pubkey: Pubkey::new_unique(), - }; - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(pubkey_struct.pubkey.as_ref()) - .as_slice(), - ) - .unwrap(); - let manual_hash_borsh = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be( - pubkey_struct.pubkey.try_to_vec().unwrap().as_slice(), - ) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - assert_eq!(manual_hash_borsh, hash); - } - // Option - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: Option, - } - // Some - { - let pubkey_struct = PubkeyStruct { - pubkey: Some(Pubkey::new_unique()), - }; - let manual_bytes = pubkey_struct.pubkey.unwrap().try_to_vec().unwrap(); - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - // None - { - let pubkey_struct = PubkeyStruct { pubkey: None }; - let manual_hash = Poseidon::hash([0u8; 32].as_slice()).unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash([0u8; 32].as_slice()).unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - } - // Vec - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: Vec, - } - let pubkey_vec = (0..3).map(|_| Pubkey::new_unique()).collect::>(); - let pubkey_struct = PubkeyStruct { pubkey: pubkey_vec }; - let manual_bytes = pubkey_struct.pubkey.try_to_vec().unwrap(); - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - // Vec> - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: Vec>, - } - // Some - { - let pubkey_vec = (0..3) - .map(|_| Some(Pubkey::new_unique())) - .collect::>(); - let pubkey_struct = PubkeyStruct { pubkey: pubkey_vec }; - let manual_bytes = pubkey_struct.pubkey.try_to_vec().unwrap(); - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - // None - { - let pubkey_vec = (0..3).map(|_| None).collect::>(); - let pubkey_struct = PubkeyStruct { pubkey: pubkey_vec }; - let manual_bytes = pubkey_struct.pubkey.try_to_vec().unwrap(); - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - } -} - -#[test] -fn test_light_hasher_sha_macro() { - use light_sdk_macros::LightHasherSha; - - // Test struct with many fields that would exceed Poseidon's limit - #[derive(LightHasherSha, BorshSerialize, BorshDeserialize, Clone)] - struct LargeShaStruct { - pub field1: u64, - pub field2: u64, - pub field3: u64, - pub field4: u64, - pub field5: u64, - pub field6: u64, - pub field7: u64, - pub field8: u64, - pub field9: u64, - pub field10: u64, - pub field11: u64, - pub field12: u64, - pub field13: u64, - pub field14: u64, - pub field15: u64, - pub owner: Pubkey, - pub authority: Pubkey, - } - - let test_struct = LargeShaStruct { - field1: 1, - field2: 2, - field3: 3, - field4: 4, - field5: 5, - field6: 6, - field7: 7, - field8: 8, - field9: 9, - field10: 10, - field11: 11, - field12: 12, - field13: 13, - field14: 14, - field15: 15, - owner: Pubkey::new_unique(), - authority: Pubkey::new_unique(), - }; - - // Verify the hash matches manual SHA256 hashing - let bytes = test_struct.try_to_vec().unwrap(); - let mut ref_hash = Sha256::hash(bytes.as_slice()).unwrap(); - - // Apply truncation for non-Poseidon hashers (ID != 0) - if Sha256::ID != 0 { - ref_hash[0] = 0; - } - - // Test with SHA256 hasher - let hash_result = test_struct.hash::().unwrap(); - assert_eq!( - hash_result, ref_hash, - "SHA256 hash should match manual hash" - ); - - // Test ToByteArray implementation - let byte_array_result = test_struct.to_byte_array().unwrap(); - assert_eq!( - byte_array_result, ref_hash, - "ToByteArray should match SHA256 hash" - ); - - // Test another struct with different values - let test_struct2 = LargeShaStruct { - field1: 100, - field2: 200, - field3: 300, - field4: 400, - field5: 500, - field6: 600, - field7: 700, - field8: 800, - field9: 900, - field10: 1000, - field11: 1100, - field12: 1200, - field13: 1300, - field14: 1400, - field15: 1500, - owner: Pubkey::new_unique(), - authority: Pubkey::new_unique(), - }; - - let bytes2 = test_struct2.try_to_vec().unwrap(); - let mut ref_hash2 = Sha256::hash(bytes2.as_slice()).unwrap(); - - if Sha256::ID != 0 { - ref_hash2[0] = 0; - } - - let hash_result2 = test_struct2.hash::().unwrap(); - assert_eq!( - hash_result2, ref_hash2, - "Second SHA256 hash should match manual hash" - ); - - // Ensure different structs produce different hashes - assert_ne!( - hash_result, hash_result2, - "Different structs should produce different hashes" - ); -} - -// Option -#[test] -fn test_borsh() { - #[derive(BorshDeserialize, BorshSerialize)] - pub struct BorshStruct { - data: [u8; 34], - } - impl Default for BorshStruct { - fn default() -> Self { - Self { data: [1u8; 34] } - } - } - // Option Borsh - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: Option, - } - // Some - { - let pubkey_struct = PubkeyStruct { - pubkey: Some(BorshStruct::default()), - }; - let manual_bytes = pubkey_struct.pubkey.as_ref().unwrap().try_to_vec().unwrap(); - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - // None - { - let pubkey_struct = PubkeyStruct { pubkey: None }; - let manual_hash = Poseidon::hash([0u8; 32].as_slice()).unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash([0u8; 32].as_slice()).unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } - } - // Borsh - { - #[derive(LightHasher)] - pub struct PubkeyStruct { - #[hash] - pub pubkey: BorshStruct, - } - - let pubkey_struct = PubkeyStruct { - pubkey: BorshStruct::default(), - }; - let manual_bytes = pubkey_struct.pubkey.try_to_vec().unwrap(); - - let manual_hash = Poseidon::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - - // Sha256 - let mut manual_hash = Sha256::hash( - light_compressed_account::hash_to_bn254_field_size_be(manual_bytes.as_slice()) - .as_slice(), - ) - .unwrap(); - // Apply truncation for non-Poseidon hashers - if Sha256::ID != 0 { - manual_hash[0] = 0; - } - let hash = pubkey_struct.hash::().unwrap(); - assert_eq!(manual_hash, hash); - } -} diff --git a/sdk-libs/macros/tests/pda.rs b/sdk-libs/macros/tests/pda.rs deleted file mode 100644 index 50fb33782e..0000000000 --- a/sdk-libs/macros/tests/pda.rs +++ /dev/null @@ -1,77 +0,0 @@ -use std::str::FromStr; - -use light_macros::derive_light_cpi_signer; -use light_sdk_types::CpiSigner; -use solana_pubkey::Pubkey; - -#[test] -fn test_compute_pda_basic() { - // Test with a known program ID using fixed "cpi_authority" seed - const RESULT: CpiSigner = - derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); - - // Verify the result has valid fields - assert_eq!(RESULT.program_id.len(), 32); - assert_eq!(RESULT.cpi_signer.len(), 32); - - // Verify this matches runtime computation - let runtime_result = Pubkey::find_program_address( - &[b"cpi_authority"], - &Pubkey::from_str("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7").unwrap(), - ); - - assert_eq!(RESULT.cpi_signer, runtime_result.0.to_bytes()); - assert_eq!(RESULT.bump, runtime_result.1); -} - -#[test] -fn test_cpi_signer() { - // Test that the macro can be used in const contexts - const PDA_RESULT: CpiSigner = - derive_light_cpi_signer!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7"); - - // Extract individual components in const context - const PROGRAM_ID: [u8; 32] = PDA_RESULT.program_id; - const CPI_SIGNER: [u8; 32] = PDA_RESULT.cpi_signer; - const BUMP: u8 = PDA_RESULT.bump; - - // Verify they're valid - assert_eq!( - PROGRAM_ID, - light_macros::pubkey_array!("SySTEM1eSU2p4BGQfQpimFEWWSC1XDFeun3Nqzz3rT7") - ); - assert_eq!( - CPI_SIGNER, - [ - 251, 179, 40, 117, 16, 92, 174, 133, 181, 180, 68, 118, 7, 237, 191, 225, 69, 39, 191, - 180, 35, 145, 28, 164, 4, 35, 191, 209, 82, 122, 38, 117 - ] - ); - assert_eq!(BUMP, 255); -} - -#[test] -fn test_cpi_signer_2() { - // Test that the macro can be used in const contexts - const PDA_RESULT: CpiSigner = - derive_light_cpi_signer!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq"); - - // Extract individual components in const context - const PROGRAM_ID: [u8; 32] = PDA_RESULT.program_id; - const CPI_SIGNER: [u8; 32] = PDA_RESULT.cpi_signer; - const BUMP: u8 = PDA_RESULT.bump; - - // Verify they're valid - assert_eq!( - PROGRAM_ID, - light_macros::pubkey_array!("compr6CUsB5m2jS4Y3831ztGSTnDpnKJTKS95d64XVq") - ); - assert_eq!( - CPI_SIGNER, - [ - 20, 12, 243, 109, 120, 11, 194, 48, 169, 64, 170, 103, 246, 66, 224, 151, 74, 116, 57, - 84, 0, 180, 16, 126, 175, 149, 24, 207, 85, 137, 3, 207 - ] - ); - assert_eq!(BUMP, 255); -} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/d5_markers.rs b/sdk-tests/csdk-anchor-full-derived-test/src/d5_markers.rs new file mode 100644 index 0000000000..570ad0bc54 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/d5_markers.rs @@ -0,0 +1,3 @@ +//! Re-export d5_markers from instructions module for top-level access. + +pub use crate::instructions::d5_markers::*; 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 new file mode 100644 index 0000000000..ffb4619e50 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/mod.rs @@ -0,0 +1,7 @@ +//! D5: Field marker attributes +//! +//! Tests #[rentfree], #[rentfree_token], and #[light_mint] attribute parsing. + +mod rentfree_bare; +// Note rent free custom rightfully is a failing test case not added here. +pub use rentfree_bare::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs new file mode 100644 index 0000000000..1bfe5b4da3 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/d5_markers/rentfree_bare.rs @@ -0,0 +1,44 @@ +//! D5 Test: #[rentfree] attribute with #[rentfree_program] macro +//! +//! Tests that the #[rentfree] attribute works correctly when used with the +//! #[rentfree_program] macro on instruction structs in submodules. +//! +//! Note: The params struct must contain `create_accounts_proof: CreateAccountsProof` +//! because the RentFree derive macro generates code that accesses this field. + +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 D5RentfreeBareParams { + pub create_accounts_proof: CreateAccountsProof, + pub owner: Pubkey, +} + +/// Tests that #[rentfree] attribute compiles with the #[rentfree_program] macro. +/// The field name can now differ from the type name (e.g., `record` with type `SinglePubkeyRecord`) +/// because the macro now uses the inner_type for seed spec correlation. +#[derive(Accounts, RentFree)] +#[instruction(params: D5RentfreeBareParams)] +pub struct D5RentfreeBare<'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"d5_bare", params.owner.as_ref()], + bump, + )] + #[rentfree] + pub 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 new file mode 100644 index 0000000000..e5f94831ee --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instructions/mod.rs @@ -0,0 +1,10 @@ +//! Instruction account test cases organized by dimension. +//! +//! Each subdirectory tests a specific macro code path dimension: +//! - d5_markers: Field marker attributes (#[rentfree], #[rentfree_token], #[light_mint]) +//! - d6_account_types: Account type extraction (Account, Box) +//! - d7_infra_names: Infrastructure field naming variations +//! - d8_builder_paths: Builder code generation paths +//! - d9_seeds: Seed expression classification + +pub mod d5_markers; 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 4477e078a5..0b43b352c4 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -5,13 +5,17 @@ use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; use light_sdk_macros::rentfree_program; use light_sdk_types::CpiSigner; +pub mod d5_markers; pub mod errors; pub mod instruction_accounts; +pub mod instructions; pub mod state; - +pub use d5_markers::*; pub use instruction_accounts::*; -pub use state::{GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord}; - +pub use state::{ + d1_field_types::single_pubkey::{PackedSinglePubkeyRecord, SinglePubkeyRecord}, + GameSession, PackedGameSession, PackedUserRecord, PlaceholderRecord, UserRecord, +}; #[inline] pub fn max_key(left: &Pubkey, right: &Pubkey) -> [u8; 32] { if left > right { @@ -41,10 +45,9 @@ pub const GAME_SESSION_SEED: &str = "game_session"; pub mod csdk_anchor_full_derived_test { #![allow(clippy::too_many_arguments)] - use super::*; - use crate::{ + use super::{ + d5_markers::{D5RentfreeBare, D5RentfreeBareParams}, instruction_accounts::CreatePdasAndMintAuto, - state::{GameSession, UserRecord}, FullAutoWithMintParams, LIGHT_CPI_SIGNER, }; @@ -131,4 +134,15 @@ pub mod csdk_anchor_full_derived_test { Ok(()) } + + /// Second instruction to test #[rentfree_program] with multiple instructions. + pub fn create_single_record<'info>( + ctx: Context<'_, '_, '_, 'info, D5RentfreeBare<'info>>, + params: D5RentfreeBareParams, + ) -> Result<()> { + let record = &mut ctx.accounts.record; + record.owner = params.owner; + record.counter = 0; + Ok(()) + } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs new file mode 100644 index 0000000000..9690c5cf2f --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/all.rs @@ -0,0 +1,36 @@ +//! D1 Test: ALL field types combined +//! +//! Exercises all field type code paths in a single struct: +//! - Multiple Pubkeys (-> u8 indices) +//! - Option (-> Option) +//! - String (-> clone() path) +//! - Arrays (-> direct copy) +//! - Option (-> unchanged) + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Comprehensive struct with all field type variations. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct AllFieldTypesRecord { + pub compression_info: Option, + // Multiple Pubkeys -> _index: u8 fields + pub owner: Pubkey, + pub delegate: Pubkey, + pub authority: Pubkey, + // Option -> Option + pub close_authority: Option, + // String -> clone() path + #[max_len(64)] + pub name: String, + // Arrays -> direct copy + pub hash: [u8; 32], + // Option -> unchanged + pub end_time: Option, + pub enabled: Option, + // Regular primitives + pub counter: u64, + pub flag: bool, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs new file mode 100644 index 0000000000..10c6ea2233 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/arrays.rs @@ -0,0 +1,18 @@ +//! D1 Test: Array fields - [u8; 32], [u8; 8] +//! +//! Exercises the code path for array field handling. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with array fields. +/// Tests [u8; 32] (byte array) and fixed-size arrays. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct ArrayRecord { + pub compression_info: Option, + pub hash: [u8; 32], + pub short_data: [u8; 8], + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs new file mode 100644 index 0000000000..cad591b53c --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs @@ -0,0 +1,12 @@ +//! D1: Field type variations +//! +//! Tests `is_pubkey_type()`, `is_copy_type()`, and Pack generation code paths. + +pub mod no_pubkey; +pub mod single_pubkey; +pub mod multi_pubkey; +pub mod non_copy; +pub mod option_pubkey; +pub mod option_primitive; +pub mod arrays; +pub mod all; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs new file mode 100644 index 0000000000..98c506a1e9 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/multi_pubkey.rs @@ -0,0 +1,20 @@ +//! D1 Test: Multiple Pubkey fields - PackedX with multiple u8 indices +//! +//! Exercises the code path where 3+ Pubkey fields exist, +//! generating a PackedX struct with multiple u8 index fields. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with multiple Pubkey fields. +/// PackedMultiPubkeyRecord will have: owner_index, delegate_index, authority_index: u8 +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct MultiPubkeyRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub delegate: Pubkey, + pub authority: Pubkey, + pub amount: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs new file mode 100644 index 0000000000..98822f70fd --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/no_pubkey.rs @@ -0,0 +1,19 @@ +//! D1 Test: No Pubkey fields - Identity Pack generation +//! +//! Exercises the code path where no Pubkey fields exist, +//! resulting in Pack/Unpack being a type alias (identity). + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with only primitive fields - no Pubkey. +/// This tests the identity Pack path where PackedNoPubkeyRecord = NoPubkeyRecord. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct NoPubkeyRecord { + pub compression_info: Option, + pub counter: u64, + pub flag: bool, + pub value: u32, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs new file mode 100644 index 0000000000..6822618d70 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/non_copy.rs @@ -0,0 +1,21 @@ +//! D1 Test: Non-Copy field (String) - clone() path +//! +//! Exercises the code path where a non-Copy field (String) exists, +//! which triggers the `.clone()` path in pack/unpack generation. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with a String field (non-Copy type). +/// This tests the clone() code path for non-Copy fields. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct NonCopyRecord { + pub compression_info: Option, + #[max_len(64)] + pub name: String, + #[max_len(128)] + pub description: String, + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs new file mode 100644 index 0000000000..bdb5bdf6a2 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_primitive.rs @@ -0,0 +1,20 @@ +//! D1 Test: Option fields +//! +//! Exercises the code path where Option, Option, etc. exist. +//! These remain unchanged in the packed struct (not converted to u8 index). + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with Option fields. +/// These stay as Option in the packed struct (not Option). +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct OptionPrimitiveRecord { + pub compression_info: Option, + pub counter: u64, + pub end_time: Option, + pub enabled: Option, + pub score: Option, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs new file mode 100644 index 0000000000..0a2ddf29bf --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/option_pubkey.rs @@ -0,0 +1,20 @@ +//! D1 Test: Option field +//! +//! Exercises the code path where Option fields exist, +//! which generates Option in the packed struct. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with Option fields. +/// PackedOptionPubkeyRecord will have: delegate_index: Option +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct OptionPubkeyRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub delegate: Option, + pub close_authority: Option, + pub amount: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs new file mode 100644 index 0000000000..e0c1c26f61 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/single_pubkey.rs @@ -0,0 +1,18 @@ +//! D1 Test: Single Pubkey field - PackedX with one u8 index +//! +//! Exercises the code path where exactly one Pubkey field exists, +//! generating a PackedX struct with a single u8 index field. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with exactly one Pubkey field. +/// PackedSinglePubkeyRecord will have: owner_index: u8 +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct SinglePubkeyRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs new file mode 100644 index 0000000000..a519ab94c3 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/absent.rs @@ -0,0 +1,19 @@ +//! D2 Test: compress_as attribute absent +//! +//! Exercises the code path where no #[compress_as] attribute is present. +//! All fields use self.field directly for compression. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct without any compress_as attribute. +/// All fields are compressed as-is using self.field. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct NoCompressAsRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub counter: u64, + pub flag: bool, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs new file mode 100644 index 0000000000..f56d3a75de --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/all.rs @@ -0,0 +1,28 @@ +//! D2 Test: ALL compress_as variations combined +//! +//! Exercises all compress_as code paths in a single struct: +//! - Multiple literal overrides (0) +//! - Option field override (None) +//! - Fields without override (use self.field) + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Comprehensive struct with all compress_as variations. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[compress_as(time = 0, end = None, score = 0, cached = 0)] +#[account] +pub struct AllCompressAsRecord { + pub compression_info: Option, + pub owner: Pubkey, + // Override with 0 + pub time: u64, + pub score: u64, + pub cached: u64, + // Override with None + pub end: Option, + // No override - uses self.field + pub counter: u64, + pub flag: bool, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs new file mode 100644 index 0000000000..74a6655629 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs @@ -0,0 +1,9 @@ +//! D2: compress_as attribute variations +//! +//! Tests override value parsing in traits.rs. + +pub mod absent; +pub mod single; +pub mod multiple; +pub mod option_none; +pub mod all; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs new file mode 100644 index 0000000000..9ce81f05b7 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/multiple.rs @@ -0,0 +1,21 @@ +//! D2 Test: compress_as with multiple overrides +//! +//! Exercises the code path where multiple fields have compress_as overrides. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with multiple compress_as overrides. +/// start, score, and cached all have compression overrides. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[compress_as(start = 0, score = 0, cached = 0)] +#[account] +pub struct MultipleCompressAsRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub start: u64, + pub score: u64, + pub cached: u64, + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs new file mode 100644 index 0000000000..701ec3b7aa --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/option_none.rs @@ -0,0 +1,20 @@ +//! D2 Test: compress_as with None value for Option fields +//! +//! Exercises the code path where Option fields are compressed as None. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with compress_as None for Option fields. +/// end_time is compressed as None instead of self.end_time. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[compress_as(end_time = None)] +#[account] +pub struct OptionNoneCompressAsRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub start_time: u64, + pub end_time: Option, + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs new file mode 100644 index 0000000000..f6916a0ae6 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/single.rs @@ -0,0 +1,19 @@ +//! D2 Test: compress_as with single override +//! +//! Exercises the code path where one field has a compress_as override. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// A struct with single compress_as override. +/// cached field is compressed as 0 instead of self.cached. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[compress_as(cached = 0)] +#[account] +pub struct SingleCompressAsRecord { + pub compression_info: Option, + pub owner: Pubkey, + pub cached: u64, + pub counter: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs new file mode 100644 index 0000000000..303b5500b0 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/all.rs @@ -0,0 +1,33 @@ +//! D4 Test: ALL composition variations combined +//! +//! Exercises a large struct with all field type variants from D1. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Comprehensive large struct with all field types. +/// 15+ fields to trigger SHA256 mode with all D1 variations. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[compress_as(cached_time = 0, end_time = None)] +#[account] +pub struct AllCompositionRecord { + // compression_info in middle position + pub owner: Pubkey, + pub delegate: Pubkey, + pub compression_info: Option, + pub authority: Pubkey, + pub close_authority: Option, + #[max_len(64)] + pub name: String, + pub hash: [u8; 32], + pub start_time: u64, + pub cached_time: u64, + pub end_time: Option, + pub counter_1: u64, + pub counter_2: u64, + pub counter_3: u64, + pub flag_1: bool, + pub flag_2: bool, + pub score: Option, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs new file mode 100644 index 0000000000..707f8cd5cd --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/info_last.rs @@ -0,0 +1,18 @@ +//! D4 Test: compression_info as last field +//! +//! Exercises struct validation with compression_info in non-first position. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Struct with compression_info as last field. +/// Tests that field ordering is handled correctly. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct InfoLastRecord { + pub owner: Pubkey, + pub counter: u64, + pub flag: bool, + pub compression_info: Option, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs new file mode 100644 index 0000000000..f940d4eaeb --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/large.rs @@ -0,0 +1,26 @@ +//! D4 Test: Large struct with many fields +//! +//! Exercises the hash mode selection for large structs (SHA256 path). + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Large struct with 12+ fields for SHA256 hash mode. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct LargeRecord { + pub compression_info: Option, + pub field_01: u64, + pub field_02: u64, + pub field_03: u64, + pub field_04: u64, + pub field_05: u64, + pub field_06: u64, + pub field_07: u64, + pub field_08: u64, + pub field_09: u64, + pub field_10: u64, + pub field_11: u64, + pub field_12: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs new file mode 100644 index 0000000000..577b71fd03 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/minimal.rs @@ -0,0 +1,15 @@ +//! D4 Test: Minimal valid struct +//! +//! Exercises the smallest valid struct with compression_info and one field. + +use anchor_lang::prelude::*; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; +use light_sdk_macros::RentFreeAccount; + +/// Smallest valid struct: compression_info + one field. +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct MinimalRecord { + pub compression_info: Option, + pub value: u64, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs new file mode 100644 index 0000000000..70c5209ba8 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs @@ -0,0 +1,8 @@ +//! D4: Struct composition and Pack generation +//! +//! Tests struct validation and size-based hash mode selection. + +pub mod minimal; +pub mod large; +pub mod info_last; +pub mod all; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs similarity index 88% rename from sdk-tests/csdk-anchor-full-derived-test/src/state.rs rename to sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs index 2a8884fb96..8354724d0f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/mod.rs @@ -1,3 +1,5 @@ +//! State structs for the test program and test cases organized by dimension. + use anchor_lang::prelude::*; use light_sdk::{ compressible::CompressionInfo, instruction::PackedAddressTreeInfo, LightDiscriminator, @@ -6,6 +8,13 @@ use light_sdk_macros::RentFreeAccount; use light_token_interface::instructions::mint_action::MintWithContext; use light_token_sdk::ValidityProof; +// Test modules +pub mod d1_field_types; +pub mod d2_compress_as; +pub mod d4_composition; + +// Original state types used by the main program + #[derive(Default, Debug, InitSpace, RentFreeAccount)] #[account] pub struct UserRecord { From 25a5507adeafffec197075e9d2e0afec619862fb Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 20:54:34 +0000 Subject: [PATCH 5/7] amm test instructions compile --- sdk-libs/macros/docs/CLAUDE.md | 25 +- sdk-libs/macros/docs/traits/compress_as.md | 223 ++++++++++++ sdk-libs/macros/docs/traits/compressible.md | 296 ++++++++++++++++ .../macros/docs/traits/compressible_pack.md | 331 ++++++++++++++++++ .../docs/traits/has_compression_info.md | 156 +++++++++ .../macros/docs/traits/light_compressible.md | 315 +++++++++++++++++ .../macros/src/rentfree/program/parsing.rs | 27 +- .../src/amm_test/deposit.rs | 87 +++++ .../src/amm_test/initialize.rs | 238 +++++++++++++ .../src/amm_test/mod.rs | 19 + .../src/amm_test/states.rs | 62 ++++ .../src/amm_test/withdraw.rs | 83 +++++ .../csdk-anchor-full-derived-test/src/lib.rs | 30 +- .../src/processors/create_single_record.rs | 17 + .../src/processors/mod.rs | 9 + 15 files changed, 1894 insertions(+), 24 deletions(-) create mode 100644 sdk-libs/macros/docs/traits/compress_as.md create mode 100644 sdk-libs/macros/docs/traits/compressible.md create mode 100644 sdk-libs/macros/docs/traits/compressible_pack.md create mode 100644 sdk-libs/macros/docs/traits/has_compression_info.md create mode 100644 sdk-libs/macros/docs/traits/light_compressible.md create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/amm_test/mod.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/processors/create_single_record.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/src/processors/mod.rs diff --git a/sdk-libs/macros/docs/CLAUDE.md b/sdk-libs/macros/docs/CLAUDE.md index 4b5d7b4910..5d68b4814e 100644 --- a/sdk-libs/macros/docs/CLAUDE.md +++ b/sdk-libs/macros/docs/CLAUDE.md @@ -14,12 +14,24 @@ 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 | + +### Traits 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 | ## Navigation Tips ### Starting Points -- **Building account structs**: Start with `rentfree.md` for the accounts-level derive macro that marks fields for compression +- **Data struct traits**: Start with `traits/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 @@ -40,11 +52,12 @@ Documentation for the rentfree macro system in `light-sdk-macros`. These macros | +-- Generates LightPreInit + LightFinalize impls | - +-- Uses trait derives: - - HasCompressionInfo - - Compressible - - Pack/Unpack - - LightCompressible + +-- 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) ``` ## Related Source Code diff --git a/sdk-libs/macros/docs/traits/compress_as.md b/sdk-libs/macros/docs/traits/compress_as.md new file mode 100644 index 0000000000..45bde94e2f --- /dev/null +++ b/sdk-libs/macros/docs/traits/compress_as.md @@ -0,0 +1,223 @@ +# CompressAs Derive Macro + +## 1. Overview + +The `#[derive(CompressAs)]` macro generates the `CompressAs` trait implementation, which creates a compressed representation of an account struct. This compressed form is used for hashing and storing in the Light Protocol compression system. + +**When to use**: Apply this derive when you need only the compression transformation logic. For most use cases, prefer `#[derive(Compressible)]` or `#[derive(LightCompressible)]` which include this trait. + +**Source**: `sdk-libs/macros/src/rentfree/traits/traits.rs` (lines 91-153) + +--- + +## 2. How It Works + +### 2.1 Compile-Time Flow + +``` ++---------------------+ +-------------------+ +-------------------+ +| Input Struct | --> | Macro at | --> | Generated | +| | | Compile Time | | Code | ++---------------------+ +-------------------+ +-------------------+ +| #[compress_as( | | 1. Parse struct | | impl CompressAs | +| cached = 0)] | | attributes | | for GameData { | +| pub struct GameData | | 2. Classify each | | fn compress_as | +| { | | field: | | -> Cow | +| score: u64, | | - Skip? | | { ... } | +| cached: u64, | | - Override? | | } | +| compression_info | | - Copy/Clone? | | | +| } | | 3. Generate impl | | | ++---------------------+ +-------------------+ +-------------------+ +``` + +### 2.2 Field Classification + +Each struct field is classified at compile time: + +``` +Field Processing Pipeline ++------------------------+ +| Input Field | ++------------------------+ + | + v ++------------------------+ YES +------------------+ +| Is "compression_info"? |------------>| Set to None | ++------------------------+ +------------------+ + | NO + v ++------------------------+ YES +------------------+ +| Has #[skip] attr? |------------>| Exclude entirely | ++------------------------+ +------------------+ + | NO + v ++------------------------+ YES +------------------+ +| Has #[compress_as] |------------>| Use override | +| override? | | expression | ++------------------------+ +------------------+ + | NO + v ++------------------------+ YES +------------------+ +| Is Copy type? |------------>| self.field | ++------------------------+ +------------------+ + | NO + v ++------------------------+ +| self.field.clone() | ++------------------------+ +``` + +### 2.3 Purpose in Compression System + +The compressed representation is used for hashing account state: + +``` +Original Account compress_as() Hash Input ++----------------------+ +----------------------+ +----------+ +| score: 100 | | score: 100 | | | +| cached: 999 | --> | cached: 0 (zeroed) | -> | SHA256 | +| last_login: 12345 | | (skipped) | | hash | +| compression_info: | | compression_info: | | | +| Some(...) | | None | | | ++----------------------+ +----------------------+ +----------+ +``` + +This ensures that: +- Transient fields (caches, timestamps) don't affect the hash +- `compression_info` metadata doesn't affect content hash +- Only semantically meaningful data is included + +--- + +## 3. Generated Trait + +The macro implements `light_sdk::compressible::CompressAs`: + +```rust +impl CompressAs for YourStruct { + type Output = Self; + + fn compress_as(&self) -> Cow<'_, Self::Output>; +} +``` + +The `compress_as()` method returns a `Cow::Owned` containing a copy of the struct with: +- `compression_info` set to `None` +- All other fields copied (Clone for non-Copy types) +- Any `#[compress_as(...)]` overrides applied + +--- + +## 4. Supported Attributes + +### `#[compress_as(field = expr, ...)]` - Field Overrides + +Override specific field values in the compressed representation. Useful for zeroing out fields that shouldn't affect the compressed hash. + +```rust +#[derive(CompressAs)] +#[compress_as(start_time = 0, cached_value = 0)] +pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, // Will be 0 in compressed form + pub cached_value: u64, // Will be 0 in compressed form + pub compression_info: Option, +} +``` + +### `#[skip]` - Exclude Fields + +Mark fields to exclude from the compressed representation entirely: + +```rust +#[derive(CompressAs)] +pub struct CachedData { + pub id: u64, + #[skip] // Not included in compress_as output + pub cached_timestamp: u64, + pub compression_info: Option, +} +``` + +--- + +## 5. Auto-Skipped Fields + +The following fields are automatically excluded from compression: +- `compression_info` - Always handled specially (set to `None`) +- Fields marked with `#[skip]` + +--- + +## 6. Code Example + +### Input + +```rust +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::CompressAs; + +#[derive(Clone, CompressAs)] +#[compress_as(cached_score = 0)] +pub struct UserRecord { + pub owner: Pubkey, + pub score: u64, + pub cached_score: u64, // Overridden to 0 + #[skip] + pub last_updated: u64, // Excluded entirely + pub compression_info: Option, +} +``` + +### Generated Output + +```rust +impl light_sdk::compressible::CompressAs for UserRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + owner: self.owner, // Copy type - direct copy + score: self.score, // Copy type - direct copy + cached_score: 0, // Override from #[compress_as] + // last_updated skipped due to #[skip] + }) + } +} +``` + +--- + +## 7. Copy vs Clone Behavior + +The macro automatically detects Copy types and handles them efficiently: + +| Type | Behavior | +|------|----------| +| Copy types (`u8`, `u64`, `Pubkey`, etc.) | Direct copy: `self.field` | +| Non-Copy types (`String`, `Vec`, etc.) | Clone: `self.field.clone()` | + +Copy types recognized: +- Primitives: `bool`, `u8`-`u128`, `i8`-`i128`, `f32`, `f64`, `char` +- Solana types: `Pubkey` +- Arrays of Copy types + +--- + +## 8. Usage Notes + +- The struct must implement `Clone` for non-Copy field types +- Field overrides in `#[compress_as(...)]` must be valid expressions for the field type +- The `compression_info` field is required but does not need to be specified in overrides + +--- + +## 9. Related Macros + +| Macro | Relationship | +|-------|--------------| +| [`HasCompressionInfo`](has_compression_info.md) | Provides compression info accessors (used alongside) | +| [`Compressible`](compressible.md) | Includes `CompressAs` + other compression traits | +| [`LightCompressible`](light_compressible.md) | Includes all compression traits including `CompressAs` | diff --git a/sdk-libs/macros/docs/traits/compressible.md b/sdk-libs/macros/docs/traits/compressible.md new file mode 100644 index 0000000000..c2ee2a1d3c --- /dev/null +++ b/sdk-libs/macros/docs/traits/compressible.md @@ -0,0 +1,296 @@ +# Compressible Derive Macro + +## 1. Overview + +The `#[derive(Compressible)]` macro is a combined derive that generates all core compression traits needed for an account struct. It is the recommended way to add compression support when you don't need hashing or discriminator traits. + +**When to use**: Apply this derive when you need compression traits but are handling hashing and discriminator separately. For full compression support, use `#[derive(LightCompressible)]` instead. + +**Source**: `sdk-libs/macros/src/rentfree/traits/traits.rs` (lines 233-272) + +--- + +## 2. How It Works + +### 2.1 Compile-Time Expansion + +``` ++------------------+ +--------------------+ +--------------------+ +| Input Struct | --> | Compressible | --> | 4 Trait Impls | +| | | Macro | | | ++------------------+ +--------------------+ +--------------------+ +| #[derive( | | Expands to 4 | | - HasCompression- | +| Compressible)] | | internal derives: | | Info | +| pub struct User {| | | | - CompressAs | +| owner: Pubkey, | | 1. HasCompression- | | - Size | +| score: u64, | | Info | | - CompressedInit- | +| compression_ | | 2. CompressAs | | Space | +| info: ... | | 3. Size | | | +| } | | 4. CompressedInit- | | | +| | | Space | | | ++------------------+ +--------------------+ +--------------------+ +``` + +### 2.2 Trait Generation Pipeline + +``` +derive_compressible() + | + +---> validate_compression_info_field() + | | + | v + | Error if missing compression_info field + | + +---> generate_has_compression_info_impl() + | | + | v + | HasCompressionInfo trait impl + | + +---> generate_compress_as_field_assignments() + | | + | +---> Process each field + | | - Skip compression_info + | | - Skip #[skip] fields + | | - Apply #[compress_as] overrides + | | - Copy vs Clone detection + | v + | generate_compress_as_impl() + | + +---> generate_size_fields() + | | + | v + | Size trait impl + | + +---> generate_compressed_init_space_impl() + | + v + CompressedInitSpace trait impl +``` + +### 2.3 Role in Compression System + +The four traits work together during compression/decompression: + +``` +COMPRESSION FLOW ++------------------------+ +| Account Data | ++------------------------+ + | + | HasCompressionInfo + v ++------------------------+ +| Set compression_info | +| with address, lamports | ++------------------------+ + | + | CompressAs + v ++------------------------+ +| Create clean copy for | +| hashing (no metadata) | ++------------------------+ + | + | Size + v ++------------------------+ +| Calculate byte size | +| for Merkle tree leaf | ++------------------------+ + | + | CompressedInitSpace + v ++------------------------+ +| Verify fits in 800 | +| byte limit | ++------------------------+ +``` + +--- + +## 3. Generated Traits + +The `Compressible` derive generates implementations for four traits: + +| Trait | Purpose | +|-------|---------| +| `HasCompressionInfo` | Accessor methods for `compression_info` field | +| `CompressAs` | Creates compressed representation for hashing | +| `Size` | Calculates serialized byte size | +| `CompressedInitSpace` | Provides `COMPRESSED_INIT_SPACE` constant | + +### Equivalent Manual Derives + +```rust +// This: +#[derive(Compressible)] +pub struct MyAccount { ... } + +// Is equivalent to: +#[derive(HasCompressionInfo, CompressAs, Size)] // + CompressedInitSpace +pub struct MyAccount { ... } +``` + +--- + +## 4. Required Field + +The struct **must** have a field named `compression_info` of type `Option`: + +```rust +pub struct MyAccount { + pub data: u64, + pub compression_info: Option, // Required +} +``` + +--- + +## 5. Supported Attributes + +### `#[compress_as(field = expr, ...)]` - Field Overrides + +Override specific field values in the compressed representation: + +```rust +#[derive(Compressible)] +#[compress_as(start_time = 0, cached_value = 0)] +pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, // Will be 0 in compressed form + pub cached_value: u64, // Will be 0 in compressed form + pub compression_info: Option, +} +``` + +### `#[skip]` - Exclude Fields + +Mark fields to exclude from both `CompressAs` output and `Size` calculation: + +```rust +#[derive(Compressible)] +pub struct CachedData { + pub id: u64, + #[skip] // Excluded from compression and size + pub cached_timestamp: u64, + pub compression_info: Option, +} +``` + +--- + +## 6. Generated Code Example + +### Input + +```rust +use anchor_lang::prelude::*; +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::Compressible; + +#[derive(Clone, InitSpace, Compressible)] +#[compress_as(cached_score = 0)] +pub struct UserRecord { + pub owner: Pubkey, + pub score: u64, + pub cached_score: u64, + pub compression_info: Option, +} +``` + +### Generated Output + +```rust +// HasCompressionInfo implementation +impl light_sdk::compressible::HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info.as_ref().expect("compression_info must be set") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info.as_mut().expect("compression_info must be set") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} + +// CompressAs implementation +impl light_sdk::compressible::CompressAs for UserRecord { + type Output = Self; + + fn compress_as(&self) -> std::borrow::Cow<'_, Self::Output> { + std::borrow::Cow::Owned(Self { + compression_info: None, + owner: self.owner, + score: self.score, + cached_score: 0, // Override applied + }) + } +} + +// Size implementation +impl light_sdk::account::Size for UserRecord { + fn size(&self) -> usize { + // CompressionInfo space: 1 (Option discriminant) + INIT_SPACE + let compression_info_size = 1 + ::INIT_SPACE; + compression_info_size + + self.owner.try_to_vec().expect("Failed to serialize").len() + + self.score.try_to_vec().expect("Failed to serialize").len() + + self.cached_score.try_to_vec().expect("Failed to serialize").len() + } +} + +// CompressedInitSpace implementation +impl light_sdk::compressible::CompressedInitSpace for UserRecord { + const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +} +``` + +--- + +## 7. Size Calculation + +The `Size` trait calculates the serialized byte size of the account: + +- **CompressionInfo space**: Always allocates space for `Some(CompressionInfo)` since it will be set during decompression +- **Field serialization**: Uses `try_to_vec()` (Borsh serialization) for accurate size +- **Auto-skipped fields**: `compression_info` and `#[skip]` fields are excluded + +--- + +## 8. CompressedInitSpace Calculation + +The `CompressedInitSpace` trait provides: + +```rust +const COMPRESSED_INIT_SPACE: usize = Self::LIGHT_DISCRIMINATOR.len() + Self::INIT_SPACE; +``` + +This requires the struct to also derive `LightDiscriminator` and Anchor's `InitSpace`. + +--- + +## 9. Usage Notes + +- The struct must derive `Clone` if it has non-Copy fields +- The struct should derive Anchor's `InitSpace` for `COMPRESSED_INIT_SPACE` to work +- For full compression support including hashing, use `#[derive(LightCompressible)]` + +--- + +## 10. Related Macros + +| Macro | Relationship | +|-------|--------------| +| [`HasCompressionInfo`](has_compression_info.md) | Included in `Compressible` | +| [`CompressAs`](compress_as.md) | Included in `Compressible` | +| [`CompressiblePack`](compressible_pack.md) | Pack/Unpack for Pubkey compression (separate derive) | +| [`LightCompressible`](light_compressible.md) | Includes `Compressible` + hashing + discriminator + pack | diff --git a/sdk-libs/macros/docs/traits/compressible_pack.md b/sdk-libs/macros/docs/traits/compressible_pack.md new file mode 100644 index 0000000000..e573ef8e2c --- /dev/null +++ b/sdk-libs/macros/docs/traits/compressible_pack.md @@ -0,0 +1,331 @@ +# CompressiblePack Derive Macro + +## 1. Overview + +The `#[derive(CompressiblePack)]` macro generates `Pack` and `Unpack` trait implementations along with a `Packed{StructName}` struct. This enables efficient Pubkey compression where 32-byte Pubkeys are replaced with u8 indices into a remaining accounts array. + +**When to use**: Apply this derive when you need to pack account data for compressed account instructions. This is automatically included in `#[derive(LightCompressible)]`. + +**Source**: `sdk-libs/macros/src/rentfree/traits/pack_unpack.rs` (lines 8-186) + +--- + +## 2. How It Works + +### 2.1 Compile-Time Decision + +``` +derive_compressible_pack() + | + v ++-------------------------+ +| Scan struct fields for | +| Pubkey types | ++-------------------------+ + | + +-----+-----+ + | | + v v ++-------+ +---------+ +| Has | | No | +| Pubkey| | Pubkey | ++-------+ +---------+ + | | + v v ++---------------+ +------------------+ +| Generate full | | Generate type | +| Packed struct | | alias + identity | +| + conversions | | impls | ++---------------+ +------------------+ +``` + +### 2.2 Pubkey Compression Flow + +32-byte Pubkeys are compressed to 1-byte indices: + +``` +PACK (Client-side) ++---------------------------+ +---------------------------+ +| UserRecord | | PackedUserRecord | ++---------------------------+ +---------------------------+ +| owner: ABC123... | -> | owner: 0 | +| authority: DEF456... | -> | authority: 1 | +| score: 100 | -> | score: 100 | ++---------------------------+ +---------------------------+ + | + v + +------------------+ + | remaining_accounts| + +------------------+ + | [0] ABC123... | + | [1] DEF456... | + +------------------+ + +UNPACK (On-chain) ++---------------------------+ +---------------------------+ +| PackedUserRecord | | UserRecord | ++---------------------------+ +---------------------------+ +| owner: 0 | -> | owner: ABC123... | +| authority: 1 | -> | authority: DEF456... | +| score: 100 | -> | score: 100 | ++---------------------------+ +---------------------------+ + ^ + | ++------------------+ +| remaining_accounts| +| [0] = ABC123... | +| [1] = DEF456... | ++------------------+ +``` + +### 2.3 Why Pack Pubkeys? + +Compressed account instructions are serialized and stored in Merkle trees. Packing provides: + +| Aspect | Unpacked | Packed | Savings | +|--------|----------|--------|---------| +| Single Pubkey | 32 bytes | 1 byte | 31 bytes | +| Two Pubkeys | 64 bytes | 2 bytes | 62 bytes | + +The remaining accounts array stores actual Pubkeys, while instruction data contains only indices. + +--- + +## 3. Generated Items + +The macro generates different outputs based on whether the struct contains Pubkey fields: + +### With Pubkey Fields + +| Item | Type | Description | +|------|------|-------------| +| `Packed{StructName}` | Struct | New struct with Pubkeys replaced by `u8` | +| `Pack for StructName` | Trait impl | Converts struct to packed form | +| `Unpack for StructName` | Trait impl | Identity unpack (returns clone) | +| `Pack for Packed{StructName}` | Trait impl | Identity pack (returns clone) | +| `Unpack for Packed{StructName}` | Trait impl | Converts packed form back to original | + +### Without Pubkey Fields + +| Item | Type | Description | +|------|------|-------------| +| `Packed{StructName}` | Type alias | `type Packed{StructName} = {StructName}` | +| `Pack for StructName` | Trait impl | Identity pack (returns clone) | +| `Unpack for StructName` | Trait impl | Identity unpack (returns clone) | + +--- + +## 4. Trait Signatures + +### Pack Trait + +```rust +pub trait Pack { + type Packed; + + fn pack(&self, remaining_accounts: &mut PackedAccounts) -> Self::Packed; +} +``` + +### Unpack Trait + +```rust +pub trait Unpack { + type Unpacked; + + fn unpack( + &self, + remaining_accounts: &[AccountInfo], + ) -> Result; +} +``` + +--- + +## 5. Code Example - With Pubkey Fields + +### Input + +```rust +use anchor_lang::prelude::*; +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::CompressiblePack; + +#[derive(Clone, CompressiblePack)] +pub struct UserRecord { + pub owner: Pubkey, + pub authority: Pubkey, + pub score: u64, + pub compression_info: Option, +} +``` + +### Generated Output + +```rust +// Packed struct with Pubkeys replaced by u8 indices +#[derive(Debug, Clone, anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize)] +pub struct PackedUserRecord { + pub owner: u8, // Pubkey -> u8 index + pub authority: u8, // Pubkey -> u8 index + pub score: u64, // Non-Pubkey unchanged + pub compression_info: Option, +} + +// Pack original -> packed +impl light_sdk::compressible::Pack for UserRecord { + type Packed = PackedUserRecord; + + #[inline(never)] + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + PackedUserRecord { + owner: remaining_accounts.insert_or_get(self.owner), + authority: remaining_accounts.insert_or_get(self.authority), + score: self.score, + compression_info: None, + } + } +} + +// Unpack original -> original (identity) +impl light_sdk::compressible::Unpack for UserRecord { + type Unpacked = Self; + + #[inline(never)] + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} + +// Pack packed -> packed (identity) +impl light_sdk::compressible::Pack for PackedUserRecord { + type Packed = Self; + + #[inline(never)] + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Unpack packed -> original +impl light_sdk::compressible::Unpack for PackedUserRecord { + type Unpacked = UserRecord; + + #[inline(never)] + fn unpack( + &self, + remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(UserRecord { + owner: *remaining_accounts[self.owner as usize].key, + authority: *remaining_accounts[self.authority as usize].key, + score: self.score, + compression_info: None, + }) + } +} +``` + +--- + +## 6. Code Example - Without Pubkey Fields + +### Input + +```rust +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::CompressiblePack; + +#[derive(Clone, CompressiblePack)] +pub struct SimpleRecord { + pub id: u64, + pub value: u32, + pub compression_info: Option, +} +``` + +### Generated Output + +```rust +// Type alias instead of new struct +pub type PackedSimpleRecord = SimpleRecord; + +// Identity pack +impl light_sdk::compressible::Pack for SimpleRecord { + type Packed = SimpleRecord; + + #[inline(never)] + fn pack(&self, _remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + self.clone() + } +} + +// Identity unpack +impl light_sdk::compressible::Unpack for SimpleRecord { + type Unpacked = Self; + + #[inline(never)] + fn unpack( + &self, + _remaining_accounts: &[anchor_lang::prelude::AccountInfo], + ) -> std::result::Result { + Ok(self.clone()) + } +} +``` + +--- + +## 7. Field Handling + +| Field Type | Pack Behavior | Unpack Behavior | +|------------|---------------|-----------------| +| `Pubkey` | `remaining_accounts.insert_or_get(pubkey)` -> `u8` | `*remaining_accounts[idx].key` -> `Pubkey` | +| `compression_info` | Always set to `None` | Always set to `None` | +| Copy types (`u64`, etc.) | Direct copy | Direct copy | +| Clone types (`String`, etc.) | `.clone()` | `.clone()` | + +### Pubkey Type Detection + +The macro recognizes these as Pubkey types: +- `Pubkey` +- `solana_pubkey::Pubkey` +- `anchor_lang::prelude::Pubkey` +- Other paths ending in `Pubkey` + +--- + +## 8. Usage in Instructions + +The pack/unpack system is used when building compressed account instructions: + +```rust +// Client-side: pack account data +let mut packed_accounts = PackedAccounts::new(); +let packed_record = user_record.pack(&mut packed_accounts); + +// On-chain: unpack from instruction data +let user_record = packed_record.unpack(ctx.remaining_accounts)?; +``` + +--- + +## 9. Usage Notes + +- The struct must implement `Clone` +- `compression_info` field is always set to `None` during pack/unpack +- All methods are marked `#[inline(never)]` for smaller program size +- The packed struct derives `AnchorSerialize` and `AnchorDeserialize` + +--- + +## 10. Related Macros + +| Macro | Relationship | +|-------|--------------| +| [`Compressible`](compressible.md) | Provides compression traits (separate concern) | +| [`LightCompressible`](light_compressible.md) | Includes `CompressiblePack` + all other traits | +| [`HasCompressionInfo`](has_compression_info.md) | Provides compression info accessors | diff --git a/sdk-libs/macros/docs/traits/has_compression_info.md b/sdk-libs/macros/docs/traits/has_compression_info.md new file mode 100644 index 0000000000..4d2ac58f6c --- /dev/null +++ b/sdk-libs/macros/docs/traits/has_compression_info.md @@ -0,0 +1,156 @@ +# HasCompressionInfo Derive Macro + +## 1. Overview + +The `#[derive(HasCompressionInfo)]` macro generates accessor methods for the `compression_info` field on compressible account structs. This trait is required for the Light Protocol compression system to read and write compression metadata. + +**When to use**: Apply this derive when you need only the compression info accessors, without the full `Compressible` or `LightCompressible` derives. + +**Source**: `sdk-libs/macros/src/rentfree/traits/traits.rs` (lines 46-88) + +--- + +## 2. How It Works + +### 2.1 Compile-Time Flow + +``` ++------------------+ +-------------------+ +------------------+ +| Input Struct | --> | Macro at | --> | Generated | +| | | Compile Time | | Code | ++------------------+ +-------------------+ +------------------+ +| pub struct User {| | 1. Find field | | impl HasCompres- | +| owner: Pubkey, | | "compression_ | | sionInfo for | +| compression_ | | info" | | User { ... } | +| info: Option< | | 2. Validate type | | | +| CompressionInfo| | 3. Generate impl | | | +| } | | | | | ++------------------+ +-------------------+ +------------------+ +``` + +### 2.2 Processing Steps + +1. **Field Extraction**: Macro extracts all named fields from the struct +2. **Validation**: Searches for `compression_info` field, errors if missing +3. **Code Generation**: Generates trait impl with hardcoded field access + +### 2.3 Runtime Behavior + +The generated methods provide access to compression metadata stored in the account: + +``` +Account State Method Call ++------------------------+ +------------------------+ +| compression_info: Some | --> | compression_info() | +| address: [u8; 32] | | Returns &CompressionInfo +| lamports: u64 | +------------------------+ +| ... | ++------------------------+ +------------------------+ +| compression_info: None | --> | compression_info() | +| | | PANICS! | ++------------------------+ +------------------------+ + | compression_info_mut_ | + | opt() - safe access | + +------------------------+ +``` + +--- + +## 3. Generated Trait + +The macro implements `light_sdk::compressible::HasCompressionInfo`: + +```rust +impl HasCompressionInfo for YourStruct { + fn compression_info(&self) -> &CompressionInfo; + fn compression_info_mut(&mut self) -> &mut CompressionInfo; + fn compression_info_mut_opt(&mut self) -> &mut Option; + fn set_compression_info_none(&mut self); +} +``` + +### Method Details + +| Method | Returns | Description | +|--------|---------|-------------| +| `compression_info()` | `&CompressionInfo` | Returns reference to compression info, panics if `None` | +| `compression_info_mut()` | `&mut CompressionInfo` | Returns mutable reference, panics if `None` | +| `compression_info_mut_opt()` | `&mut Option` | Returns mutable reference to the `Option` itself | +| `set_compression_info_none()` | `()` | Sets the field to `None` | + +--- + +## 4. Required Field + +The struct **must** have a field named `compression_info` of type `Option`: + +```rust +pub struct MyAccount { + pub data: u64, + pub compression_info: Option, // Required +} +``` + +If this field is missing, the macro will emit a compile error: + +``` +error: Struct must have a 'compression_info' field of type Option +``` + +--- + +## 5. Code Example + +### Input + +```rust +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::HasCompressionInfo; + +#[derive(HasCompressionInfo)] +pub struct UserRecord { + pub owner: Pubkey, + pub score: u64, + pub compression_info: Option, +} +``` + +### Generated Output + +```rust +impl light_sdk::compressible::HasCompressionInfo for UserRecord { + fn compression_info(&self) -> &light_sdk::compressible::CompressionInfo { + self.compression_info.as_ref().expect("compression_info must be set") + } + + fn compression_info_mut(&mut self) -> &mut light_sdk::compressible::CompressionInfo { + self.compression_info.as_mut().expect("compression_info must be set") + } + + fn compression_info_mut_opt(&mut self) -> &mut Option { + &mut self.compression_info + } + + fn set_compression_info_none(&mut self) { + self.compression_info = None; + } +} +``` + +--- + +## 6. Usage Notes + +- The `compression_info()` and `compression_info_mut()` methods will panic if called when the field is `None`. Use `compression_info_mut_opt()` for safe access. +- This trait is automatically included when using `#[derive(Compressible)]` or `#[derive(LightCompressible)]`. +- The field must be named exactly `compression_info` (not `info`, `compress_info`, etc.). + +--- + +## 7. Related Macros + +| Macro | Relationship | +|-------|--------------| +| [`Compressible`](compressible.md) | Includes `HasCompressionInfo` + `CompressAs` + `Size` + `CompressedInitSpace` | +| [`LightCompressible`](light_compressible.md) | Includes all compression traits | +| [`CompressAs`](compress_as.md) | Uses `HasCompressionInfo` to access compression metadata | diff --git a/sdk-libs/macros/docs/traits/light_compressible.md b/sdk-libs/macros/docs/traits/light_compressible.md new file mode 100644 index 0000000000..34a9f026c7 --- /dev/null +++ b/sdk-libs/macros/docs/traits/light_compressible.md @@ -0,0 +1,315 @@ +# LightCompressible Derive Macro + +## 1. Overview + +The `#[derive(LightCompressible)]` macro is a convenience derive that combines all traits required for a fully compressible account. It is the recommended way to prepare account structs for Light Protocol's rent-free compression system. + +**When to use**: Apply this derive to any account struct that will be used with `#[rentfree]` in an Accounts struct. This is the standard approach for most use cases. + +**Source**: `sdk-libs/macros/src/rentfree/traits/light_compressible.rs` (lines 56-79) + +--- + +## 2. How It Works + +### 2.1 Compile-Time Expansion + +``` +#[derive(LightCompressible)] + | + v ++----------------------------------+ +| derive_rentfree_account() | +| (light_compressible.rs:56) | ++----------------------------------+ + | + +---> derive_light_hasher_sha() + | | + | v + | DataHasher + ToByteArray impls + | + +---> discriminator() + | | + | v + | LightDiscriminator impl + | + +---> derive_compressible() + | | + | v + | HasCompressionInfo + CompressAs + + | Size + CompressedInitSpace impls + | + +---> derive_compressible_pack() + | + v + Pack + Unpack impls + + Packed{Name} struct +``` + +### 2.2 Full Transformation Flow + +``` +INPUT GENERATED ++---------------------------+ +------------------------------------------+ +| #[derive(LightCompressible)] | // 8+ trait implementations | +| pub struct UserRecord { | | | +| pub owner: Pubkey, | | impl DataHasher for UserRecord { ... } | +| pub score: u64, | | impl ToByteArray for UserRecord { ... } | +| pub compression_info: | | impl LightDiscriminator for UserRecord { | +| Option | const LIGHT_DISCRIMINATOR = [...]; | +| } | | } | ++---------------------------+ | impl HasCompressionInfo for UserRecord { | + | fn compression_info() -> &... | + | fn compression_info_mut() -> &mut ... | + | } | + | impl CompressAs for UserRecord { ... } | + | impl Size for UserRecord { ... } | + | impl CompressedInitSpace for UserRecord {| + | impl Pack for UserRecord { ... } | + | impl Unpack for UserRecord { ... } | + | pub struct PackedUserRecord { ... } | + | impl Pack for PackedUserRecord { ... } | + | impl Unpack for PackedUserRecord { ... } | + +------------------------------------------+ +``` + +### 2.3 Role in Compression Lifecycle + +``` + COMPRESSION LIFECYCLE + ==================== + ++-------------------+ +-------------------+ +-------------------+ +| Data Struct | --> | Accounts Struct | --> | Runtime | ++-------------------+ +-------------------+ +-------------------+ +| #[derive( | | #[derive(Accounts,| | light_pre_init() | +| LightCompressible)] | RentFree)] | | Uses: | +| | | #[instruction] | | - DataHasher | +| Provides: | | pub struct Create | | - LightDiscrim. | +| - Hashing | | { | | - HasCompression| +| - Discriminator | | #[rentfree] | | Info | +| - Compression | | pub user_record | | - CompressAs | +| - Pack/Unpack | | } | | - Size | ++-------------------+ +-------------------+ | - Pack | + +-------------------+ +``` + +--- + +## 3. Generated Traits + +`LightCompressible` expands to four derive macros: + +| Derive | Traits Generated | +|--------|------------------| +| `LightHasherSha` | `DataHasher`, `ToByteArray` | +| `LightDiscriminator` | `LightDiscriminator` | +| `Compressible` | `HasCompressionInfo`, `CompressAs`, `Size`, `CompressedInitSpace` | +| `CompressiblePack` | `Pack`, `Unpack`, `Packed{Name}` struct | + +### Equivalent Manual Derives + +```rust +// This: +#[derive(LightCompressible)] +pub struct MyAccount { ... } + +// Is equivalent to: +#[derive(LightHasherSha, LightDiscriminator, Compressible, CompressiblePack)] +pub struct MyAccount { ... } +``` + +--- + +## 4. Required Field + +The struct **must** have a field named `compression_info` of type `Option`: + +```rust +pub struct MyAccount { + pub data: u64, + pub compression_info: Option, // Required +} +``` + +--- + +## 5. Supported Attributes + +### `#[compress_as(field = expr, ...)]` - Field Overrides + +Override specific field values in the compressed representation (passed to `Compressible` derive): + +```rust +#[derive(LightCompressible)] +#[compress_as(start_time = 0, cached_value = 0)] +pub struct GameSession { + pub session_id: u64, + pub player: Pubkey, + pub start_time: u64, // Will be 0 in compressed form + pub cached_value: u64, // Will be 0 in compressed form + pub compression_info: Option, +} +``` + +### `#[skip]` - Exclude Fields + +Mark fields to exclude from compression and size calculations: + +```rust +#[derive(LightCompressible)] +pub struct CachedData { + pub id: u64, + #[skip] // Excluded from compression + pub cached_timestamp: u64, + pub compression_info: Option, +} +``` + +--- + +## 6. Complete Code Example + +### Input + +```rust +use anchor_lang::prelude::*; +use light_sdk::compressible::CompressionInfo; +use light_sdk_macros::LightCompressible; + +#[derive(Default, Debug, Clone, InitSpace, LightCompressible)] +#[account] +pub struct UserRecord { + pub owner: Pubkey, + #[max_len(32)] + pub name: String, + pub score: u64, + pub compression_info: Option, +} +``` + +### Generated Output Summary + +```rust +// From LightHasherSha: +impl light_hasher::DataHasher for UserRecord { ... } +impl light_hasher::ToByteArray for UserRecord { ... } + +// From LightDiscriminator: +impl light_sdk::discriminator::LightDiscriminator for UserRecord { + const LIGHT_DISCRIMINATOR: &'static [u8] = &[...]; // 8-byte unique ID +} + +// From Compressible: +impl light_sdk::compressible::HasCompressionInfo for UserRecord { ... } +impl light_sdk::compressible::CompressAs for UserRecord { ... } +impl light_sdk::account::Size for UserRecord { ... } +impl light_sdk::compressible::CompressedInitSpace for UserRecord { ... } + +// From CompressiblePack: +#[derive(Debug, Clone, AnchorSerialize, AnchorDeserialize)] +pub struct PackedUserRecord { + pub owner: u8, // Pubkey compressed to index + pub name: String, + pub score: u64, + pub compression_info: Option, +} +impl light_sdk::compressible::Pack for UserRecord { ... } +impl light_sdk::compressible::Unpack for UserRecord { ... } +impl light_sdk::compressible::Pack for PackedUserRecord { ... } +impl light_sdk::compressible::Unpack for PackedUserRecord { ... } +``` + +--- + +## 7. Hashing Behavior + +The `LightHasherSha` component uses SHA256 to hash the entire struct: + +- **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` + +--- + +## 8. Discriminator + +The `LightDiscriminator` component generates an 8-byte unique identifier: + +```rust +const LIGHT_DISCRIMINATOR: &'static [u8] = &[0x12, 0x34, ...]; // SHA256("light:UserRecord")[..8] +``` + +This discriminator is used to identify account types in compressed account data. + +--- + +## 9. Pubkey Packing + +If the struct contains `Pubkey` fields, `CompressiblePack` generates: + +- A `Packed{Name}` struct with `Pubkey` fields replaced by `u8` indices +- `Pack` implementation to convert to packed form +- `Unpack` implementation to restore from packed form + +If no `Pubkey` fields exist, identity implementations are generated instead. + +--- + +## 10. Usage with RentFree + +`LightCompressible` prepares the data struct for use with `#[derive(RentFree)]` on Accounts structs: + +```rust +// Data struct - apply LightCompressible +#[derive(Default, Debug, Clone, InitSpace, LightCompressible)] +#[account] +pub struct UserRecord { + pub owner: Pubkey, + pub score: u64, + pub compression_info: Option, +} + +// Accounts struct - apply RentFree +#[derive(Accounts, RentFree)] +#[instruction(params: CreateParams)] +pub struct Create<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + #[account(init, payer = fee_payer, space = 8 + UserRecord::INIT_SPACE, ...)] + #[rentfree] + pub user_record: Account<'info, UserRecord>, +} +``` + +--- + +## 11. Usage Notes + +- The struct must derive `Clone` (required by `CompressiblePack`) +- The struct should derive Anchor's `InitSpace` (required by `CompressedInitSpace`) +- The `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) +- Only works with named-field structs, not tuple structs or unit structs +- Enums are not supported + +--- + +## 12. Error Conditions + +| Error | Cause | +|-------|-------| +| `LightCompressible can only be derived for structs` | Applied to enum or union | +| `Struct must have a 'compression_info' field` | Missing required field | + +--- + +## 13. Related Macros + +| Macro | Relationship | +|-------|--------------| +| [`HasCompressionInfo`](has_compression_info.md) | Included via `Compressible` | +| [`CompressAs`](compress_as.md) | Included via `Compressible` | +| [`Compressible`](compressible.md) | Included in `LightCompressible` | +| [`CompressiblePack`](compressible_pack.md) | Included in `LightCompressible` | +| [`RentFree`](../rentfree.md) | Uses traits from `LightCompressible` | diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index 2a3cfe033e..c5cf063aa8 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -410,25 +410,24 @@ pub fn wrap_function_with_rentfree(fn_item: &ItemFn, params_ident: &Ident) -> It #fn_vis #fn_sig { // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) use light_sdk::compressible::{LightPreInit, LightFinalize}; - let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) + let _ = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) .map_err(|e| { let pe: solana_program_error::ProgramError = e.into(); pe })?; - // Execute the original handler body in a closure - let __light_handler_result = (|| #fn_block)(); - - // Phase 2: On success, finalize compression - if __light_handler_result.is_ok() { - ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe - })?; - } - - __light_handler_result + // Execute the original handler body + #fn_block + + // TODO(diff-pr): Reactivate light_finalize for top up transfers. + // Currently disabled because user code may move ctx, making it + // inaccessible after the handler body executes. When top up + // transfers are implemented, we'll need to store AccountInfo + // references before user code runs. + // + // if __light_handler_result.is_ok() { + // ctx.accounts.light_finalize(ctx.remaining_accounts, ¶ms, __has_pre_init)?; + // } } }; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs new file mode 100644 index 0000000000..ba65cd2fef --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs @@ -0,0 +1,87 @@ +//! Deposit instruction with MintToCpi. + +use anchor_lang::prelude::*; +use anchor_spl::token::Token; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use light_token_sdk::token::MintToCpi; + +use super::states::*; + +#[derive(Accounts)] +pub struct Deposit<'info> { + #[account(mut)] + pub owner: Signer<'info>, + + #[account( + seeds = [AUTH_SEED.as_bytes()], + bump, + )] + pub authority: UncheckedAccount<'info>, + + #[account(mut)] + pub pool_state: Box>, + + #[account(mut)] + pub owner_lp_token: UncheckedAccount<'info>, + + #[account( + mut, + token::mint = vault_0_mint, + token::authority = owner, + )] + pub token_0_account: Box>, + + #[account( + mut, + token::mint = vault_1_mint, + token::authority = owner, + )] + pub token_1_account: Box>, + + #[account( + mut, + constraint = token_0_vault.key() == pool_state.token_0_vault, + )] + pub token_0_vault: Box>, + + #[account( + mut, + constraint = token_1_vault.key() == pool_state.token_1_vault, + )] + pub token_1_vault: Box>, + + #[account(address = pool_state.token_0_mint)] + pub vault_0_mint: Box>, + + #[account(address = pool_state.token_1_mint)] + pub vault_1_mint: Box>, + + #[account( + mut, + constraint = lp_mint.key() == pool_state.lp_mint, + )] + pub lp_mint: UncheckedAccount<'info>, + + pub token_program: Program<'info, Token>, + pub token_program_2022: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} + +/// Deposit instruction handler with MintToCpi. +pub fn process_deposit(ctx: Context, lp_token_amount: u64) -> Result<()> { + let pool_state = &ctx.accounts.pool_state; + let auth_bump = pool_state.auth_bump; + + // Mint LP tokens to owner using MintToCpi + MintToCpi { + mint: ctx.accounts.lp_mint.to_account_info(), + destination: ctx.accounts.owner_lp_token.to_account_info(), + amount: lp_token_amount, + authority: ctx.accounts.authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[auth_bump]]])?; + + Ok(()) +} 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 new file mode 100644 index 0000000000..49708d6572 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/initialize.rs @@ -0,0 +1,238 @@ +//! Initialize instruction with all rentfree markers. +//! +//! Tests: +//! - 2x #[rentfree] (pool_state, observation_state) +//! - 2x #[rentfree_token(authority = [...])] (token_0_vault, token_1_vault) +//! - 1x #[light_mint(...)] (lp_mint) +//! - CreateTokenAccountCpi.rent_free() +//! - CreateTokenAtaCpi.rent_free() +//! - MintToCpi + +use anchor_lang::prelude::*; +use anchor_spl::{ + associated_token::AssociatedToken, + token::Token, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use light_compressible::CreateAccountsProof; +use light_sdk_macros::RentFree; +use light_token_sdk::token::{ + CreateTokenAccountCpi, CreateTokenAtaCpi, MintToCpi, COMPRESSIBLE_CONFIG_V1, + RENT_SPONSOR as CTOKEN_RENT_SPONSOR, +}; + +use super::states::*; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone)] +pub struct InitializeParams { + pub init_amount_0: u64, + pub init_amount_1: u64, + pub open_time: u64, + pub create_accounts_proof: CreateAccountsProof, + pub lp_mint_signer_bump: u8, + pub creator_lp_token_bump: u8, +} + +#[derive(Accounts, RentFree)] +#[instruction(params: InitializeParams)] +pub struct InitializePool<'info> { + #[account(mut)] + pub creator: Signer<'info>, + + /// CHECK: AMM config account + pub amm_config: AccountInfo<'info>, + + #[account( + seeds = [AUTH_SEED.as_bytes()], + bump, + )] + pub authority: UncheckedAccount<'info>, + + #[account( + init, + seeds = [ + POOL_SEED.as_bytes(), + amm_config.key().as_ref(), + token_0_mint.key().as_ref(), + token_1_mint.key().as_ref(), + ], + bump, + payer = creator, + space = 8 + PoolState::INIT_SPACE + )] + #[rentfree] + pub pool_state: Box>, + + #[account( + constraint = token_0_mint.key() < token_1_mint.key(), + mint::token_program = token_0_program, + )] + pub token_0_mint: Box>, + + #[account(mint::token_program = token_1_program)] + pub token_1_mint: Box>, + + #[account( + seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state.key().as_ref()], + bump, + )] + pub lp_mint_signer: UncheckedAccount<'info>, + + #[account(mut)] + #[light_mint( + mint_signer = lp_mint_signer, + authority = authority, + decimals = 9, + signer_seeds = &[POOL_LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]] + )] + pub lp_mint: UncheckedAccount<'info>, + + #[account( + mut, + token::mint = token_0_mint, + token::authority = creator, + )] + pub creator_token_0: Box>, + + #[account( + mut, + token::mint = token_1_mint, + token::authority = creator, + )] + pub creator_token_1: Box>, + + #[account(mut)] + pub creator_lp_token: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + pool_state.key().as_ref(), + token_0_mint.key().as_ref() + ], + bump, + )] + #[rentfree_token(authority = [AUTH_SEED.as_bytes()])] + pub token_0_vault: UncheckedAccount<'info>, + + #[account( + mut, + seeds = [ + POOL_VAULT_SEED.as_bytes(), + pool_state.key().as_ref(), + token_1_mint.key().as_ref() + ], + bump, + )] + #[rentfree_token(authority = [AUTH_SEED.as_bytes()])] + pub token_1_vault: UncheckedAccount<'info>, + + #[account( + init, + seeds = [OBSERVATION_SEED.as_bytes(), pool_state.key().as_ref()], + bump, + payer = creator, + space = 8 + ObservationState::INIT_SPACE + )] + #[rentfree] + pub observation_state: Box>, + + pub token_program: Program<'info, Token>, + pub token_0_program: Interface<'info, TokenInterface>, + pub token_1_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + pub rent: Sysvar<'info, Rent>, + + pub compression_config: AccountInfo<'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 light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority. + pub ctoken_cpi_authority: AccountInfo<'info>, +} + +/// Initialize instruction handler (noop for compilation test). +pub fn process_initialize_pool<'info>( + ctx: Context<'_, '_, '_, 'info, InitializePool<'info>>, + params: InitializeParams, +) -> Result<()> { + let pool_state_key = ctx.accounts.pool_state.key(); + + // Create token_0 vault using CreateTokenAccountCpi.rent_free() + CreateTokenAccountCpi { + payer: ctx.accounts.creator.to_account_info(), + account: ctx.accounts.token_0_vault.to_account_info(), + mint: ctx.accounts.token_0_mint.to_account_info(), + owner: ctx.accounts.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(&[ + POOL_VAULT_SEED.as_bytes(), + pool_state_key.as_ref(), + ctx.accounts.token_0_mint.key().as_ref(), + &[ctx.bumps.token_0_vault], + ])?; + + // Create token_1 vault using CreateTokenAccountCpi.rent_free() + CreateTokenAccountCpi { + payer: ctx.accounts.creator.to_account_info(), + account: ctx.accounts.token_1_vault.to_account_info(), + mint: ctx.accounts.token_1_mint.to_account_info(), + owner: ctx.accounts.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(&[ + POOL_VAULT_SEED.as_bytes(), + pool_state_key.as_ref(), + ctx.accounts.token_1_mint.key().as_ref(), + &[ctx.bumps.token_1_vault], + ])?; + + // Create creator LP token ATA using CreateTokenAtaCpi.rent_free() + CreateTokenAtaCpi { + payer: ctx.accounts.creator.to_account_info(), + owner: ctx.accounts.creator.to_account_info(), + mint: ctx.accounts.lp_mint.to_account_info(), + ata: ctx.accounts.creator_lp_token.to_account_info(), + bump: params.creator_lp_token_bump, + } + .idempotent() + .rent_free( + ctx.accounts.ctoken_compressible_config.to_account_info(), + ctx.accounts.ctoken_rent_sponsor.to_account_info(), + ctx.accounts.system_program.to_account_info(), + ) + .invoke()?; + + // Mint LP tokens using MintToCpi + let lp_amount = 1000u64; // Placeholder amount + MintToCpi { + mint: ctx.accounts.lp_mint.to_account_info(), + destination: ctx.accounts.creator_lp_token.to_account_info(), + amount: lp_amount, + authority: ctx.accounts.authority.to_account_info(), + system_program: ctx.accounts.system_program.to_account_info(), + max_top_up: None, + } + .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; + + Ok(()) +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/mod.rs new file mode 100644 index 0000000000..a8a3ff5b5d --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/mod.rs @@ -0,0 +1,19 @@ +//! AMM test cases based on cp-swap-reference patterns. +//! +//! Tests: +//! - Multiple #[rentfree] fields +//! - #[rentfree_token] with authority seeds +//! - #[light_mint] for LP token creation +//! - CreateTokenAccountCpi.rent_free() +//! - CreateTokenAtaCpi.rent_free() +//! - MintToCpi / BurnCpi + +mod deposit; +mod initialize; +mod states; +mod withdraw; + +pub use deposit::*; +pub use initialize::*; +pub use states::*; +pub use withdraw::*; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs new file mode 100644 index 0000000000..e8e8cb2007 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs @@ -0,0 +1,62 @@ +//! AMM state structs adapted from cp-swap-reference. + +use anchor_lang::prelude::*; +use light_sdk::compressible::CompressionInfo; +use light_sdk::LightDiscriminator; +use light_sdk_macros::RentFreeAccount; + +pub const POOL_SEED: &str = "pool"; +pub const POOL_VAULT_SEED: &str = "pool_vault"; +pub const OBSERVATION_SEED: &str = "observation"; +pub const POOL_LP_MINT_SIGNER_SEED: &[u8] = b"pool_lp_mint"; +pub const AUTH_SEED: &str = "vault_and_lp_mint_auth_seed"; + +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +#[repr(C)] +pub struct PoolState { + pub compression_info: Option, + pub amm_config: Pubkey, + pub pool_creator: Pubkey, + pub token_0_vault: Pubkey, + pub token_1_vault: Pubkey, + pub lp_mint: Pubkey, + pub token_0_mint: Pubkey, + pub token_1_mint: Pubkey, + pub token_0_program: Pubkey, + pub token_1_program: Pubkey, + pub observation_key: Pubkey, + pub auth_bump: u8, + pub status: u8, + pub lp_mint_decimals: u8, + pub mint_0_decimals: u8, + pub mint_1_decimals: u8, + pub lp_supply: u64, + pub protocol_fees_token_0: u64, + pub protocol_fees_token_1: u64, + pub fund_fees_token_0: u64, + pub fund_fees_token_1: u64, + pub open_time: u64, + pub recent_epoch: u64, + pub padding: [u64; 1], +} + +pub const OBSERVATION_NUM: usize = 2; + +#[derive(Default, Clone, Copy, AnchorSerialize, AnchorDeserialize, InitSpace, Debug)] +pub struct Observation { + pub block_timestamp: u64, + pub cumulative_token_0_price_x32: u128, + pub cumulative_token_1_price_x32: u128, +} + +#[derive(Default, Debug, InitSpace, RentFreeAccount)] +#[account] +pub struct ObservationState { + pub compression_info: Option, + pub initialized: bool, + pub observation_index: u16, + pub pool_id: Pubkey, + pub observations: [Observation; OBSERVATION_NUM], + pub padding: [u64; 4], +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs new file mode 100644 index 0000000000..e6e5ce452c --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs @@ -0,0 +1,83 @@ +//! Withdraw instruction with BurnCpi. + +use anchor_lang::prelude::*; +use anchor_spl::token::Token; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; +use light_token_sdk::token::BurnCpi; + +use super::states::*; + +#[derive(Accounts)] +pub struct Withdraw<'info> { + #[account(mut)] + pub owner: Signer<'info>, + + #[account( + seeds = [AUTH_SEED.as_bytes()], + bump, + )] + pub authority: UncheckedAccount<'info>, + + #[account(mut)] + pub pool_state: Box>, + + #[account(mut)] + pub owner_lp_token: UncheckedAccount<'info>, + + #[account( + mut, + token::mint = vault_0_mint, + token::authority = owner, + )] + pub token_0_account: Box>, + + #[account( + mut, + token::mint = vault_1_mint, + token::authority = owner, + )] + pub token_1_account: Box>, + + #[account( + mut, + constraint = token_0_vault.key() == pool_state.token_0_vault, + )] + pub token_0_vault: Box>, + + #[account( + mut, + constraint = token_1_vault.key() == pool_state.token_1_vault, + )] + pub token_1_vault: Box>, + + #[account(address = pool_state.token_0_mint)] + pub vault_0_mint: Box>, + + #[account(address = pool_state.token_1_mint)] + pub vault_1_mint: Box>, + + #[account( + mut, + constraint = lp_mint.key() == pool_state.lp_mint, + )] + pub lp_mint: UncheckedAccount<'info>, + + pub token_program: Program<'info, Token>, + pub token_program_2022: Interface<'info, TokenInterface>, + pub system_program: Program<'info, System>, +} + +/// Withdraw instruction handler with BurnCpi. +pub fn process_withdraw(ctx: Context, lp_token_amount: u64) -> Result<()> { + // Burn LP tokens from owner using BurnCpi + BurnCpi { + source: ctx.accounts.owner_lp_token.to_account_info(), + mint: ctx.accounts.lp_mint.to_account_info(), + amount: lp_token_amount, + authority: ctx.accounts.owner.to_account_info(), + max_top_up: None, + } + .invoke()?; + + 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 0b43b352c4..676abac7cb 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -5,11 +5,14 @@ use light_sdk::{derive_light_cpi_signer, derive_light_rent_sponsor_pda}; use light_sdk_macros::rentfree_program; use light_sdk_types::CpiSigner; +pub mod amm_test; pub mod d5_markers; pub mod errors; pub mod instruction_accounts; pub mod instructions; +pub mod processors; pub mod state; +pub use amm_test::*; pub use d5_markers::*; pub use instruction_accounts::*; pub use state::{ @@ -46,6 +49,7 @@ pub mod csdk_anchor_full_derived_test { #![allow(clippy::too_many_arguments)] use super::{ + amm_test::{Deposit, InitializeParams, InitializePool, Withdraw}, d5_markers::{D5RentfreeBare, D5RentfreeBareParams}, instruction_accounts::CreatePdasAndMintAuto, FullAutoWithMintParams, LIGHT_CPI_SIGNER, @@ -136,13 +140,31 @@ pub mod csdk_anchor_full_derived_test { } /// Second instruction to test #[rentfree_program] with multiple instructions. + /// Delegates to nested processor in separate module. pub fn create_single_record<'info>( ctx: Context<'_, '_, '_, 'info, D5RentfreeBare<'info>>, params: D5RentfreeBareParams, ) -> Result<()> { - let record = &mut ctx.accounts.record; - record.owner = params.owner; - record.counter = 0; - Ok(()) + crate::processors::process_create_single_record(ctx, params) + } + + /// AMM initialize instruction with all rentfree markers. + /// Tests: 2x #[rentfree], 2x #[rentfree_token], 1x #[light_mint], + /// CreateTokenAccountCpi.rent_free(), CreateTokenAtaCpi.rent_free(), MintToCpi + pub fn initialize_pool<'info>( + ctx: Context<'_, '_, '_, 'info, InitializePool<'info>>, + params: InitializeParams, + ) -> Result<()> { + crate::amm_test::process_initialize_pool(ctx, params) + } + + /// AMM deposit instruction with MintToCpi. + pub fn deposit(ctx: Context, lp_token_amount: u64) -> Result<()> { + crate::amm_test::process_deposit(ctx, lp_token_amount) + } + + /// AMM withdraw instruction with BurnCpi. + pub fn withdraw(ctx: Context, lp_token_amount: u64) -> Result<()> { + crate::amm_test::process_withdraw(ctx, lp_token_amount) } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/processors/create_single_record.rs b/sdk-tests/csdk-anchor-full-derived-test/src/processors/create_single_record.rs new file mode 100644 index 0000000000..e0804c0ea1 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/processors/create_single_record.rs @@ -0,0 +1,17 @@ +//! Processor for create_single_record instruction. + +use anchor_lang::prelude::*; + +use crate::d5_markers::{D5RentfreeBare, D5RentfreeBareParams}; + +/// Process the create_single_record instruction. +/// Called by the instruction handler in the program module. +pub fn process_create_single_record( + ctx: Context<'_, '_, '_, '_, D5RentfreeBare<'_>>, + params: D5RentfreeBareParams, +) -> Result<()> { + let record = &mut ctx.accounts.record; + record.owner = params.owner; + record.counter = 0; + Ok(()) +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/processors/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/processors/mod.rs new file mode 100644 index 0000000000..b6b1504d17 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/src/processors/mod.rs @@ -0,0 +1,9 @@ +//! Processor functions called by instruction handlers. +//! +//! This module demonstrates the nested processor pattern where +//! instruction handlers in the program module delegate to +//! processor functions in separate modules. + +mod create_single_record; + +pub use create_single_record::process_create_single_record; From 6493ea8076ec82e8c52c3f193811a4e3613f9973 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 22:08:44 +0000 Subject: [PATCH 6/7] fix amm test --- Cargo.lock | 4 +- sdk-libs/macros/docs/rentfree.md | 3 +- .../src/rentfree/accounts/light_mint.rs | 59 +- .../csdk-anchor-full-derived-test/Cargo.toml | 2 +- .../src/amm_test/deposit.rs | 4 +- .../src/amm_test/initialize.rs | 36 +- .../src/amm_test/withdraw.rs | 4 +- .../src/instruction_accounts.rs | 2 +- .../tests/amm_test.rs | 633 ++++++++++++++++++ .../tests/shared.rs | 137 ++++ 10 files changed, 855 insertions(+), 29 deletions(-) create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs diff --git a/Cargo.lock b/Cargo.lock index 85eafabf0d..8531f685da 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=d8a2b3d9#d8a2b3d99d61ef900d1f6cdaabcef14eb9af6279" +source = "git+https://github.com/lightprotocol/anchor?rev=4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3#4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3" 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=d8a2b3d9)", + "anchor-spl 0.31.1 (git+https://github.com/lightprotocol/anchor?rev=4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3)", "bincode", "borsh 0.10.4", "light-client", diff --git a/sdk-libs/macros/docs/rentfree.md b/sdk-libs/macros/docs/rentfree.md index 5b449e92ce..24ae6cf515 100644 --- a/sdk-libs/macros/docs/rentfree.md +++ b/sdk-libs/macros/docs/rentfree.md @@ -87,8 +87,9 @@ Creates a compressed mint with automatic decompression. mint_signer = mint_signer, // AccountInfo that seeds the mint PDA (required) authority = authority, // Mint authority (required) decimals = 9, // Token decimals (required) + mint_seeds = &[b"mint", &[bump]], // PDA signer seeds for mint_signer (required) freeze_authority = freeze_auth, // Optional freeze authority - signer_seeds = &[b"mint", &[bump]], // Optional PDA signer seeds + authority_seeds = &[b"auth", &[auth_bump]], // PDA signer seeds for authority (optional - if not provided, authority must be a tx signer) rent_payment = 2, // Rent payment epochs (default: 2) write_top_up = 0 // Write top-up lamports (default: 0) )] diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index 62a19f78e9..cbdbebfa40 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -6,8 +6,8 @@ //! //! ## Parsed Attributes //! -//! Required: `mint_signer`, `authority`, `decimals` -//! Optional: `address_tree_info`, `freeze_authority`, `signer_seeds`, `rent_payment`, `write_top_up` +//! Required: `mint_signer`, `authority`, `decimals`, `mint_seeds` +//! Optional: `address_tree_info`, `freeze_authority`, `authority_seeds`, `rent_payment`, `write_top_up` //! //! ## Code Generation //! @@ -43,8 +43,10 @@ pub(super) struct LightMintField { pub address_tree_info: Expr, /// Optional freeze authority pub freeze_authority: Option, - /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) - pub signer_seeds: Option, + /// Signer seeds for the mint_signer PDA (required) + pub mint_seeds: Expr, + /// Signer seeds for the authority PDA (optional - if not provided, authority must be a tx signer) + pub authority_seeds: Option, /// Rent payment epochs for decompression (default: 2) pub rent_payment: Option, /// Write top-up lamports for decompression (default: 0) @@ -54,7 +56,7 @@ pub(super) struct LightMintField { /// Arguments inside #[light_mint(...)] parsed by darling. /// /// Required fields (darling auto-validates): mint_signer, authority, decimals -/// Optional fields: address_tree_info, freeze_authority, signer_seeds, rent_payment, write_top_up +/// Optional fields: address_tree_info, freeze_authority, mint_seeds, authority_seeds, rent_payment, write_top_up #[derive(FromMeta)] struct LightMintArgs { /// The mint_signer field (AccountInfo that seeds the mint PDA) - REQUIRED @@ -69,9 +71,11 @@ struct LightMintArgs { /// Optional freeze authority (field name, e.g., `freeze_authority = freeze_auth`) #[darling(default)] freeze_authority: Option, - /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) + /// Signer seeds for the mint_signer PDA (required) + mint_seeds: MetaExpr, + /// Signer seeds for the authority PDA (optional - if not provided, authority must be a tx signer) #[darling(default)] - signer_seeds: Option, + authority_seeds: Option, /// Rent payment epochs for decompression #[darling(default)] rent_payment: Option, @@ -104,7 +108,8 @@ pub(super) fn parse_light_mint_attr( decimals: args.decimals.into(), address_tree_info, freeze_authority: args.freeze_authority, - signer_seeds: args.signer_seeds.map(Into::into), + mint_seeds: args.mint_seeds.into(), + authority_seeds: args.authority_seeds.map(Into::into), rent_payment: args.rent_payment.map(Into::into), write_top_up: args.write_top_up.map(Into::into), })); @@ -267,7 +272,8 @@ fn generate_mint_invocation(builder: &LightMintBuilder) -> TokenStream { let infra = &builder.infra; // 2. Generate optional field expressions - let signer_seeds = quote_option_or(&mint.signer_seeds, quote! { &[] as &[&[u8]] }); + let mint_seeds = &mint.mint_seeds; + let authority_seeds = &mint.authority_seeds; let freeze_authority = mint .freeze_authority .as_ref() @@ -301,6 +307,34 @@ fn generate_mint_invocation(builder: &LightMintBuilder) -> TokenStream { data_binding, } = cpi; + // Generate invoke_signed call with appropriate signer seeds + let invoke_signed_call = match authority_seeds { + Some(auth_seeds) => { + quote! { + let authority_seeds: &[&[u8]] = #auth_seeds; + anchor_lang::solana_program::program::invoke_signed( + &mint_action_ix, + &account_infos, + &[mint_seeds, authority_seeds] + )?; + } + } + None => { + // authority_seeds not provided - authority must be a transaction signer + 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] + )?; + } + } + }; + // ------------------------------------------------------------------------- // Generated code block for mint_action CPI invocation. // @@ -319,7 +353,8 @@ fn generate_mint_invocation(builder: &LightMintBuilder) -> TokenStream { // #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) - // #signer_seeds - PDA signer seeds (default: &[] as &[&[u8]]) + // #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, @@ -414,8 +449,8 @@ fn generate_mint_invocation(builder: &LightMintBuilder) -> TokenStream { account_infos.push(self.#fee_payer.to_account_info()); // Step 10: Invoke CPI with signer seeds - let signer_seeds: &[&[u8]] = #signer_seeds; - anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; + let mint_seeds: &[&[u8]] = #mint_seeds; + #invoke_signed_call } } } diff --git a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml index 1e92999796..4941c8cfb6 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml +++ b/sdk-tests/csdk-anchor-full-derived-test/Cargo.toml @@ -30,7 +30,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 = "d8a2b3d9", features = ["memo", "metadata", "idl-build"] } +anchor-spl = { version = "=0.31.1", git = "https://github.com/lightprotocol/anchor", rev = "4e8ff59a7de5b2a8ba37f88cf7b0431999f1b1f3", 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/amm_test/deposit.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs index ba65cd2fef..6d5f733314 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/deposit.rs @@ -1,7 +1,6 @@ //! Deposit instruction with MintToCpi. use anchor_lang::prelude::*; -use anchor_spl::token::Token; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use light_token_sdk::token::MintToCpi; @@ -13,6 +12,7 @@ pub struct Deposit<'info> { pub owner: Signer<'info>, #[account( + mut, seeds = [AUTH_SEED.as_bytes()], bump, )] @@ -62,7 +62,7 @@ pub struct Deposit<'info> { )] pub lp_mint: UncheckedAccount<'info>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, pub token_program_2022: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } 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 49708d6572..d9712eff94 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 @@ -9,11 +9,7 @@ //! - MintToCpi use anchor_lang::prelude::*; -use anchor_spl::{ - associated_token::AssociatedToken, - token::Token, - token_interface::{Mint, TokenAccount, TokenInterface}, -}; +use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use light_compressible::CreateAccountsProof; use light_sdk_macros::RentFree; use light_token_sdk::token::{ @@ -31,6 +27,7 @@ pub struct InitializeParams { pub create_accounts_proof: CreateAccountsProof, pub lp_mint_signer_bump: u8, pub creator_lp_token_bump: u8, + pub authority_bump: u8, } #[derive(Accounts, RentFree)] @@ -43,6 +40,7 @@ pub struct InitializePool<'info> { pub amm_config: AccountInfo<'info>, #[account( + mut, seeds = [AUTH_SEED.as_bytes()], bump, )] @@ -83,7 +81,8 @@ pub struct InitializePool<'info> { mint_signer = lp_mint_signer, authority = authority, decimals = 9, - signer_seeds = &[POOL_LP_MINT_SIGNER_SEED, self.pool_state.to_account_info().key.as_ref(), &[params.lp_mint_signer_bump]] + 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]] )] pub lp_mint: UncheckedAccount<'info>, @@ -138,10 +137,11 @@ pub struct InitializePool<'info> { #[rentfree] pub observation_state: Box>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, pub token_0_program: Interface<'info, TokenInterface>, pub token_1_program: Interface<'info, TokenInterface>, - pub associated_token_program: Program<'info, AssociatedToken>, + /// CHECK: Associated token program (SPL ATA or Light Token). + pub associated_token_program: UncheckedAccount<'info>, pub system_program: Program<'info, System>, pub rent: Sysvar<'info, Rent>, @@ -234,5 +234,25 @@ pub fn process_initialize_pool<'info>( } .invoke_signed(&[&[AUTH_SEED.as_bytes(), &[ctx.bumps.authority]]])?; + // Populate pool state + let pool_state = &mut ctx.accounts.pool_state; + pool_state.amm_config = ctx.accounts.amm_config.key(); + pool_state.pool_creator = ctx.accounts.creator.key(); + pool_state.token_0_vault = ctx.accounts.token_0_vault.key(); + pool_state.token_1_vault = ctx.accounts.token_1_vault.key(); + pool_state.lp_mint = ctx.accounts.lp_mint.key(); + pool_state.token_0_mint = ctx.accounts.token_0_mint.key(); + pool_state.token_1_mint = ctx.accounts.token_1_mint.key(); + pool_state.token_0_program = ctx.accounts.token_0_program.key(); + pool_state.token_1_program = ctx.accounts.token_1_program.key(); + pool_state.observation_key = ctx.accounts.observation_state.key(); + pool_state.auth_bump = ctx.bumps.authority; + pool_state.status = 1; // Active + pool_state.lp_mint_decimals = 9; + pool_state.mint_0_decimals = 9; + pool_state.mint_1_decimals = 9; + pool_state.lp_supply = lp_amount; + pool_state.open_time = params.open_time; + Ok(()) } diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs index e6e5ce452c..0ca248350f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/withdraw.rs @@ -1,7 +1,6 @@ //! Withdraw instruction with BurnCpi. use anchor_lang::prelude::*; -use anchor_spl::token::Token; use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface}; use light_token_sdk::token::BurnCpi; @@ -13,6 +12,7 @@ pub struct Withdraw<'info> { pub owner: Signer<'info>, #[account( + mut, seeds = [AUTH_SEED.as_bytes()], bump, )] @@ -62,7 +62,7 @@ pub struct Withdraw<'info> { )] pub lp_mint: UncheckedAccount<'info>, - pub token_program: Program<'info, Token>, + pub token_program: Interface<'info, TokenInterface>, pub token_program_2022: Interface<'info, TokenInterface>, pub system_program: Program<'info, System>, } 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 dc76c417d0..cc16540a9b 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 @@ -75,7 +75,7 @@ pub struct CreatePdasAndMintAuto<'info> { mint_signer = mint_signer, authority = mint_authority, decimals = 9, - signer_seeds = &[LP_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] + mint_seeds = &[LP_MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref(), &[params.mint_signer_bump]] )] pub cmint: UncheckedAccount<'info>, 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 new file mode 100644 index 0000000000..327350c58b --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/amm_test.rs @@ -0,0 +1,633 @@ +/// AMM Full Lifecycle Integration Test +/// +/// Tests the complete AMM flow: +/// 1. Initialize pool with rent-free PDAs and LP mint +/// 2. Deposit tokens and receive LP tokens +/// 3. Withdraw tokens by burning LP tokens +/// 4. Advance epochs to trigger auto-compression +/// 5. Decompress all accounts +/// 6. Deposit after decompression to verify pool works + +mod shared; + +use anchor_lang::{InstructionData, ToAccountMetas}; +use light_compressible::rent::SLOTS_PER_EPOCH; +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 light_token_interface::state::Token; +use light_token_sdk::token::{ + find_mint_address, get_associated_token_address_and_bump, 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; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +use csdk_anchor_full_derived_test::amm_test::{ + InitializeParams, AUTH_SEED, OBSERVATION_SEED, POOL_LP_MINT_SIGNER_SEED, POOL_SEED, + POOL_VAULT_SEED, +}; + +const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); + +// ============================================================================= +// Assertion Helpers +// ============================================================================= + +async fn assert_onchain_exists(rpc: &mut LightProgramTest, pda: &Pubkey) { + assert!( + rpc.get_account(*pda).await.unwrap().is_some(), + "Account {} should exist on-chain", + pda + ); +} + +async fn assert_onchain_closed(rpc: &mut LightProgramTest, pda: &Pubkey) { + let acc = rpc.get_account(*pda).await.unwrap(); + assert!( + acc.is_none() || acc.unwrap().lamports == 0, + "Account {} should be closed", + pda + ); +} + +fn parse_token(data: &[u8]) -> Token { + borsh::BorshDeserialize::deserialize(&mut &data[..]).unwrap() +} + +async fn assert_compressed_exists_with_data(rpc: &mut LightProgramTest, addr: [u8; 32]) { + let acc = rpc + .get_compressed_account(addr, None) + .await + .unwrap() + .value + .unwrap(); + assert_eq!(acc.address.unwrap(), addr); + assert!(!acc.data.as_ref().unwrap().data.is_empty()); +} + +async fn assert_compressed_token_exists( + rpc: &mut LightProgramTest, + owner: &Pubkey, + expected_amount: u64, +) { + let accs = rpc + .get_compressed_token_accounts_by_owner(owner, None, None) + .await + .unwrap() + .value + .items; + assert!(!accs.is_empty(), "Compressed token account should exist"); + assert_eq!( + accs[0].token.amount, expected_amount, + "Compressed token amount mismatch" + ); +} + +/// Stores all AMM-related PDAs +struct AmmPdas { + pool_state: Pubkey, + #[allow(dead_code)] + pool_state_bump: u8, + observation_state: Pubkey, + #[allow(dead_code)] + observation_state_bump: u8, + authority: Pubkey, + #[allow(dead_code)] + authority_bump: u8, + token_0_vault: Pubkey, + #[allow(dead_code)] + token_0_vault_bump: u8, + token_1_vault: Pubkey, + #[allow(dead_code)] + token_1_vault_bump: u8, + lp_mint_signer: Pubkey, + lp_mint_signer_bump: u8, + lp_mint: Pubkey, + creator_lp_token: Pubkey, + creator_lp_token_bump: u8, +} + +/// Context for AMM tests +struct AmmTestContext { + rpc: LightProgramTest, + payer: Keypair, + config_pda: Pubkey, + program_id: Pubkey, + token_0_mint: Pubkey, + token_1_mint: Pubkey, + creator: Keypair, + creator_token_0: Pubkey, + creator_token_1: Pubkey, + amm_config: Keypair, +} + +/// Setup the test environment with light mints +async fn setup() -> AmmTestContext { + 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(); + + // Setup mock program data and compression config + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + let (init_config_ix, config_pda) = InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + // Create creator keypair and fund + let creator = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &creator.pubkey(), 10_000_000_000) + .await + .unwrap(); + + // Create two light mints (cmints) for token_0 and token_1 + // Using shared::setup_create_mint which creates both compressed mint and on-chain Mint account + let (mint_a, _compression_addr_a, ata_pubkeys_a, _mint_seed_a) = shared::setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), // mint_authority + 9, // decimals + vec![(10_000_000, creator.pubkey())], // mint to creator + ) + .await; + + let (mint_b, _compression_addr_b, ata_pubkeys_b, _mint_seed_b) = shared::setup_create_mint( + &mut rpc, + &payer, + payer.pubkey(), // mint_authority + 9, // decimals + vec![(10_000_000, creator.pubkey())], // mint to creator + ) + .await; + + // Ensure proper ordering: token_0_mint.key() < token_1_mint.key() + let (token_0_mint, token_1_mint, creator_token_0, creator_token_1) = if mint_a < mint_b { + (mint_a, mint_b, ata_pubkeys_a[0], ata_pubkeys_b[0]) + } else { + (mint_b, mint_a, ata_pubkeys_b[0], ata_pubkeys_a[0]) + }; + + // Create amm_config account (simple funded account for this test) + let amm_config = Keypair::new(); + light_test_utils::airdrop_lamports(&mut rpc, &amm_config.pubkey(), 1_000_000) + .await + .unwrap(); + + AmmTestContext { + rpc, + payer, + config_pda, + program_id, + token_0_mint, + token_1_mint, + creator, + creator_token_0, + creator_token_1, + amm_config, + } +} + +/// Derive all AMM PDAs +fn derive_amm_pdas( + program_id: &Pubkey, + amm_config: &Pubkey, + token_0_mint: &Pubkey, + token_1_mint: &Pubkey, + creator: &Pubkey, +) -> AmmPdas { + // Pool state: seeds = [POOL_SEED, amm_config, token_0_mint, token_1_mint] + let (pool_state, pool_state_bump) = Pubkey::find_program_address( + &[ + POOL_SEED.as_bytes(), + amm_config.as_ref(), + token_0_mint.as_ref(), + token_1_mint.as_ref(), + ], + program_id, + ); + + // Authority: seeds = [AUTH_SEED] + let (authority, authority_bump) = + Pubkey::find_program_address(&[AUTH_SEED.as_bytes()], program_id); + + // Observation: seeds = [OBSERVATION_SEED, pool_state] + let (observation_state, observation_state_bump) = Pubkey::find_program_address( + &[OBSERVATION_SEED.as_bytes(), pool_state.as_ref()], + program_id, + ); + + // Vault 0: seeds = [POOL_VAULT_SEED, pool_state, token_0_mint] + let (token_0_vault, token_0_vault_bump) = Pubkey::find_program_address( + &[ + POOL_VAULT_SEED.as_bytes(), + pool_state.as_ref(), + token_0_mint.as_ref(), + ], + program_id, + ); + + // Vault 1: seeds = [POOL_VAULT_SEED, pool_state, token_1_mint] + let (token_1_vault, token_1_vault_bump) = Pubkey::find_program_address( + &[ + POOL_VAULT_SEED.as_bytes(), + pool_state.as_ref(), + token_1_mint.as_ref(), + ], + program_id, + ); + + // LP mint signer: seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state] + let (lp_mint_signer, lp_mint_signer_bump) = Pubkey::find_program_address( + &[POOL_LP_MINT_SIGNER_SEED, pool_state.as_ref()], + program_id, + ); + + // LP mint: derived from lp_mint_signer using find_mint_address + let (lp_mint, _) = find_mint_address(&lp_mint_signer); + + // Creator LP token ATA: using get_associated_token_address_and_bump + let (creator_lp_token, creator_lp_token_bump) = + get_associated_token_address_and_bump(creator, &lp_mint); + + AmmPdas { + pool_state, + pool_state_bump, + observation_state, + observation_state_bump, + authority, + authority_bump, + token_0_vault, + token_0_vault_bump, + token_1_vault, + token_1_vault_bump, + lp_mint_signer, + lp_mint_signer_bump, + lp_mint, + creator_lp_token, + creator_lp_token_bump, + } +} + +/// AMM full lifecycle integration test +#[tokio::test] +async fn test_amm_full_lifecycle() { + // ========================================================================== + // PHASE 1: Setup + // ========================================================================== + let mut ctx = setup().await; + + // ========================================================================== + // PHASE 2: Derive PDAs + // ========================================================================== + let pdas = derive_amm_pdas( + &ctx.program_id, + &ctx.amm_config.pubkey(), + &ctx.token_0_mint, + &ctx.token_1_mint, + &ctx.creator.pubkey(), + ); + + println!("Derived PDAs:"); + println!(" pool_state: {}", pdas.pool_state); + println!(" observation_state: {}", pdas.observation_state); + println!(" authority: {}", pdas.authority); + println!(" token_0_vault: {}", pdas.token_0_vault); + println!(" token_1_vault: {}", pdas.token_1_vault); + println!(" lp_mint_signer: {}", pdas.lp_mint_signer); + println!(" lp_mint: {}", pdas.lp_mint); + println!(" creator_lp_token: {}", pdas.creator_lp_token); + + // ========================================================================== + // PHASE 3: Get create accounts proof + // ========================================================================== + let proof_result = get_create_accounts_proof( + &ctx.rpc, + &ctx.program_id, + vec![ + CreateAccountsProofInput::pda(pdas.pool_state), + CreateAccountsProofInput::pda(pdas.observation_state), + CreateAccountsProofInput::mint(pdas.lp_mint_signer), + ], + ) + .await + .unwrap(); + + // ========================================================================== + // PHASE 4: Initialize Pool + // ========================================================================== + let init_amount_0 = 1000u64; + let init_amount_1 = 1000u64; + let open_time = 0u64; + + let init_params = InitializeParams { + init_amount_0, + init_amount_1, + open_time, + create_accounts_proof: proof_result.create_accounts_proof, + lp_mint_signer_bump: pdas.lp_mint_signer_bump, + creator_lp_token_bump: pdas.creator_lp_token_bump, + authority_bump: pdas.authority_bump, + }; + + let accounts = csdk_anchor_full_derived_test::accounts::InitializePool { + creator: ctx.creator.pubkey(), + amm_config: ctx.amm_config.pubkey(), + authority: pdas.authority, + pool_state: pdas.pool_state, + token_0_mint: ctx.token_0_mint, + token_1_mint: ctx.token_1_mint, + lp_mint_signer: pdas.lp_mint_signer, + lp_mint: pdas.lp_mint, + creator_token_0: ctx.creator_token_0, + creator_token_1: ctx.creator_token_1, + creator_lp_token: pdas.creator_lp_token, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + observation_state: pdas.observation_state, + token_program: LIGHT_TOKEN_PROGRAM_ID, + token_0_program: LIGHT_TOKEN_PROGRAM_ID, + token_1_program: LIGHT_TOKEN_PROGRAM_ID, + associated_token_program: LIGHT_TOKEN_PROGRAM_ID, + system_program: solana_sdk::system_program::ID, + rent: solana_sdk::sysvar::rent::ID, + compression_config: ctx.config_pda, + ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, + ctoken_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + ctoken_cpi_authority: LIGHT_TOKEN_CPI_AUTHORITY, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::InitializePool { + params: init_params, + }; + + 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, &ctx.creator], + ) + .await + .expect("Initialize pool should succeed"); + + // ========================================================================== + // PHASE 5: Verify Initial State (assert_after_initialize) + // ========================================================================== + 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 creator LP token balance (should have initial LP amount from initialize) + let lp_token_data = parse_token( + &ctx.rpc + .get_account(pdas.creator_lp_token) + .await + .unwrap() + .unwrap() + .data, + ); + let initial_lp_balance = lp_token_data.amount; + assert!( + initial_lp_balance > 0, + "Creator should have received LP tokens" + ); + println!("Initial LP balance: {}", initial_lp_balance); + + // ========================================================================== + // PHASE 6: Deposit + // ========================================================================== + let deposit_amount = 500u64; + + let deposit_accounts = csdk_anchor_full_derived_test::accounts::Deposit { + owner: ctx.creator.pubkey(), + authority: pdas.authority, + pool_state: pdas.pool_state, + owner_lp_token: pdas.creator_lp_token, + token_0_account: ctx.creator_token_0, + token_1_account: ctx.creator_token_1, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + vault_0_mint: ctx.token_0_mint, + vault_1_mint: ctx.token_1_mint, + lp_mint: pdas.lp_mint, + token_program: LIGHT_TOKEN_PROGRAM_ID, + token_program_2022: LIGHT_TOKEN_PROGRAM_ID, + system_program: solana_sdk::system_program::ID, + }; + + let deposit_instruction_data = csdk_anchor_full_derived_test::instruction::Deposit { + lp_token_amount: deposit_amount, + }; + + let deposit_instruction = Instruction { + program_id: ctx.program_id, + accounts: deposit_accounts.to_account_metas(None), + data: deposit_instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction( + &[deposit_instruction], + &ctx.payer.pubkey(), + &[&ctx.payer, &ctx.creator], + ) + .await + .expect("Deposit should succeed"); + + // Verify LP balance after deposit (assert_after_deposit) + let lp_token_data_after_deposit = parse_token( + &ctx.rpc + .get_account(pdas.creator_lp_token) + .await + .unwrap() + .unwrap() + .data, + ); + let expected_balance_after_deposit = initial_lp_balance + deposit_amount; + assert_eq!( + lp_token_data_after_deposit.amount, expected_balance_after_deposit, + "LP balance should increase after deposit" + ); + println!( + "LP balance after deposit: {} (expected: {})", + lp_token_data_after_deposit.amount, expected_balance_after_deposit + ); + + // ========================================================================== + // PHASE 7: Withdraw + // ========================================================================== + let withdraw_amount = 200u64; + + let withdraw_accounts = csdk_anchor_full_derived_test::accounts::Withdraw { + owner: ctx.creator.pubkey(), + authority: pdas.authority, + pool_state: pdas.pool_state, + owner_lp_token: pdas.creator_lp_token, + token_0_account: ctx.creator_token_0, + token_1_account: ctx.creator_token_1, + token_0_vault: pdas.token_0_vault, + token_1_vault: pdas.token_1_vault, + vault_0_mint: ctx.token_0_mint, + vault_1_mint: ctx.token_1_mint, + lp_mint: pdas.lp_mint, + token_program: LIGHT_TOKEN_PROGRAM_ID, + token_program_2022: LIGHT_TOKEN_PROGRAM_ID, + system_program: solana_sdk::system_program::ID, + }; + + let withdraw_instruction_data = csdk_anchor_full_derived_test::instruction::Withdraw { + lp_token_amount: withdraw_amount, + }; + + let withdraw_instruction = Instruction { + program_id: ctx.program_id, + accounts: withdraw_accounts.to_account_metas(None), + data: withdraw_instruction_data.data(), + }; + + ctx.rpc + .create_and_send_transaction( + &[withdraw_instruction], + &ctx.payer.pubkey(), + &[&ctx.payer, &ctx.creator], + ) + .await + .expect("Withdraw should succeed"); + + // Verify LP balance after withdraw (assert_after_withdraw) + let lp_token_data_after_withdraw = parse_token( + &ctx.rpc + .get_account(pdas.creator_lp_token) + .await + .unwrap() + .unwrap() + .data, + ); + let expected_balance_after_withdraw = expected_balance_after_deposit - withdraw_amount; + assert_eq!( + lp_token_data_after_withdraw.amount, expected_balance_after_withdraw, + "LP balance should decrease after withdraw" + ); + println!( + "LP balance after withdraw: {} (expected: {})", + lp_token_data_after_withdraw.amount, expected_balance_after_withdraw + ); + + // ========================================================================== + // PHASE 8: Advance Epochs (trigger auto-compression) + // ========================================================================== + println!("\nAdvancing epochs to trigger auto-compression..."); + ctx.rpc + .warp_slot_forward(SLOTS_PER_EPOCH * 30) + .await + .unwrap(); + + // Derive compressed addresses for verification + let address_tree_pubkey = ctx.rpc.get_address_tree_v2().tree; + + let pool_compressed_address = light_compressed_account::address::derive_address( + &pdas.pool_state.to_bytes(), + &address_tree_pubkey.to_bytes(), + &ctx.program_id.to_bytes(), + ); + let observation_compressed_address = light_compressed_account::address::derive_address( + &pdas.observation_state.to_bytes(), + &address_tree_pubkey.to_bytes(), + &ctx.program_id.to_bytes(), + ); + let mint_compressed_address = + light_token_sdk::compressed_token::create_compressed_mint::derive_mint_compressed_address( + &pdas.lp_mint_signer, + &address_tree_pubkey, + ); + + // Assert compression (assert_after_compression) + assert_onchain_closed(&mut ctx.rpc, &pdas.pool_state).await; + assert_onchain_closed(&mut ctx.rpc, &pdas.observation_state).await; + assert_onchain_closed(&mut ctx.rpc, &pdas.lp_mint).await; + assert_onchain_closed(&mut ctx.rpc, &pdas.token_0_vault).await; + assert_onchain_closed(&mut ctx.rpc, &pdas.token_1_vault).await; + assert_onchain_closed(&mut ctx.rpc, &pdas.creator_lp_token).await; + + // Verify compressed accounts exist with non-empty data + assert_compressed_exists_with_data(&mut ctx.rpc, pool_compressed_address).await; + assert_compressed_exists_with_data(&mut ctx.rpc, observation_compressed_address).await; + assert_compressed_exists_with_data(&mut ctx.rpc, mint_compressed_address).await; + + // Verify compressed token accounts + assert_compressed_token_exists(&mut ctx.rpc, &pdas.token_0_vault, 0).await; + assert_compressed_token_exists(&mut ctx.rpc, &pdas.token_1_vault, 0).await; + assert_compressed_token_exists( + &mut ctx.rpc, + &pdas.creator_lp_token, + expected_balance_after_withdraw, + ) + .await; + + println!("All accounts compressed successfully!"); + + // ========================================================================== + // 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!("\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)"); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs new file mode 100644 index 0000000000..200f6e4432 --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/shared.rs @@ -0,0 +1,137 @@ +// Shared test utilities for csdk-anchor-full-derived-test + +use light_client::{indexer::Indexer, rpc::Rpc}; +use solana_sdk::{pubkey::Pubkey, signature::Keypair, signer::Signer}; + +/// Setup helper: Creates a compressed mint directly using the ctoken SDK (not via wrapper program) +/// Optionally creates ATAs and mints tokens for each recipient. +/// Note: This decompresses the mint first, then uses MintTo to mint to ctoken accounts. +/// Returns (mint_pda, compression_address, ata_pubkeys, mint_seed_keypair) +#[allow(unused)] +pub async fn setup_create_mint( + rpc: &mut (impl Rpc + Indexer), + payer: &Keypair, + mint_authority: Pubkey, + decimals: u8, + recipients: Vec<(u64, Pubkey)>, +) -> (Pubkey, [u8; 32], Vec, Keypair) { + use light_token_sdk::token::{ + CreateAssociatedTokenAccount, CreateMint, CreateMintParams, MintTo, + }; + + let mint_seed = Keypair::new(); + let address_tree = rpc.get_address_tree_v2(); + let output_queue = rpc.get_random_state_tree_info().unwrap().queue; + + // Derive compression address using SDK helpers + let compression_address = light_token_sdk::token::derive_mint_compressed_address( + &mint_seed.pubkey(), + &address_tree.tree, + ); + + let (mint, bump) = light_token_sdk::token::find_mint_address(&mint_seed.pubkey()); + + // Get validity proof for the address + let rpc_result = rpc + .get_validity_proof( + vec![], + vec![light_client::indexer::AddressWithTree { + address: compression_address, + tree: address_tree.tree, + }], + None, + ) + .await + .unwrap() + .value; + + // Build params for the SDK + let params = CreateMintParams { + decimals, + address_merkle_tree_root_index: rpc_result.addresses[0].root_index, + mint_authority, + proof: rpc_result.proof.0.unwrap(), + compression_address, + mint, + bump, + freeze_authority: None, + extensions: None, + rent_payment: 16, + write_top_up: 766, + }; + + // Create instruction directly using SDK + let create_mint_builder = CreateMint::new( + params, + mint_seed.pubkey(), + payer.pubkey(), + address_tree.tree, + output_queue, + ); + let instruction = create_mint_builder.instruction().unwrap(); + + // Send transaction + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[payer, &mint_seed]) + .await + .unwrap(); + + // Verify the compressed mint was created + let compressed_account = rpc + .get_compressed_account(compression_address, None) + .await + .unwrap() + .value; + + assert!( + compressed_account.is_some(), + "Compressed mint should exist after setup" + ); + + // If no recipients, return early + if recipients.is_empty() { + return (mint, compression_address, vec![], mint_seed); + } + + // Create ATAs for each recipient + use light_token_sdk::token::derive_token_ata; + + let mut ata_pubkeys = Vec::with_capacity(recipients.len()); + + for (_amount, owner) in &recipients { + let (ata_address, _bump) = derive_token_ata(owner, &mint); + ata_pubkeys.push(ata_address); + + let create_ata = CreateAssociatedTokenAccount::new(payer.pubkey(), *owner, mint); + let ata_instruction = create_ata.instruction().unwrap(); + + rpc.create_and_send_transaction(&[ata_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + // Mint tokens to recipients with amount > 0 + let recipients_with_amount: Vec<_> = recipients + .iter() + .enumerate() + .filter(|(_, (amount, _))| *amount > 0) + .collect(); + + // Mint to each recipient using the decompressed Mint (CreateMint already decompresses) + for (idx, (amount, _)) in &recipients_with_amount { + let mint_instruction = MintTo { + mint, + destination: ata_pubkeys[*idx], + amount: *amount, + authority: mint_authority, + max_top_up: None, + } + .instruction() + .unwrap(); + + rpc.create_and_send_transaction(&[mint_instruction], &payer.pubkey(), &[payer]) + .await + .unwrap(); + } + + (mint, compression_address, ata_pubkeys, mint_seed) +} From 3e6fd1af0bd1ecbba3701e95cd390c443648bf52 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 22:13:36 +0000 Subject: [PATCH 7/7] format --- .../macros/src/rentfree/accounts/builder.rs | 4 +-- .../macros/src/rentfree/accounts/parse.rs | 2 +- .../macros/src/rentfree/program/decompress.rs | 6 ++--- .../src/rentfree/program/instructions.rs | 11 ++++---- .../src/rentfree/program/variant_enum.rs | 4 +-- .../src/amm_test/states.rs | 3 +-- .../src/state/d1_field_types/mod.rs | 10 +++---- .../src/state/d2_compress_as/mod.rs | 4 +-- .../src/state/d4_composition/mod.rs | 6 ++--- .../tests/amm_test.rs | 26 ++++++++----------- 10 files changed, 35 insertions(+), 41 deletions(-) diff --git a/sdk-libs/macros/src/rentfree/accounts/builder.rs b/sdk-libs/macros/src/rentfree/accounts/builder.rs index 0c8d5b3184..a6d0cee7bb 100644 --- a/sdk-libs/macros/src/rentfree/accounts/builder.rs +++ b/sdk-libs/macros/src/rentfree/accounts/builder.rs @@ -198,8 +198,8 @@ impl RentFreeBuilder { let mint = &self.parsed.light_mint_fields[0]; // Generate mint action invocation without CPI context - let mint_invocation = LightMintBuilder::new(mint, params_ident, &self.infra) - .generate_invocation(); + let mint_invocation = + LightMintBuilder::new(mint, params_ident, &self.infra).generate_invocation(); // Infrastructure field reference for quote! interpolation let fee_payer = &self.infra.fee_payer; diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index faa31aa648..fcf1932847 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::traits::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 c07b337e7b..f9f129820d 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -4,15 +4,13 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; -use crate::rentfree::shared_utils::qualify_type_with_crate; - use super::{ expr_traversal::transform_expr_for_ctx_seeds, parsing::{InstructionVariant, SeedElement, TokenSeedSpec}, seed_utils::ctx_fields_to_set, variant_enum::PdaCtxSeedInfo, }; -use crate::rentfree::shared_utils::is_constant_identifier; +use crate::rentfree::shared_utils::{is_constant_identifier, qualify_type_with_crate}; // ============================================================================= // DECOMPRESS CONTEXT IMPL @@ -251,7 +249,7 @@ pub fn generate_pda_seed_provider_impls( let variant_str = ctx_info.variant_name.to_string(); let spec = pda_seed_specs .iter() - .find(|s| s.variant.to_string() == variant_str) + .find(|s| s.variant == variant_str) .ok_or_else(|| { super::parsing::macro_error!( &ctx_info.variant_name, diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index cf76cef80a..87af7c562b 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -26,8 +26,10 @@ use super::{ }, variant_enum::PdaCtxSeedInfo, }; -use crate::rentfree::shared_utils::{ident_to_type, qualify_type_with_crate}; -use crate::utils::to_snake_case; +use crate::{ + rentfree::shared_utils::{ident_to_type, qualify_type_with_crate}, + utils::to_snake_case, +}; // ============================================================================= // MAIN CODEGEN @@ -93,9 +95,8 @@ fn codegen( }) .unwrap_or_default(); - let enum_and_traits = super::variant_enum::compressed_account_variant_with_ctx_seeds( - &pda_ctx_seeds, - )?; + 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)] diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index 18268b4286..d0d5659d15 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -48,8 +48,8 @@ pub fn compressed_account_variant_with_ctx_seeds( let inner_type = qualify_type_with_crate(&info.inner_type); let packed_variant_name = make_packed_variant_name(variant_name); // Create packed type (also qualified with crate::) - let packed_inner_type = make_packed_type(&info.inner_type) - .expect("inner_type should be a valid type path"); + 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; // Unpacked variant: Pubkey fields for ctx.* seeds diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs index e8e8cb2007..bcae3c176f 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/amm_test/states.rs @@ -1,8 +1,7 @@ //! AMM state structs adapted from cp-swap-reference. use anchor_lang::prelude::*; -use light_sdk::compressible::CompressionInfo; -use light_sdk::LightDiscriminator; +use light_sdk::{compressible::CompressionInfo, LightDiscriminator}; use light_sdk_macros::RentFreeAccount; pub const POOL_SEED: &str = "pool"; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs index cad591b53c..3273985fe3 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d1_field_types/mod.rs @@ -2,11 +2,11 @@ //! //! Tests `is_pubkey_type()`, `is_copy_type()`, and Pack generation code paths. -pub mod no_pubkey; -pub mod single_pubkey; +pub mod all; +pub mod arrays; pub mod multi_pubkey; +pub mod no_pubkey; pub mod non_copy; -pub mod option_pubkey; pub mod option_primitive; -pub mod arrays; -pub mod all; +pub mod option_pubkey; +pub mod single_pubkey; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs index 74a6655629..68fb05acee 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d2_compress_as/mod.rs @@ -3,7 +3,7 @@ //! Tests override value parsing in traits.rs. pub mod absent; -pub mod single; +pub mod all; pub mod multiple; pub mod option_none; -pub mod all; +pub mod single; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs index 70c5209ba8..6dd8e038a2 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/state/d4_composition/mod.rs @@ -2,7 +2,7 @@ //! //! Tests struct validation and size-based hash mode selection. -pub mod minimal; -pub mod large; -pub mod info_last; pub mod all; +pub mod info_last; +pub mod large; +pub mod minimal; 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 327350c58b..013ecb25ef 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 @@ -7,10 +7,13 @@ /// 4. Advance epochs to trigger auto-compression /// 5. Decompress all accounts /// 6. Deposit after decompression to verify pool works - 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 light_compressible::rent::SLOTS_PER_EPOCH; use light_compressible_client::{ get_create_accounts_proof, CreateAccountsProofInput, InitializeRentFreeConfig, @@ -30,11 +33,6 @@ use solana_keypair::Keypair; use solana_pubkey::Pubkey; use solana_signer::Signer; -use csdk_anchor_full_derived_test::amm_test::{ - InitializeParams, AUTH_SEED, OBSERVATION_SEED, POOL_LP_MINT_SIGNER_SEED, POOL_SEED, - POOL_VAULT_SEED, -}; - const RENT_SPONSOR: Pubkey = pubkey!("CLEuMG7pzJX9xAuKCFzBP154uiG1GaNo4Fq7x6KAcAfG"); // ============================================================================= @@ -168,8 +166,8 @@ async fn setup() -> AmmTestContext { let (mint_a, _compression_addr_a, ata_pubkeys_a, _mint_seed_a) = shared::setup_create_mint( &mut rpc, &payer, - payer.pubkey(), // mint_authority - 9, // decimals + payer.pubkey(), // mint_authority + 9, // decimals vec![(10_000_000, creator.pubkey())], // mint to creator ) .await; @@ -177,8 +175,8 @@ async fn setup() -> AmmTestContext { let (mint_b, _compression_addr_b, ata_pubkeys_b, _mint_seed_b) = shared::setup_create_mint( &mut rpc, &payer, - payer.pubkey(), // mint_authority - 9, // decimals + payer.pubkey(), // mint_authority + 9, // decimals vec![(10_000_000, creator.pubkey())], // mint to creator ) .await; @@ -260,10 +258,8 @@ fn derive_amm_pdas( ); // LP mint signer: seeds = [POOL_LP_MINT_SIGNER_SEED, pool_state] - let (lp_mint_signer, lp_mint_signer_bump) = Pubkey::find_program_address( - &[POOL_LP_MINT_SIGNER_SEED, pool_state.as_ref()], - program_id, - ); + let (lp_mint_signer, lp_mint_signer_bump) = + Pubkey::find_program_address(&[POOL_LP_MINT_SIGNER_SEED, pool_state.as_ref()], program_id); // LP mint: derived from lp_mint_signer using find_mint_address let (lp_mint, _) = find_mint_address(&lp_mint_signer); @@ -376,7 +372,7 @@ async fn test_amm_full_lifecycle() { compression_config: ctx.config_pda, ctoken_compressible_config: COMPRESSIBLE_CONFIG_V1, ctoken_rent_sponsor: LIGHT_TOKEN_RENT_SPONSOR, - light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_program: LIGHT_TOKEN_PROGRAM_ID, ctoken_cpi_authority: LIGHT_TOKEN_CPI_AUTHORITY, };