From 0081c8891bdef469b6443c37b74023b7b2f42079 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 00:33:43 +0000 Subject: [PATCH 1/9] refactor: switch from file_scanner to crate_context for module parsing Replace brute-force file scanning with Anchor-style module following. CrateContext parses only declared modules via `mod xxx;` statements instead of scanning all .rs files recursively. --- .../macros/src/compressible/anchor_seeds.rs | 8 +- .../macros/src/compressible/crate_context.rs | 280 ++++++++++++++++++ .../macros/src/compressible/file_scanner.rs | 188 ------------ .../macros/src/compressible/instructions.rs | 38 +-- sdk-libs/macros/src/compressible/mod.rs | 2 +- sdk-libs/macros/src/finalize/parse.rs | 196 ++++++++---- 6 files changed, 438 insertions(+), 274 deletions(-) create mode 100644 sdk-libs/macros/src/compressible/crate_context.rs delete mode 100644 sdk-libs/macros/src/compressible/file_scanner.rs diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs index f80596a6eb..bcd254495a 100644 --- a/sdk-libs/macros/src/compressible/anchor_seeds.rs +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -196,7 +196,7 @@ struct RentFreeTokenAttr { /// Convert snake_case field name to CamelCase variant name /// e.g., token_0_vault -> Token0Vault, vault -> Vault -fn snake_to_camel_case(s: &str) -> String { +pub fn snake_to_camel_case(s: &str) -> String { s.split('_') .map(|part| { let mut chars = part.chars(); @@ -312,7 +312,7 @@ fn parse_rentfree_token_list(tokens: &proc_macro2::TokenStream) -> syn::Result, Box>, /// AccountLoader<'info, T>, or InterfaceAccount<'info, T> -fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { +pub fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { match ty { Type::Path(type_path) => { let segment = type_path.path.segments.last()?; @@ -354,7 +354,7 @@ fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { } /// Extract seeds from #[account(seeds = [...], bump)] attribute -fn extract_anchor_seeds(attrs: &[syn::Attribute]) -> syn::Result> { +pub fn extract_anchor_seeds(attrs: &[syn::Attribute]) -> syn::Result> { for attr in attrs { if !attr.path().is_ident("account") { continue; @@ -439,7 +439,7 @@ fn classify_seeds_array(expr: &Expr) -> syn::Result> { } /// Classify a single seed expression -fn classify_seed_expr(expr: &Expr) -> syn::Result { +pub fn classify_seed_expr(expr: &Expr) -> syn::Result { match expr { // b"literal" Expr::Lit(lit) => { diff --git a/sdk-libs/macros/src/compressible/crate_context.rs b/sdk-libs/macros/src/compressible/crate_context.rs new file mode 100644 index 0000000000..caf2f27c46 --- /dev/null +++ b/sdk-libs/macros/src/compressible/crate_context.rs @@ -0,0 +1,280 @@ +//! Anchor-style crate context parser for `#[rentfree_program]`. +//! +//! This module recursively reads all module files at macro expansion time, +//! allowing `#[rentfree_program]` to discover all `#[derive(RentFree)]` structs +//! across the entire crate. +//! +//! Based on Anchor's `CrateContext::parse()` pattern from `anchor-syn/src/parser/context.rs`. + +use std::collections::BTreeMap; +use std::path::{Path, PathBuf}; +use syn::{Item, ItemStruct}; + +/// Context containing all parsed modules in the crate. +pub struct CrateContext { + modules: BTreeMap, +} + +impl CrateContext { + /// Parse all modules starting from the crate root (lib.rs or main.rs). + /// + /// Uses `CARGO_MANIFEST_DIR` environment variable to locate the crate root. + pub fn parse_from_manifest() -> syn::Result { + let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").map_err(|_| { + syn::Error::new( + proc_macro2::Span::call_site(), + "CARGO_MANIFEST_DIR not set - cannot parse crate context", + ) + })?; + + let src_dir = PathBuf::from(&manifest_dir).join("src"); + + // Try lib.rs first, then main.rs + let root_file = if src_dir.join("lib.rs").exists() { + src_dir.join("lib.rs") + } else if src_dir.join("main.rs").exists() { + src_dir.join("main.rs") + } else { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + format!( + "Could not find lib.rs or main.rs in {:?}", + src_dir + ), + )); + }; + + Self::parse(&root_file) + } + + /// Parse all modules starting from a specific root file. + pub fn parse(root: &Path) -> syn::Result { + let modules = ParsedModule::parse_recursive(root, "crate")?; + Ok(CrateContext { modules }) + } + + /// Iterate over all struct items in all parsed modules. + pub fn structs(&self) -> impl Iterator { + self.modules + .values() + .flat_map(|module| module.structs()) + } + + /// Find structs that have a specific derive attribute (e.g., "RentFree"). + pub fn structs_with_derive(&self, derive_name: &str) -> Vec<&ItemStruct> { + self.structs() + .filter(|s| has_derive_attribute(&s.attrs, derive_name)) + .collect() + } + + /// Get a reference to a specific module by path (e.g., "crate::instruction_accounts"). + #[allow(dead_code)] + pub fn module(&self, path: &str) -> Option<&ParsedModule> { + self.modules.get(path) + } +} + +/// A parsed module containing its items. +pub struct ParsedModule { + /// Module name (e.g., "instruction_accounts") + #[allow(dead_code)] + name: String, + /// File path where this module is defined + #[allow(dead_code)] + file: PathBuf, + /// Full module path (e.g., "crate::instruction_accounts") + #[allow(dead_code)] + path: String, + /// All items in the module + items: Vec, +} + +impl ParsedModule { + /// Recursively parse all modules starting from a root file. + fn parse_recursive( + root: &Path, + module_path: &str, + ) -> syn::Result> { + let mut modules = BTreeMap::new(); + + // Read and parse the root file + let content = std::fs::read_to_string(root).map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("Failed to read {:?}: {}", root, e), + ) + })?; + + let file: syn::File = syn::parse_str(&content).map_err(|e| { + syn::Error::new( + proc_macro2::Span::call_site(), + format!("Failed to parse {:?}: {}", root, e), + ) + })?; + + let root_dir = root.parent().unwrap_or(Path::new(".")); + let root_name = root + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or("root"); + + // Create the root module + let root_module = ParsedModule { + name: root_name.to_string(), + file: root.to_path_buf(), + path: module_path.to_string(), + items: file.items.clone(), + }; + modules.insert(module_path.to_string(), root_module); + + // Process each item for nested modules + for item in &file.items { + if let Item::Mod(item_mod) = item { + let mod_name = item_mod.ident.to_string(); + let child_path = format!("{}::{}", module_path, mod_name); + + if let Some((_, items)) = &item_mod.content { + // Inline module: mod foo { ... } + let inline_module = ParsedModule { + name: mod_name.clone(), + file: root.to_path_buf(), + path: child_path.clone(), + items: items.clone(), + }; + modules.insert(child_path, inline_module); + } else { + // External module: mod foo; - need to find the file + if let Some(mod_file) = find_module_file(root_dir, root_name, &mod_name) { + // Recursively parse the external module + let child_modules = + Self::parse_recursive(&mod_file, &child_path)?; + modules.extend(child_modules); + } + // If file not found, silently skip (might be a cfg'd out module) + } + } + } + + Ok(modules) + } + + /// Get all struct items in this module. + fn structs(&self) -> impl Iterator { + self.items.iter().filter_map(|item| { + if let Item::Struct(s) = item { + Some(s) + } else { + None + } + }) + } +} + +/// Find the file for an external module declaration. +/// +/// Tries multiple paths following Rust module resolution: +/// - sibling_dir/mod_name.rs +/// - sibling_dir/mod_name/mod.rs +/// - parent_mod/mod_name.rs (if parent is a mod.rs) +/// - parent_mod/mod_name/mod.rs (if parent is a mod.rs) +fn find_module_file(parent_dir: &Path, parent_name: &str, mod_name: &str) -> Option { + // Standard paths relative to parent directory + let paths = vec![ + // sibling file: parent_dir/mod_name.rs + parent_dir.join(format!("{}.rs", mod_name)), + // directory module: parent_dir/mod_name/mod.rs + parent_dir.join(mod_name).join("mod.rs"), + ]; + + // If parent is mod.rs or lib.rs, also check parent_name directory + if parent_name == "mod" || parent_name == "lib" { + for path in &paths { + if path.exists() { + return Some(path.clone()); + } + } + } else { + // Parent is a regular file like foo.rs, check foo/mod_name.rs + let parent_mod_dir = parent_dir.join(parent_name); + let extra_paths = vec![ + parent_mod_dir.join(format!("{}.rs", mod_name)), + parent_mod_dir.join(mod_name).join("mod.rs"), + ]; + + for path in paths.iter().chain(extra_paths.iter()) { + if path.exists() { + return Some(path.clone()); + } + } + } + + // Check standard paths + for path in &paths { + if path.exists() { + return Some(path.clone()); + } + } + + None +} + +/// Check if a struct has a specific derive attribute. +fn has_derive_attribute(attrs: &[syn::Attribute], derive_name: &str) -> bool { + for attr in attrs { + if !attr.path().is_ident("derive") { + continue; + } + + // Parse the derive contents + if let Ok(nested) = attr.parse_args_with( + syn::punctuated::Punctuated::::parse_terminated, + ) { + for path in nested { + // Check simple ident: #[derive(RentFree)] + if let Some(ident) = path.get_ident() { + if ident == derive_name { + return true; + } + } + // Check path: #[derive(light_sdk::RentFree)] + if let Some(segment) = path.segments.last() { + if segment.ident == derive_name { + return true; + } + } + } + } + } + false +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_has_derive_attribute() { + let code = quote::quote! { + #[derive(Accounts, RentFree)] + pub struct CreateUser<'info> { + pub fee_payer: Signer<'info>, + } + }; + let item: ItemStruct = syn::parse2(code).unwrap(); + assert!(has_derive_attribute(&item.attrs, "RentFree")); + assert!(has_derive_attribute(&item.attrs, "Accounts")); + assert!(!has_derive_attribute(&item.attrs, "Clone")); + } + + #[test] + fn test_has_derive_attribute_qualified() { + let code = quote::quote! { + #[derive(light_sdk::RentFree)] + pub struct CreateUser<'info> { + pub fee_payer: Signer<'info>, + } + }; + let item: ItemStruct = syn::parse2(code).unwrap(); + assert!(has_derive_attribute(&item.attrs, "RentFree")); + } +} diff --git a/sdk-libs/macros/src/compressible/file_scanner.rs b/sdk-libs/macros/src/compressible/file_scanner.rs deleted file mode 100644 index ea52500298..0000000000 --- a/sdk-libs/macros/src/compressible/file_scanner.rs +++ /dev/null @@ -1,188 +0,0 @@ -//! File scanning for #[rentfree_program] macro. -//! -//! This module reads external Rust source files to extract seed information -//! from Accounts structs that contain #[rentfree] fields. - -use std::path::{Path, PathBuf}; - -use syn::{Item, ItemMod, ItemStruct}; - -use crate::compressible::anchor_seeds::{ - extract_from_accounts_struct, ExtractedAccountsInfo, ExtractedSeedSpec, ExtractedTokenSpec, -}; - -/// Result of scanning a module and its external files -#[derive(Debug, Default)] -pub struct ScannedModuleInfo { - pub pda_specs: Vec, - pub token_specs: Vec, - pub errors: Vec, - /// Names of Accounts structs that have rentfree fields (for auto-wrapping handlers) - pub rentfree_struct_names: std::collections::HashSet, -} - -/// Scan the entire src/ directory for Accounts structs with #[rentfree] fields. -/// -/// This function scans all .rs files in the crate's src/ directory -/// and extracts seed information from Accounts structs. -pub fn scan_module_for_compressible( - _module: &ItemMod, - base_path: &Path, -) -> syn::Result { - let mut result = ScannedModuleInfo::default(); - - // Scan all .rs files in the src directory - scan_directory_recursive(base_path, &mut result); - - Ok(result) -} - -/// Recursively scan a directory for .rs files -fn scan_directory_recursive(dir: &Path, result: &mut ScannedModuleInfo) { - let entries = match std::fs::read_dir(dir) { - Ok(e) => e, - Err(e) => { - result - .errors - .push(format!("Failed to read directory {:?}: {}", dir, e)); - return; - } - }; - - for entry in entries.flatten() { - let path = entry.path(); - - if path.is_dir() { - scan_directory_recursive(&path, result); - } else if path.extension().map(|e| e == "rs").unwrap_or(false) { - scan_rust_file(&path, result); - } - } -} - -/// Scan a single Rust file for Accounts structs -fn scan_rust_file(path: &Path, result: &mut ScannedModuleInfo) { - let contents = match std::fs::read_to_string(path) { - Ok(c) => c, - Err(e) => { - result - .errors - .push(format!("Failed to read {:?}: {}", path, e)); - return; - } - }; - - let parsed: syn::File = match syn::parse_str(&contents) { - Ok(f) => f, - Err(e) => { - // Not all files may be valid on their own (e.g., test files with main) - // Just skip them silently - let _ = e; - return; - } - }; - - for item in parsed.items { - match item { - Item::Struct(item_struct) => { - if let Ok(Some((info, struct_name))) = try_extract_from_struct(&item_struct) { - result.pda_specs.extend(info.pda_fields); - result.token_specs.extend(info.token_fields); - result.rentfree_struct_names.insert(struct_name); - } - } - Item::Mod(inner_mod) if inner_mod.content.is_some() => { - // Inline module - recursively scan - scan_inline_module(&inner_mod, result); - } - _ => {} - } - } -} - -/// Scan an inline module for Accounts structs -fn scan_inline_module(module: &ItemMod, result: &mut ScannedModuleInfo) { - let content = match &module.content { - Some((_, items)) => items, - None => return, - }; - - for item in content { - match item { - Item::Struct(item_struct) => { - if let Ok(Some((info, struct_name))) = try_extract_from_struct(item_struct) { - result.pda_specs.extend(info.pda_fields); - result.token_specs.extend(info.token_fields); - result.rentfree_struct_names.insert(struct_name); - } - } - Item::Mod(inner_mod) if inner_mod.content.is_some() => { - scan_inline_module(inner_mod, result); - } - _ => {} - } - } -} - -/// Try to extract rentfree info from a struct. -/// Returns (ExtractedAccountsInfo, struct_name) if the struct has rentfree fields. -fn try_extract_from_struct( - item_struct: &ItemStruct, -) -> syn::Result> { - // Check if it has #[derive(Accounts)] - let has_accounts_derive = item_struct.attrs.iter().any(|attr| { - if attr.path().is_ident("derive") { - if let Ok(meta) = attr.parse_args_with( - syn::punctuated::Punctuated::::parse_terminated, - ) { - return meta.iter().any(|p| p.is_ident("Accounts")); - } - } - false - }); - - if !has_accounts_derive { - return Ok(None); - } - - let info = extract_from_accounts_struct(item_struct)?; - match info { - Some(extracted) => { - let struct_name = extracted.struct_name.to_string(); - Ok(Some((extracted, struct_name))) - } - None => Ok(None), - } -} - -/// Resolve the base path for the crate source -/// -/// This attempts to find the src/ directory by looking at CARGO_MANIFEST_DIR -/// or falling back to current directory. -pub fn resolve_crate_src_path() -> PathBuf { - // Try CARGO_MANIFEST_DIR first (set during cargo build) - if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") { - let src_path = PathBuf::from(&manifest_dir).join("src"); - if src_path.exists() { - return src_path; - } - // Fallback to manifest dir itself - return PathBuf::from(manifest_dir); - } - - // Fallback to current directory - std::env::current_dir() - .unwrap_or_else(|_| PathBuf::from(".")) - .join("src") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_resolve_path() { - let path = resolve_crate_src_path(); - println!("Resolved path: {:?}", path); - } -} diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 50265641ae..44b2c861a6 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -1482,32 +1482,34 @@ fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> s #[inline(never)] pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { use crate::compressible::{ - anchor_seeds::get_data_fields, - file_scanner::{resolve_crate_src_path, scan_module_for_compressible}, + anchor_seeds::{extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec}, + crate_context::CrateContext, }; if module.content.is_none() { return Err(macro_error!(&module, "Module must have a body")); } - // Resolve the crate's src/ directory - let base_path = resolve_crate_src_path(); + // Parse the crate following mod declarations (Anchor-style) + let crate_ctx = CrateContext::parse_from_manifest()?; - // Scan the module (and external files) for compressible fields - let scanned = scan_module_for_compressible(&module, &base_path)?; + // Find all structs with #[derive(Accounts)] and extract rentfree field info + let mut pda_specs: Vec = Vec::new(); + let mut token_specs: Vec = Vec::new(); + let mut rentfree_struct_names = std::collections::HashSet::new(); - // Report any errors from file scanning - if !scanned.errors.is_empty() { - let error_msg = scanned.errors.join("\n"); - return Err(macro_error!( - &module, - "Errors while scanning for rentfree types:\n{}", - error_msg - )); + for item_struct in crate_ctx.structs_with_derive("Accounts") { + if let Some(info) = extract_from_accounts_struct(item_struct)? { + if !info.pda_fields.is_empty() || !info.token_fields.is_empty() { + rentfree_struct_names.insert(info.struct_name.to_string()); + pda_specs.extend(info.pda_fields); + token_specs.extend(info.token_fields); + } + } } // Check if we found anything - if scanned.pda_specs.is_empty() && scanned.token_specs.is_empty() { + if pda_specs.is_empty() && token_specs.is_empty() { return Err(macro_error!( &module, "No #[rentfree] or #[rentfree_token] fields found in any Accounts struct.\n\ @@ -1521,7 +1523,7 @@ pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Res if let Item::Fn(fn_item) = item { // Check if this function uses a rentfree Accounts struct if let Some((context_type, params_ident)) = extract_context_and_params(fn_item) { - if scanned.rentfree_struct_names.contains(&context_type) { + if rentfree_struct_names.contains(&context_type) { // Wrap the function with pre_init/finalize logic *fn_item = wrap_function_with_rentfree(fn_item, ¶ms_ident); } @@ -1535,7 +1537,7 @@ pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Res let mut found_data_fields: Vec = Vec::new(); let mut account_types: Vec = Vec::new(); - for pda in &scanned.pda_specs { + for pda in &pda_specs { account_types.push(pda.inner_type.clone()); let seed_elements = convert_classified_to_seed_elements(&pda.seeds); @@ -1567,7 +1569,7 @@ pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Res // Convert token specs let mut found_token_seeds: Vec = Vec::new(); - for token in &scanned.token_specs { + for token in &token_specs { let seed_elements = convert_classified_to_seed_elements(&token.seeds); let authority_elements = token .authority_seeds diff --git a/sdk-libs/macros/src/compressible/mod.rs b/sdk-libs/macros/src/compressible/mod.rs index c02abdeb69..32c636b59f 100644 --- a/sdk-libs/macros/src/compressible/mod.rs +++ b/sdk-libs/macros/src/compressible/mod.rs @@ -1,8 +1,8 @@ //! Compressible account macro generation. pub mod anchor_seeds; +pub mod crate_context; pub mod decompress_context; -pub mod file_scanner; pub mod instructions; pub mod light_compressible; pub mod pack_unpack; diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs index 869073ba13..a204ff1304 100644 --- a/sdk-libs/macros/src/finalize/parse.rs +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -6,11 +6,18 @@ use syn::{ DeriveInput, Error, Expr, Ident, Token, Type, }; +// Re-export shared seed classification types from anchor_seeds module +pub(super) use crate::compressible::anchor_seeds::{ + classify_seed_expr, extract_account_inner_type, extract_anchor_seeds, snake_to_camel_case, + ClassifiedSeed, +}; + /// Parsed representation of a struct with rentfree and light_mint fields. -pub struct ParsedCompressibleStruct { +pub(super) struct ParsedCompressibleStruct { pub struct_name: Ident, pub generics: syn::Generics, pub rentfree_fields: Vec, + pub rentfree_token_fields: Vec, pub light_mint_fields: Vec, pub instruction_args: Option>, pub fee_payer_field: Option, @@ -26,17 +33,33 @@ pub struct ParsedCompressibleStruct { } /// A field marked with #[rentfree(...)] -pub struct RentFreeField { +pub(super) struct RentFreeField { pub ident: Ident, pub ty: Type, pub address_tree_info: Expr, pub output_tree: Expr, /// True if the field is Box>, false if Account pub is_boxed: bool, + /// The inner type T from Account<'info, T> (e.g., UserRecord) + pub inner_type: Ident, + /// Seeds extracted from #[account(seeds = [...], bump)] + pub anchor_seeds: Vec, +} + +/// A field marked with #[rentfree_token(...)] +pub(super) struct RentFreeTokenField { + /// The variant name (derived from field name: snake_case -> CamelCase) + pub variant_name: Ident, + /// Seeds from #[account(seeds = [...])] + pub anchor_seeds: Vec, + /// Authority seeds from #[rentfree_token(authority = [...])] + /// Note: Used by seeds.rs when connected to the module + #[allow(dead_code)] + pub authority_seeds: Option>, } /// A field marked with #[light_mint(...)] -pub struct LightMintField { +pub(super) struct LightMintField { /// The field name where #[light_mint] is attached (CMint account) pub field_ident: Ident, /// The mint_signer field (AccountInfo that seeds the mint PDA) @@ -58,11 +81,20 @@ pub struct LightMintField { } /// Instruction argument from #[instruction(...)] -pub struct InstructionArg { +pub(super) struct InstructionArg { pub name: Ident, pub ty: Type, } +impl Parse for InstructionArg { + fn parse(input: ParseStream) -> syn::Result { + let name: Ident = input.parse()?; + input.parse::()?; + let ty: Type = input.parse()?; + Ok(Self { name, ty }) + } +} + /// Arguments inside #[rentfree(...)] struct RentFreeArgs { address_tree_info: Option, @@ -160,37 +192,14 @@ impl Parse for KeyValueArg { } } -/// A single instruction argument: `name: Type` -struct InstructionArgParsed { - name: Ident, - _colon: Token![:], - ty: Type, -} - -impl Parse for InstructionArgParsed { - fn parse(input: ParseStream) -> syn::Result { - Ok(InstructionArgParsed { - name: input.parse()?, - _colon: input.parse()?, - ty: input.parse()?, - }) - } -} - /// Parse #[instruction(...)] attribute from struct fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Option> { for attr in attrs { if attr.path().is_ident("instruction") { if let Ok(args) = attr.parse_args_with(|input: ParseStream| { - let content: Punctuated = + let content: Punctuated = Punctuated::parse_terminated(input)?; - Ok(content - .into_iter() - .map(|arg| InstructionArg { - name: arg.name, - ty: arg.ty, - }) - .collect::>()) + Ok(content.into_iter().collect::>()) }) { return Some(args); } @@ -199,39 +208,8 @@ fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Option or Box> -fn extract_account_type(ty: &Type) -> Option<(bool, &syn::Path)> { - match ty { - Type::Path(type_path) => { - let path = &type_path.path; - if let Some(segment) = path.segments.last() { - let ident_str = segment.ident.to_string(); - if ident_str == "Account" { - return Some((false, path)); - } - if ident_str == "Box" { - // Check for Box> - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(Type::Path(inner_path))) = - args.args.first() - { - if let Some(inner_seg) = inner_path.path.segments.last() { - if inner_seg.ident == "Account" { - return Some((true, &inner_path.path)); - } - } - } - } - } - } - None - } - _ => None, - } -} - /// Parse a struct to extract rentfree and light_mint fields -pub fn parse_compressible_struct(input: &DeriveInput) -> Result { +pub(super) fn parse_compressible_struct(input: &DeriveInput) -> Result { let struct_name = input.ident.clone(); let generics = input.generics.clone(); @@ -246,6 +224,7 @@ pub fn parse_compressible_struct(input: &DeriveInput) -> Result Result fields", ) })?; + // Extract seeds from #[account(seeds = [...], bump)] + let anchor_seeds = extract_anchor_seeds(&field.attrs)?; + rentfree_fields.push(RentFreeField { ident: field_ident.clone(), ty: field.ty.clone(), address_tree_info, output_tree, is_boxed, + inner_type, + anchor_seeds, }); break; } @@ -370,6 +354,28 @@ pub fn parse_compressible_struct(input: &DeriveInput) -> Result CamelCase + let variant_name = { + let camel = snake_to_camel_case(&field_ident.to_string()); + Ident::new(&camel, field_ident.span()) + }; + + rentfree_token_fields.push(RentFreeTokenField { + variant_name, + anchor_seeds, + authority_seeds, + }); + break; + } } } @@ -377,6 +383,7 @@ pub fn parse_compressible_struct(input: &DeriveInput) -> Result Result syn::Result>> { + match &attr.meta { + // #[rentfree_token] with no arguments + syn::Meta::Path(_) => Ok(None), + + // #[rentfree_token = Variant] (deprecated but still supported - no authority) + syn::Meta::NameValue(_) => Ok(None), + + // #[rentfree_token(authority = [...])] + syn::Meta::List(list) => { + let tokens = list.tokens.clone(); + parse_rentfree_token_authority_seeds(tokens) + } + } +} + +/// Parse authority = [...] from rentfree_token attribute content +fn parse_rentfree_token_authority_seeds(tokens: proc_macro2::TokenStream) -> syn::Result>> { + use syn::parse::Parser; + + let parser = |input: syn::parse::ParseStream| -> syn::Result>> { + // Look for authority = [...] + while !input.is_empty() { + if input.peek(Ident) { + let ident: Ident = input.parse()?; + + if ident == "authority" { + input.parse::()?; + let array: syn::ExprArray = input.parse()?; + let mut seeds = Vec::new(); + for elem in &array.elems { + seeds.push(classify_seed_expr(elem)?); + } + return Ok(Some(seeds)); + } + + // Skip other key-value pairs + if input.peek(Token![=]) { + input.parse::()?; + let _: Expr = input.parse()?; + } + } + + // Skip commas + if input.peek(Token![,]) { + input.parse::()?; + } else if !input.is_empty() { + // Unexpected token - just skip it + break; + } + } + + Ok(None) + }; + + parser.parse2(tokens) +} From 193c35e8a442ee6d64573d1807b5f723699eb852 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 01:33:16 +0000 Subject: [PATCH 2/9] stash cleanup --- .../macros/src/compressible/anchor_seeds.rs | 42 ++-- .../macros/src/compressible/instructions.rs | 109 ++++------- sdk-libs/macros/src/finalize/codegen.rs | 62 +++--- sdk-libs/macros/src/finalize/mod.rs | 2 +- sdk-libs/macros/src/finalize/parse.rs | 181 +++++------------- sdk-libs/macros/src/utils.rs | 29 +++ 6 files changed, 165 insertions(+), 260 deletions(-) diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/compressible/anchor_seeds.rs index bcd254495a..96c1031a13 100644 --- a/sdk-libs/macros/src/compressible/anchor_seeds.rs +++ b/sdk-libs/macros/src/compressible/anchor_seeds.rs @@ -3,6 +3,7 @@ //! This module extracts PDA seeds from Anchor's attribute syntax and classifies them //! into the categories needed for compression: literals, ctx fields, data fields, etc. +use crate::utils::snake_to_camel_case; use syn::{Expr, Ident, ItemStruct, Type}; /// Classified seed element from Anchor's seeds array @@ -73,7 +74,6 @@ pub fn extract_from_accounts_struct( let mut pda_fields = Vec::new(); let mut token_fields = Vec::new(); - let mut all_fields = Vec::new(); for field in fields { let field_ident = match &field.ident { @@ -81,8 +81,6 @@ pub fn extract_from_accounts_struct( None => continue, }; - all_fields.push((field_ident.clone(), field.ty.clone())); - // Check for #[rentfree] attribute let has_rentfree = field .attrs @@ -94,7 +92,8 @@ pub fn extract_from_accounts_struct( if has_rentfree { // Extract inner type from Account<'info, T> or Box> - let (is_boxed, inner_type) = match extract_account_inner_type(&field.ty) { + // Note: is_boxed is not needed for ExtractedSeedSpec, only inner_type + let (_, inner_type) = match extract_account_inner_type(&field.ty) { Some(result) => result, None => { return Err(syn::Error::new_spanned( @@ -113,7 +112,6 @@ pub fn extract_from_accounts_struct( Ident::new(&camel, field_ident.span()) }; - let _ = (field_ident, is_boxed); // Suppress unused warnings pda_fields.push(ExtractedSeedSpec { variant_name, inner_type, @@ -160,26 +158,26 @@ pub fn extract_from_accounts_struct( ]; for candidate in &authority_candidates { - if let Some((auth_field, _)) = all_fields.iter().find(|(name, _)| name == candidate) { - token.authority_field = Some(auth_field.clone()); - - // Try to extract authority seeds from the authority field - if let Some(auth_field_info) = fields - .iter() - .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(candidate.clone())) - { + // Search fields directly instead of using a separate all_fields collection + if let Some(auth_field_info) = fields + .iter() + .find(|f| f.ident.as_ref().map(|i| i.to_string()) == Some(candidate.clone())) + { + if let Some(auth_ident) = &auth_field_info.ident { + token.authority_field = Some(auth_ident.clone()); + + // Try to extract authority seeds from the authority field if let Ok(auth_seeds) = extract_anchor_seeds(&auth_field_info.attrs) { if !auth_seeds.is_empty() { token.authority_seeds = Some(auth_seeds); } } + break; } - break; } } } - let _ = all_fields; // Suppress unused warning Ok(Some(ExtractedAccountsInfo { struct_name: item.ident.clone(), pda_fields, @@ -194,20 +192,6 @@ struct RentFreeTokenAttr { authority_seeds: Option>, } -/// Convert snake_case field name to CamelCase variant name -/// e.g., token_0_vault -> Token0Vault, vault -> Vault -pub fn snake_to_camel_case(s: &str) -> String { - s.split('_') - .map(|part| { - let mut chars = part.chars(); - match chars.next() { - None => String::new(), - Some(first) => first.to_uppercase().chain(chars).collect(), - } - }) - .collect() -} - /// Extract #[rentfree_token(authority = [...])] attribute /// Variant name is now derived from field name, not specified in attribute fn extract_rentfree_token_attr(attrs: &[syn::Attribute]) -> Option { diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/compressible/instructions.rs index 44b2c861a6..65bea72992 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/compressible/instructions.rs @@ -1,5 +1,6 @@ //! Compressible instructions generation. +use crate::utils::to_snake_case; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ @@ -8,22 +9,6 @@ use syn::{ Expr, Ident, Item, ItemMod, LitStr, Result, Token, }; -/// Convert PascalCase to snake_case (e.g., UserRecord -> user_record) -fn to_snake_case(s: &str) -> String { - let mut result = String::new(); - for (i, c) in s.chars().enumerate() { - if c.is_uppercase() { - if i > 0 { - result.push('_'); - } - result.push(c.to_ascii_lowercase()); - } else { - result.push(c); - } - } - result -} - macro_rules! macro_error { ($span:expr, $msg:expr) => { syn::Error::new_spanned( @@ -208,30 +193,41 @@ impl Parse for SeedElement { } } -/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. +/// Recursively extract field names from expressions matching `base.field` or `base.nested.field`. /// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) -fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { +/// +/// Parameters: +/// - `base_ident`: The base identifier to match (e.g., "ctx" or "data") +/// - `nested_prefix`: Optional nested field name (e.g., "accounts" for ctx.accounts.XXX) +fn extract_fields_by_base( + expr: &syn::Expr, + base_ident: &str, + nested_prefix: Option<&str>, + fields: &mut Vec, +) { match expr { syn::Expr::Field(field_expr) => { if let syn::Member::Named(field_name) = &field_expr.member { - // Check for ctx.XXX pattern (direct field access) + // Check for base.XXX pattern (direct field access) if let syn::Expr::Path(path) = &*field_expr.base { if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { + if segment.ident == base_ident { fields.push(field_name.clone()); return; } } } - // Check for ctx.accounts.XXX pattern (nested field access) - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - fields.push(field_name.clone()); - return; + // Check for base.nested.XXX pattern (nested field access) if nested_prefix is provided + if let Some(nested) = nested_prefix { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == nested { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == base_ident { + fields.push(field_name.clone()); + return; + } } } } @@ -240,31 +236,36 @@ fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { } } // Recurse into base expression - extract_ctx_fields_from_expr(&field_expr.base, fields); + extract_fields_by_base(&field_expr.base, base_ident, nested_prefix, fields); } syn::Expr::MethodCall(method) => { // Recurse into receiver and args - extract_ctx_fields_from_expr(&method.receiver, fields); + extract_fields_by_base(&method.receiver, base_ident, nested_prefix, fields); for arg in &method.args { - extract_ctx_fields_from_expr(arg, fields); + extract_fields_by_base(arg, base_ident, nested_prefix, fields); } } syn::Expr::Call(call) => { // Recurse into function args for arg in &call.args { - extract_ctx_fields_from_expr(arg, fields); + extract_fields_by_base(arg, base_ident, nested_prefix, fields); } } syn::Expr::Reference(ref_expr) => { - extract_ctx_fields_from_expr(&ref_expr.expr, fields); + extract_fields_by_base(&ref_expr.expr, base_ident, nested_prefix, fields); } syn::Expr::Paren(paren) => { - extract_ctx_fields_from_expr(&paren.expr, fields); + extract_fields_by_base(&paren.expr, base_ident, nested_prefix, fields); } _ => {} } } +/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. +fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + extract_fields_by_base(expr, "ctx", Some("accounts"), fields); +} + /// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { let mut fields = Vec::new(); @@ -291,43 +292,9 @@ pub fn extract_ctx_seed_fields( .collect() } -/// Phase 5: Extract data.XXX field names from an expression recursively. +/// Extract data.XXX field names from an expression recursively. fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for data.XXX pattern - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - fields.push(field_name.clone()); - return; - } - } - } - } - // Recurse into base expression - extract_data_fields_from_expr(&field_expr.base, fields); - } - syn::Expr::MethodCall(method) => { - extract_data_fields_from_expr(&method.receiver, fields); - for arg in &method.args { - extract_data_fields_from_expr(arg, fields); - } - } - syn::Expr::Call(call) => { - for arg in &call.args { - extract_data_fields_from_expr(arg, fields); - } - } - syn::Expr::Reference(ref_expr) => { - extract_data_fields_from_expr(&ref_expr.expr, fields); - } - syn::Expr::Paren(paren) => { - extract_data_fields_from_expr(&paren.expr, fields); - } - _ => {} - } + extract_fields_by_base(expr, "data", None, fields); } /// Phase 5: Extract all data.XXX field names from a list of seed elements. diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/finalize/codegen.rs index 0e8059c3c6..f33fb8d98a 100644 --- a/sdk-libs/macros/src/finalize/codegen.rs +++ b/sdk-libs/macros/src/finalize/codegen.rs @@ -17,8 +17,18 @@ use quote::{format_ident, quote}; use super::parse::{ParsedCompressibleStruct, RentFreeField}; -/// Generate both trait implementations -pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream { +/// 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 both trait implementations. +/// +/// Returns `Err` if the parsed struct has inconsistent state (e.g., params type without ident). +pub fn generate_finalize_impl( + parsed: &ParsedCompressibleStruct, +) -> Result { let struct_name = &parsed.struct_name; let (impl_generics, ty_generics, where_clause) = parsed.generics.split_for_impl(); @@ -33,7 +43,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream Some(ty) => ty, None => { // No instruction args - generate no-op impls - return quote! { + return Ok(quote! { #[automatically_derived] impl #impl_generics light_sdk::compressible::LightPreInit<'info, ()> for #struct_name #ty_generics #where_clause { fn light_pre_init( @@ -56,7 +66,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream Ok(()) } } - }; + }); } }; @@ -65,7 +75,12 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream .as_ref() .and_then(|args| args.first()) .map(|arg| &arg.name) - .expect("params ident must exist if type exists"); + .ok_or_else(|| { + syn::Error::new( + parsed.struct_name.span(), + "internal error: instruction params type exists but params ident is missing", + ) + })?; let has_pdas = !parsed.rentfree_fields.is_empty(); let has_mints = !parsed.light_mint_fields.is_empty(); @@ -143,7 +158,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream // LightFinalize: No-op (all work done in pre_init) let finalize_body = quote! { Ok(()) }; - quote! { + Ok(quote! { #[automatically_derived] impl #impl_generics light_sdk::compressible::LightPreInit<'info, #params_type> for #struct_name #ty_generics #where_clause { fn light_pre_init( @@ -168,7 +183,7 @@ pub fn generate_finalize_impl(parsed: &ParsedCompressibleStruct) -> TokenStream #finalize_body } } - } + }) } /// Generate LightPreInit body for PDAs + mints: @@ -187,7 +202,7 @@ fn generate_pre_init_pdas_and_mints( ctoken_cpi_authority: &TokenStream, ) -> TokenStream { let (compress_blocks, new_addr_idents) = - generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + generate_pda_compress_blocks(&parsed.rentfree_fields); let rentfree_count = parsed.rentfree_fields.len() as u8; let pda_count = parsed.rentfree_fields.len(); @@ -216,18 +231,20 @@ fn generate_pre_init_pdas_and_mints( quote! { None } }; - // rent_payment defaults to 2 epochs (u8) + // rent_payment defaults to DEFAULT_RENT_PAYMENT_EPOCHS let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { quote! { #rent } } else { - quote! { 2u8 } + let default = DEFAULT_RENT_PAYMENT_EPOCHS; + quote! { #default } }; - // write_top_up defaults to 0 (u32) + // write_top_up defaults to DEFAULT_WRITE_TOP_UP_LAMPORTS let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { quote! { #top_up } } else { - quote! { 0u32 } + let default = DEFAULT_WRITE_TOP_UP_LAMPORTS; + quote! { #default } }; // assigned_account_index for mint is after PDAs @@ -347,7 +364,7 @@ fn generate_pre_init_pdas_and_mints( use light_compressed_account::instruction_data::traits::LightInstructionData; let ix_data = instruction_data.data() - .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), @@ -415,18 +432,20 @@ fn generate_pre_init_mints_only( quote! { None } }; - // rent_payment defaults to 2 epochs (u8) + // rent_payment defaults to DEFAULT_RENT_PAYMENT_EPOCHS let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { quote! { #rent } } else { - quote! { 2u8 } + let default = DEFAULT_RENT_PAYMENT_EPOCHS; + quote! { #default } }; - // write_top_up defaults to 0 (u32) + // write_top_up defaults to DEFAULT_WRITE_TOP_UP_LAMPORTS let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { quote! { #top_up } } else { - quote! { 0u32 } + let default = DEFAULT_WRITE_TOP_UP_LAMPORTS; + quote! { #default } }; quote! { @@ -497,7 +516,7 @@ fn generate_pre_init_mints_only( use light_compressed_account::instruction_data::traits::LightInstructionData; let ix_data = instruction_data.data() - .map_err(|e| light_sdk::error::LightSdkError::Borsh)?; + .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), @@ -538,7 +557,7 @@ fn generate_pre_init_pdas_only( compression_config: &TokenStream, ) -> TokenStream { let (compress_blocks, new_addr_idents) = - generate_pda_compress_blocks(&parsed.rentfree_fields, params_ident); + generate_pda_compress_blocks(&parsed.rentfree_fields); let rentfree_count = parsed.rentfree_fields.len() as u8; quote! { @@ -574,10 +593,7 @@ fn generate_pre_init_pdas_only( } /// Generate compression blocks for PDA fields -fn generate_pda_compress_blocks( - fields: &[RentFreeField], - _params_ident: &syn::Ident, -) -> (Vec, Vec) { +fn generate_pda_compress_blocks(fields: &[RentFreeField]) -> (Vec, Vec) { let mut blocks = Vec::new(); let mut addr_idents = Vec::new(); diff --git a/sdk-libs/macros/src/finalize/mod.rs b/sdk-libs/macros/src/finalize/mod.rs index 2f6b3991fc..0758987f54 100644 --- a/sdk-libs/macros/src/finalize/mod.rs +++ b/sdk-libs/macros/src/finalize/mod.rs @@ -14,5 +14,5 @@ use syn::DeriveInput; pub fn derive_light_finalize(input: DeriveInput) -> Result { let parsed = parse::parse_compressible_struct(&input)?; - Ok(codegen::generate_finalize_impl(&parsed)) + codegen::generate_finalize_impl(&parsed) } diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/finalize/parse.rs index a204ff1304..6dfa4d35a2 100644 --- a/sdk-libs/macros/src/finalize/parse.rs +++ b/sdk-libs/macros/src/finalize/parse.rs @@ -6,18 +6,14 @@ use syn::{ DeriveInput, Error, Expr, Ident, Token, Type, }; -// Re-export shared seed classification types from anchor_seeds module -pub(super) use crate::compressible::anchor_seeds::{ - classify_seed_expr, extract_account_inner_type, extract_anchor_seeds, snake_to_camel_case, - ClassifiedSeed, -}; +// Import shared types from anchor_seeds module +pub(super) use crate::compressible::anchor_seeds::extract_account_inner_type; /// Parsed representation of a struct with rentfree and light_mint fields. pub(super) struct ParsedCompressibleStruct { pub struct_name: Ident, pub generics: syn::Generics, pub rentfree_fields: Vec, - pub rentfree_token_fields: Vec, pub light_mint_fields: Vec, pub instruction_args: Option>, pub fee_payer_field: Option, @@ -40,22 +36,6 @@ pub(super) struct RentFreeField { pub output_tree: Expr, /// True if the field is Box>, false if Account pub is_boxed: bool, - /// The inner type T from Account<'info, T> (e.g., UserRecord) - pub inner_type: Ident, - /// Seeds extracted from #[account(seeds = [...], bump)] - pub anchor_seeds: Vec, -} - -/// A field marked with #[rentfree_token(...)] -pub(super) struct RentFreeTokenField { - /// The variant name (derived from field name: snake_case -> CamelCase) - pub variant_name: Ident, - /// Seeds from #[account(seeds = [...])] - pub anchor_seeds: Vec, - /// Authority seeds from #[rentfree_token(authority = [...])] - /// Note: Used by seeds.rs when connected to the module - #[allow(dead_code)] - pub authority_seeds: Option>, } /// A field marked with #[light_mint(...)] @@ -192,28 +172,32 @@ impl Parse for KeyValueArg { } } -/// Parse #[instruction(...)] attribute from struct -fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Option> { +/// Parse #[instruction(...)] attribute from struct. +/// +/// Returns `Ok(None)` if no instruction attribute is present, +/// `Ok(Some(args))` if successfully parsed, or `Err` on malformed syntax. +fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Result>, Error> { for attr in attrs { if attr.path().is_ident("instruction") { - if let Ok(args) = attr.parse_args_with(|input: ParseStream| { + let args = attr.parse_args_with(|input: ParseStream| { let content: Punctuated = Punctuated::parse_terminated(input)?; Ok(content.into_iter().collect::>()) - }) { - return Some(args); - } + })?; + return Ok(Some(args)); } } - None + Ok(None) } /// Parse a struct to extract rentfree and light_mint fields -pub(super) fn parse_compressible_struct(input: &DeriveInput) -> Result { +pub(super) fn parse_compressible_struct( + input: &DeriveInput, +) -> Result { let struct_name = input.ident.clone(); let generics = input.generics.clone(); - let instruction_args = parse_instruction_attr(&input.attrs); + let instruction_args = parse_instruction_attr(&input.attrs)?; let fields = match &input.data { syn::Data::Struct(data) => match &data.fields { @@ -224,7 +208,6 @@ pub(super) fn parse_compressible_struct(input: &DeriveInput) -> Result Result Result Result fields", - ) - })?; - - // Extract seeds from #[account(seeds = [...], bump)] - let anchor_seeds = extract_anchor_seeds(&field.attrs)?; + // Validate this is an Account type + let (is_boxed, _inner_type) = + extract_account_inner_type(&field.ty).ok_or_else(|| { + Error::new_spanned( + &field.ty, + "#[rentfree] can only be applied to Account<...> fields", + ) + })?; rentfree_fields.push(RentFreeField { ident: field_ident.clone(), @@ -315,8 +312,6 @@ pub(super) fn parse_compressible_struct(input: &DeriveInput) -> Result Result CamelCase - let variant_name = { - let camel = snake_to_camel_case(&field_ident.to_string()); - Ident::new(&camel, field_ident.span()) - }; - - rentfree_token_fields.push(RentFreeTokenField { - variant_name, - anchor_seeds, - authority_seeds, - }); - break; - } } } @@ -383,7 +356,6 @@ pub(super) fn parse_compressible_struct(input: &DeriveInput) -> Result Result syn::Result>> { - match &attr.meta { - // #[rentfree_token] with no arguments - syn::Meta::Path(_) => Ok(None), - - // #[rentfree_token = Variant] (deprecated but still supported - no authority) - syn::Meta::NameValue(_) => Ok(None), - - // #[rentfree_token(authority = [...])] - syn::Meta::List(list) => { - let tokens = list.tokens.clone(); - parse_rentfree_token_authority_seeds(tokens) - } - } -} - -/// Parse authority = [...] from rentfree_token attribute content -fn parse_rentfree_token_authority_seeds(tokens: proc_macro2::TokenStream) -> syn::Result>> { - use syn::parse::Parser; - - let parser = |input: syn::parse::ParseStream| -> syn::Result>> { - // Look for authority = [...] - while !input.is_empty() { - if input.peek(Ident) { - let ident: Ident = input.parse()?; - - if ident == "authority" { - input.parse::()?; - let array: syn::ExprArray = input.parse()?; - let mut seeds = Vec::new(); - for elem in &array.elems { - seeds.push(classify_seed_expr(elem)?); - } - return Ok(Some(seeds)); - } - - // Skip other key-value pairs - if input.peek(Token![=]) { - input.parse::()?; - let _: Expr = input.parse()?; - } - } - - // Skip commas - if input.peek(Token![,]) { - input.parse::()?; - } else if !input.is_empty() { - // Unexpected token - just skip it - break; - } - } - - Ok(None) - }; - - parser.parse2(tokens) -} diff --git a/sdk-libs/macros/src/utils.rs b/sdk-libs/macros/src/utils.rs index b84eb1e9f8..eccbaf9ff1 100644 --- a/sdk-libs/macros/src/utils.rs +++ b/sdk-libs/macros/src/utils.rs @@ -17,3 +17,32 @@ use syn::Result; pub(crate) fn into_token_stream(result: Result) -> TokenStream { result.unwrap_or_else(|err| err.to_compile_error()).into() } + +/// Convert snake_case to CamelCase (e.g., user_record -> UserRecord) +pub(crate) fn snake_to_camel_case(s: &str) -> String { + s.split('_') + .map(|part| { + let mut chars = part.chars(); + match chars.next() { + None => String::new(), + Some(first) => first.to_uppercase().chain(chars).collect(), + } + }) + .collect() +} + +/// Convert PascalCase/CamelCase to snake_case (e.g., UserRecord -> user_record) +pub(crate) fn to_snake_case(s: &str) -> String { + let mut result = String::new(); + for (i, c) in s.chars().enumerate() { + if c.is_uppercase() { + if i > 0 { + result.push('_'); + } + result.push(c.to_ascii_lowercase()); + } else { + result.push(c); + } + } + result +} From f874a0fb04ac6e3e8a0a0eb9ddbba48e3d3f3524 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 01:53:11 +0000 Subject: [PATCH 3/9] stash --- sdk-libs/macros/src/compressible/README.md | 45 -------------- sdk-libs/macros/src/compressible/mod.rs | 12 ---- sdk-libs/macros/src/finalize/mod.rs | 18 ------ sdk-libs/macros/src/lib.rs | 17 +++--- sdk-libs/macros/src/rentfree/README.md | 58 +++++++++++++++++++ .../accounts}/codegen.rs | 35 ++++++++--- sdk-libs/macros/src/rentfree/accounts/mod.rs | 17 ++++++ .../{finalize => rentfree/accounts}/parse.rs | 51 +++++++++++++--- sdk-libs/macros/src/rentfree/mod.rs | 10 ++++ .../program}/crate_context.rs | 0 .../program}/instructions.rs | 38 ++++++------ sdk-libs/macros/src/rentfree/program/mod.rs | 13 +++++ .../program}/seed_providers.rs | 4 +- .../program}/variant_enum.rs | 0 .../traits}/anchor_seeds.rs | 10 ++++ .../traits}/decompress_context.rs | 2 +- .../traits}/light_compressible.rs | 14 ++--- sdk-libs/macros/src/rentfree/traits/mod.rs | 16 +++++ .../traits}/pack_unpack.rs | 0 .../traits}/traits.rs | 0 .../traits}/utils.rs | 0 21 files changed, 231 insertions(+), 129 deletions(-) delete mode 100644 sdk-libs/macros/src/compressible/README.md delete mode 100644 sdk-libs/macros/src/compressible/mod.rs delete mode 100644 sdk-libs/macros/src/finalize/mod.rs create mode 100644 sdk-libs/macros/src/rentfree/README.md rename sdk-libs/macros/src/{finalize => rentfree/accounts}/codegen.rs (95%) create mode 100644 sdk-libs/macros/src/rentfree/accounts/mod.rs rename sdk-libs/macros/src/{finalize => rentfree/accounts}/parse.rs (86%) create mode 100644 sdk-libs/macros/src/rentfree/mod.rs rename sdk-libs/macros/src/{compressible => rentfree/program}/crate_context.rs (100%) rename sdk-libs/macros/src/{compressible => rentfree/program}/instructions.rs (97%) create mode 100644 sdk-libs/macros/src/rentfree/program/mod.rs rename sdk-libs/macros/src/{compressible => rentfree/program}/seed_providers.rs (99%) rename sdk-libs/macros/src/{compressible => rentfree/program}/variant_enum.rs (100%) rename sdk-libs/macros/src/{compressible => rentfree/traits}/anchor_seeds.rs (97%) rename sdk-libs/macros/src/{compressible => rentfree/traits}/decompress_context.rs (99%) rename sdk-libs/macros/src/{compressible => rentfree/traits}/light_compressible.rs (94%) create mode 100644 sdk-libs/macros/src/rentfree/traits/mod.rs rename sdk-libs/macros/src/{compressible => rentfree/traits}/pack_unpack.rs (100%) rename sdk-libs/macros/src/{compressible => rentfree/traits}/traits.rs (100%) rename sdk-libs/macros/src/{compressible => rentfree/traits}/utils.rs (100%) diff --git a/sdk-libs/macros/src/compressible/README.md b/sdk-libs/macros/src/compressible/README.md deleted file mode 100644 index aac0ceb9e3..0000000000 --- a/sdk-libs/macros/src/compressible/README.md +++ /dev/null @@ -1,45 +0,0 @@ -# Compressible Macros - -Procedural macros for generating rent-free account types and their hooks for Solana programs. - -## Files - -**`mod.rs`** - Module declaration - -**`traits.rs`** - Core trait implementations - -- `HasCompressionInfo` - CompressionInfo field access -- `CompressAs` - Field-level compression control -- `Compressible` - Full trait bundle (Size + HasCompressionInfo + CompressAs) - -**`pack_unpack.rs`** - Pubkey compression - -- Generates `PackedXxx` structs where Pubkey fields become u8 indices -- Implements Pack/Unpack traits for serialization efficiency - -**`variant_enum.rs`** - Account variant enum - -- Generates `RentFreeAccountVariant` enum from account types -- Implements all required traits (Default, DataHasher, Size, Pack, Unpack) -- Creates `RentFreeAccountData` wrapper struct - -**`instructions.rs`** - Instruction generation - -- Main macro: `#[rentfree]` -- Generates compress/decompress instruction handlers -- Creates context structs and account validation -- **Compress**: PDA-only (ctokens compressed via registry) -- **Decompress**: Full PDA + ctoken support - -**`seed_providers.rs`** - Seed derivation - -- PDA seed provider implementations -- CToken seed provider with account/authority derivation -- Client-side seed functions for off-chain use - -**`decompress_context.rs`** - Decompression trait - -- Generates `DecompressContext` implementation -- Account accessor methods -- PDA/token separation logic -- Token processing delegation diff --git a/sdk-libs/macros/src/compressible/mod.rs b/sdk-libs/macros/src/compressible/mod.rs deleted file mode 100644 index 32c636b59f..0000000000 --- a/sdk-libs/macros/src/compressible/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -//! Compressible account macro generation. - -pub mod anchor_seeds; -pub mod crate_context; -pub mod decompress_context; -pub mod instructions; -pub mod light_compressible; -pub mod pack_unpack; -pub mod seed_providers; -pub mod traits; -pub mod utils; -pub mod variant_enum; diff --git a/sdk-libs/macros/src/finalize/mod.rs b/sdk-libs/macros/src/finalize/mod.rs deleted file mode 100644 index 0758987f54..0000000000 --- a/sdk-libs/macros/src/finalize/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -//! RentFree derive macro for Accounts structs. -//! -//! This module provides: -//! - `#[derive(RentFree)]` - Generates the LightFinalize trait impl for accounts structs -//! with fields marked `#[rentfree(...)]` -//! -//! Note: Instruction handlers are auto-wrapped by `#[rentfree_program]`. - -mod codegen; -mod parse; - -use proc_macro2::TokenStream; -use syn::DeriveInput; - -pub fn derive_light_finalize(input: DeriveInput) -> Result { - let parsed = parse::parse_compressible_struct(&input)?; - codegen::generate_finalize_impl(&parsed) -} diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 64755504ef..aeac65ad48 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -6,11 +6,10 @@ use syn::{parse_macro_input, DeriveInput, ItemStruct}; use utils::into_token_stream; mod account; -mod compressible; mod discriminator; -mod finalize; mod hasher; mod rent_sponsor; +mod rentfree; mod utils; #[proc_macro_derive(LightDiscriminator)] @@ -125,7 +124,7 @@ pub fn light_hasher_sha(input: TokenStream) -> TokenStream { #[proc_macro_derive(HasCompressionInfo)] pub fn has_compression_info(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - into_token_stream(compressible::traits::derive_has_compression_info(input)) + into_token_stream(rentfree::traits::traits::derive_has_compression_info(input)) } /// Legacy CompressAs trait implementation (use Compressible instead). @@ -165,7 +164,7 @@ pub fn has_compression_info(input: TokenStream) -> TokenStream { #[proc_macro_derive(CompressAs, attributes(compress_as))] pub fn compress_as_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as ItemStruct); - into_token_stream(compressible::traits::derive_compress_as(input)) + into_token_stream(rentfree::traits::traits::derive_compress_as(input)) } /// Auto-discovering rent-free program macro that reads external module files. @@ -203,7 +202,7 @@ pub fn compress_as_derive(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn rentfree_program(args: TokenStream, input: TokenStream) -> TokenStream { let module = syn::parse_macro_input!(input as syn::ItemMod); - into_token_stream(compressible::instructions::compressible_program_impl( + into_token_stream(rentfree::program::rentfree_program_impl( args.into(), module, )) @@ -258,7 +257,7 @@ pub fn account(_: TokenStream, input: TokenStream) -> TokenStream { #[proc_macro_derive(Compressible, attributes(compress_as, light_seeds))] pub fn compressible_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(compressible::traits::derive_compressible(input)) + into_token_stream(rentfree::traits::traits::derive_compressible(input)) } /// Automatically implements Pack and Unpack traits for compressible accounts. @@ -285,7 +284,7 @@ pub fn compressible_derive(input: TokenStream) -> TokenStream { #[proc_macro_derive(CompressiblePack)] pub fn compressible_pack(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(compressible::pack_unpack::derive_compressible_pack(input)) + into_token_stream(rentfree::traits::pack_unpack::derive_compressible_pack(input)) } /// Consolidates all required traits for rent-free state accounts into a single derive. @@ -333,7 +332,7 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { #[proc_macro_derive(RentFreeAccount, attributes(compress_as))] pub fn rent_free_account(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(compressible::light_compressible::derive_light_compressible( + into_token_stream(rentfree::traits::light_compressible::derive_rentfree_account( input, )) } @@ -456,5 +455,5 @@ pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { )] pub fn rent_free_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(finalize::derive_light_finalize(input)) + into_token_stream(rentfree::accounts::derive_rentfree(input)) } diff --git a/sdk-libs/macros/src/rentfree/README.md b/sdk-libs/macros/src/rentfree/README.md new file mode 100644 index 0000000000..6c78ca533b --- /dev/null +++ b/sdk-libs/macros/src/rentfree/README.md @@ -0,0 +1,58 @@ +# Rent-Free Macros + +Procedural macros for generating rent-free account types and their hooks for Solana programs. + +## Directory Structure + +``` +rentfree/ +├── mod.rs # Module declaration +├── README.md # This file +├── accounts/ # #[derive(RentFree)] implementation +│ ├── mod.rs # Entry point: derive_rentfree() +│ ├── parse.rs # Parsing #[rentfree], #[light_mint] attributes +│ └── codegen.rs # LightPreInit/LightFinalize trait generation +├── program/ # #[rentfree_program] implementation +│ ├── mod.rs # Entry point: rentfree_program_impl() +│ ├── instructions.rs # Instruction generation and handler wrapping +│ ├── crate_context.rs # Crate scanning for #[derive(Accounts)] structs +│ ├── variant_enum.rs # RentFreeAccountVariant enum generation +│ └── seed_providers.rs # PDA/CToken seed derivation implementations +└── traits/ # Shared trait derive macros + ├── mod.rs # Module declaration + ├── traits.rs # HasCompressionInfo, CompressAs, Compressible + ├── pack_unpack.rs # Pack/Unpack trait implementations + ├── light_compressible.rs # RentFreeAccount combined derive + ├── anchor_seeds.rs # Seed extraction from Anchor attributes + ├── decompress_context.rs # DecompressContext trait generation + └── utils.rs # Shared utility functions +``` + +## Modules + +### `accounts/` - RentFree Derive Macro + +Implements `#[derive(RentFree)]` for Anchor Accounts structs: + +- **parse.rs** - Parses `#[rentfree]`, `#[rentfree_token]`, `#[light_mint]` attributes +- **codegen.rs** - Generates `LightPreInit` and `LightFinalize` trait implementations + +### `program/` - RentFree Program Macro + +Implements `#[rentfree_program]` attribute macro: + +- **instructions.rs** - Main macro logic, generates compress/decompress handlers +- **crate_context.rs** - Scans crate for `#[derive(Accounts)]` structs +- **variant_enum.rs** - Generates `RentFreeAccountVariant` enum with all traits +- **seed_providers.rs** - PDA and CToken seed provider implementations + +### `traits/` - Shared Trait Derives + +Core trait implementations shared across macros: + +- **traits.rs** - `HasCompressionInfo`, `CompressAs`, `Compressible` derives +- **pack_unpack.rs** - Generates `PackedXxx` structs, `Pack`/`Unpack` traits +- **light_compressible.rs** - `RentFreeAccount` combined derive macro +- **anchor_seeds.rs** - Extracts seeds from `#[account(seeds = [...])]` +- **decompress_context.rs** - `DecompressContext` trait generation +- **utils.rs** - Shared utilities (e.g., empty CToken enum generation) diff --git a/sdk-libs/macros/src/finalize/codegen.rs b/sdk-libs/macros/src/rentfree/accounts/codegen.rs similarity index 95% rename from sdk-libs/macros/src/finalize/codegen.rs rename to sdk-libs/macros/src/rentfree/accounts/codegen.rs index f33fb8d98a..3c747b3557 100644 --- a/sdk-libs/macros/src/finalize/codegen.rs +++ b/sdk-libs/macros/src/rentfree/accounts/codegen.rs @@ -15,7 +15,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use super::parse::{ParsedCompressibleStruct, RentFreeField}; +use super::parse::{LightMintField, ParsedRentFreeStruct, RentFreeField}; /// Default rent payment period in epochs (how long to prepay rent for decompressed accounts). const DEFAULT_RENT_PAYMENT_EPOCHS: u8 = 2; @@ -26,12 +26,27 @@ const DEFAULT_WRITE_TOP_UP_LAMPORTS: u32 = 0; /// Generate both trait implementations. /// /// Returns `Err` if the parsed struct has inconsistent state (e.g., params type without ident). -pub fn generate_finalize_impl( - parsed: &ParsedCompressibleStruct, +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 + ), + )); + } + // Get the params type from instruction args (first arg) let params_type = parsed .instruction_args @@ -192,7 +207,7 @@ pub fn generate_finalize_impl( /// After this, Mint is "hot" and usable in instruction body #[allow(clippy::too_many_arguments)] fn generate_pre_init_pdas_and_mints( - parsed: &ParsedCompressibleStruct, + parsed: &ParsedRentFreeStruct, params_ident: &syn::Ident, fee_payer: &TokenStream, compression_config: &TokenStream, @@ -209,7 +224,9 @@ fn generate_pre_init_pdas_and_mints( // Get the first PDA's output tree index (for the state tree output queue) let first_pda_output_tree = &parsed.rentfree_fields[0].output_tree; - // Get the first mint (we only support one mint currently) + // 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]; let mint_field_ident = &mint.field_ident; let mint_signer = &mint.mint_signer; @@ -402,7 +419,7 @@ fn generate_pre_init_pdas_and_mints( /// After this, CMint is "hot" and usable in instruction body #[allow(clippy::too_many_arguments)] fn generate_pre_init_mints_only( - parsed: &ParsedCompressibleStruct, + parsed: &ParsedRentFreeStruct, params_ident: &syn::Ident, fee_payer: &TokenStream, ctoken_config: &TokenStream, @@ -410,7 +427,9 @@ fn generate_pre_init_mints_only( light_token_program: &TokenStream, ctoken_cpi_authority: &TokenStream, ) -> TokenStream { - // Get the first mint (we only support one mint currently) + // 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]; let mint_field_ident = &mint.field_ident; let mint_signer = &mint.mint_signer; @@ -551,7 +570,7 @@ fn generate_pre_init_mints_only( /// Generate LightPreInit body for PDAs only (no mints) /// After this, compressed addresses are registered fn generate_pre_init_pdas_only( - parsed: &ParsedCompressibleStruct, + parsed: &ParsedRentFreeStruct, params_ident: &syn::Ident, fee_payer: &TokenStream, compression_config: &TokenStream, diff --git a/sdk-libs/macros/src/rentfree/accounts/mod.rs b/sdk-libs/macros/src/rentfree/accounts/mod.rs new file mode 100644 index 0000000000..42e597fb99 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/accounts/mod.rs @@ -0,0 +1,17 @@ +//! Rent-free accounts derive macro implementation. +//! +//! This module provides `#[derive(RentFree)]` which generates: +//! - `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 + +pub mod codegen; +pub mod parse; + +use proc_macro2::TokenStream; +use syn::DeriveInput; + +pub fn derive_rentfree(input: DeriveInput) -> Result { + let parsed = parse::parse_rentfree_struct(&input)?; + codegen::generate_rentfree_impl(&parsed) +} diff --git a/sdk-libs/macros/src/finalize/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs similarity index 86% rename from sdk-libs/macros/src/finalize/parse.rs rename to sdk-libs/macros/src/rentfree/accounts/parse.rs index 6dfa4d35a2..ae44eb2ccf 100644 --- a/sdk-libs/macros/src/finalize/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -7,10 +7,10 @@ use syn::{ }; // Import shared types from anchor_seeds module -pub(super) use crate::compressible::anchor_seeds::extract_account_inner_type; +pub(super) use crate::rentfree::traits::anchor_seeds::extract_account_inner_type; /// Parsed representation of a struct with rentfree and light_mint fields. -pub(super) struct ParsedCompressibleStruct { +pub(super) struct ParsedRentFreeStruct { pub struct_name: Ident, pub generics: syn::Generics, pub rentfree_fields: Vec, @@ -191,9 +191,9 @@ fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Result Result { +) -> Result { let struct_name = input.ident.clone(); let generics = input.generics.clone(); @@ -265,9 +265,21 @@ pub(super) fn parse_compressible_struct( ctoken_cpi_authority_field = Some(field_ident.clone()); } + // Track if this field already has a compression attribute + let mut has_compression_attr = false; + // Look for #[rentfree] or #[rentfree(...)] attribute for attr in &field.attrs { if attr.path().is_ident("rentfree") { + // Check for duplicate compression attributes on same field + if has_compression_attr { + return Err(Error::new_spanned( + attr, + "Field already has a compression attribute (#[rentfree] or #[light_mint]). \ + Only one is allowed per field.", + )); + } + has_compression_attr = true; // Handle both #[rentfree] and #[rentfree(...)] let args: RentFreeArgs = match &attr.meta { syn::Meta::Path(_) => { @@ -297,12 +309,13 @@ pub(super) fn parse_compressible_struct( syn::parse_quote!(params.create_accounts_proof.output_state_tree_index) }); - // Validate this is an Account type + // Validate this is an Account type (or Box) let (is_boxed, _inner_type) = extract_account_inner_type(&field.ty).ok_or_else(|| { Error::new_spanned( &field.ty, - "#[rentfree] can only be applied to Account<...> fields", + "#[rentfree] can only be applied to Account<...> or Box> fields. \ + Nested Box> is not supported.", ) })?; @@ -316,8 +329,21 @@ pub(super) fn parse_compressible_struct( break; } + // TODO(diff-pr): Add parsing for #[rentfree_token(...)] attribute for token accounts and ATAs. + // Would need RentFreeTokenField struct with: field_ident, authority_seeds, mint field ref. + // Look for #[light_mint(...)] attribute if attr.path().is_ident("light_mint") { + // Check for duplicate compression attributes on same field + if has_compression_attr { + return Err(Error::new_spanned( + attr, + "Field already has a compression attribute (#[rentfree] or #[light_mint]). \ + Only one is allowed per field.", + )); + } + has_compression_attr = true; + let args: LightMintArgs = attr.parse_args()?; // Validate required fields @@ -352,7 +378,18 @@ pub(super) fn parse_compressible_struct( } } - Ok(ParsedCompressibleStruct { + // Validation: #[rentfree] and #[light_mint] require #[instruction] attribute + if (!rentfree_fields.is_empty() || !light_mint_fields.is_empty()) + && instruction_args.is_none() + { + return Err(Error::new_spanned( + input, + "#[rentfree] and #[light_mint] fields require #[instruction(params: YourParamsType)] \ + attribute on the struct", + )); + } + + Ok(ParsedRentFreeStruct { struct_name, generics, rentfree_fields, diff --git a/sdk-libs/macros/src/rentfree/mod.rs b/sdk-libs/macros/src/rentfree/mod.rs new file mode 100644 index 0000000000..67d141cc2f --- /dev/null +++ b/sdk-libs/macros/src/rentfree/mod.rs @@ -0,0 +1,10 @@ +//! Rent-free account compression macros. +//! +//! This module organizes all rent-free related macros: +//! - `program/` - `#[rentfree_program]` attribute macro for program-level auto-discovery +//! - `accounts/` - `#[derive(RentFree)]` derive macro for Accounts structs +//! - `traits/` - Shared trait derive macros (Compressible, Pack, HasCompressionInfo, etc.) + +pub mod accounts; +pub mod program; +pub mod traits; diff --git a/sdk-libs/macros/src/compressible/crate_context.rs b/sdk-libs/macros/src/rentfree/program/crate_context.rs similarity index 100% rename from sdk-libs/macros/src/compressible/crate_context.rs rename to sdk-libs/macros/src/rentfree/program/crate_context.rs diff --git a/sdk-libs/macros/src/compressible/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs similarity index 97% rename from sdk-libs/macros/src/compressible/instructions.rs rename to sdk-libs/macros/src/rentfree/program/instructions.rs index 65bea72992..83d477c58a 100644 --- a/sdk-libs/macros/src/compressible/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -335,13 +335,13 @@ impl Parse for InstructionDataSpec { pub fn generate_decompress_context_impl( _variant: InstructionVariant, - pda_ctx_seeds: Vec, + pda_ctx_seeds: Vec, token_variant_ident: Ident, ) -> Result { let lifetime: syn::Lifetime = syn::parse_quote!('info); let trait_impl = - crate::compressible::decompress_context::generate_decompress_context_trait_impl( + crate::rentfree::traits::decompress_context::generate_decompress_context_trait_impl( pda_ctx_seeds, token_variant_ident, lifetime, @@ -851,7 +851,7 @@ fn generate_error_codes(variant: InstructionVariant) -> Result { Ok(quote! { #[error_code] - pub enum CompressibleInstructionError { + pub enum RentFreeInstructionError { #base_errors #variant_specific_errors } @@ -860,9 +860,9 @@ fn generate_error_codes(variant: InstructionVariant) -> Result { /// Convert ClassifiedSeed to SeedElement (Punctuated) fn convert_classified_to_seed_elements( - seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], + seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], ) -> Punctuated { - use crate::compressible::anchor_seeds::ClassifiedSeed; + use crate::rentfree::traits::anchor_seeds::ClassifiedSeed; let mut result = Punctuated::new(); for seed in seeds { @@ -915,7 +915,7 @@ fn convert_classified_to_seed_elements( } fn convert_classified_to_seed_elements_vec( - seeds: &[crate::compressible::anchor_seeds::ClassifiedSeed], + seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], ) -> Vec { convert_classified_to_seed_elements(seeds) .into_iter() @@ -936,14 +936,14 @@ fn generate_from_extracted_seeds( let content = module.content.as_mut().unwrap(); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { - crate::compressible::seed_providers::generate_ctoken_account_variant_enum( + super::seed_providers::generate_ctoken_account_variant_enum( token_seed_specs, )? } else { - crate::compressible::utils::generate_empty_ctoken_enum() + crate::rentfree::traits::utils::generate_empty_ctoken_enum() } } else { - crate::compressible::utils::generate_empty_ctoken_enum() + crate::rentfree::traits::utils::generate_empty_ctoken_enum() }; if let Some(ref token_seed_specs) = token_seeds { @@ -958,14 +958,14 @@ fn generate_from_extracted_seeds( } } - let pda_ctx_seeds: Vec = pda_seeds + let pda_ctx_seeds: Vec = pda_seeds .as_ref() .map(|specs| { specs .iter() .map(|spec| { let ctx_fields = extract_ctx_seed_fields(&spec.seeds); - crate::compressible::variant_enum::PdaCtxSeedInfo::new( + super::variant_enum::PdaCtxSeedInfo::new( spec.variant.clone(), ctx_fields, ) @@ -976,7 +976,7 @@ fn generate_from_extracted_seeds( let account_type_refs: Vec<&Ident> = account_types.iter().collect(); let enum_and_traits = - crate::compressible::variant_enum::compressed_account_variant_with_ctx_seeds( + super::variant_enum::compressed_account_variant_with_ctx_seeds( &account_type_refs, &pda_ctx_seeds, )?; @@ -1015,7 +1015,7 @@ fn generate_from_extracted_seeds( let data_verifications: Vec<_> = data_fields.iter().map(|field| { quote! { if data.#field != seeds.#field { - return std::result::Result::Err(CompressibleInstructionError::SeedMismatch.into()); + return std::result::Result::Err(RentFreeInstructionError::SeedMismatch.into()); } } }).collect(); @@ -1265,7 +1265,7 @@ fn generate_from_extracted_seeds( } }; - let client_functions = crate::compressible::seed_providers::generate_client_seed_functions( + let client_functions = super::seed_providers::generate_client_seed_functions( &account_types, &pda_seeds, &token_seeds, @@ -1312,7 +1312,7 @@ fn generate_from_extracted_seeds( if let Some(ref seeds) = token_seeds { if !seeds.is_empty() { let impl_code = - crate::compressible::seed_providers::generate_ctoken_seed_provider_implementation( + super::seed_providers::generate_ctoken_seed_provider_implementation( seeds, )?; let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; @@ -1447,11 +1447,9 @@ fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> s } #[inline(never)] -pub fn compressible_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { - use crate::compressible::{ - anchor_seeds::{extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec}, - crate_context::CrateContext, - }; +pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { + use crate::rentfree::traits::anchor_seeds::{extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec}; + use super::crate_context::CrateContext; if module.content.is_none() { return Err(macro_error!(&module, "Module must have a body")); diff --git a/sdk-libs/macros/src/rentfree/program/mod.rs b/sdk-libs/macros/src/rentfree/program/mod.rs new file mode 100644 index 0000000000..4f2b929448 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/mod.rs @@ -0,0 +1,13 @@ +//! Rent-free program macro implementation. +//! +//! This module provides `#[rentfree_program]` attribute macro that: +//! - Automatically discovers #[rentfree] fields in Accounts structs +//! - Auto-wraps instruction handlers with light_pre_init/light_finalize logic +//! - Generates all necessary types, enums, and instruction handlers + +pub mod crate_context; +pub mod instructions; +pub mod seed_providers; +pub mod variant_enum; + +pub use instructions::rentfree_program_impl; diff --git a/sdk-libs/macros/src/compressible/seed_providers.rs b/sdk-libs/macros/src/rentfree/program/seed_providers.rs similarity index 99% rename from sdk-libs/macros/src/compressible/seed_providers.rs rename to sdk-libs/macros/src/rentfree/program/seed_providers.rs index b2d54de2cc..298c0e761e 100644 --- a/sdk-libs/macros/src/compressible/seed_providers.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_providers.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; -use crate::compressible::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; +use super::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; /// Extract ctx.* field names from seed elements (both token seeds and authority seeds) fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { @@ -378,7 +378,7 @@ pub fn generate_ctoken_seed_provider_implementation( let authority_arm = quote! { #pattern => { Err(solana_program_error::ProgramError::Custom( - CompressibleInstructionError::MissingSeedAccount.into() + RentFreeInstructionError::MissingSeedAccount.into() )) } }; diff --git a/sdk-libs/macros/src/compressible/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs similarity index 100% rename from sdk-libs/macros/src/compressible/variant_enum.rs rename to sdk-libs/macros/src/rentfree/program/variant_enum.rs diff --git a/sdk-libs/macros/src/compressible/anchor_seeds.rs b/sdk-libs/macros/src/rentfree/traits/anchor_seeds.rs similarity index 97% rename from sdk-libs/macros/src/compressible/anchor_seeds.rs rename to sdk-libs/macros/src/rentfree/traits/anchor_seeds.rs index 96c1031a13..fb2e810fec 100644 --- a/sdk-libs/macros/src/compressible/anchor_seeds.rs +++ b/sdk-libs/macros/src/rentfree/traits/anchor_seeds.rs @@ -323,6 +323,16 @@ pub fn extract_account_inner_type(ty: &Type) -> Option<(bool, Ident)> { // Check for Box> if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { if let Some(syn::GenericArgument::Type(inner_ty)) = args.args.first() { + // Check for nested Box> which is not supported + if let Type::Path(inner_path) = inner_ty { + if let Some(inner_seg) = inner_path.path.segments.last() { + if inner_seg.ident == "Box" { + // Nested Box detected - return None to signal unsupported type + return None; + } + } + } + if let Some((_, inner_type)) = extract_account_inner_type(inner_ty) { return Some((true, inner_type)); } diff --git a/sdk-libs/macros/src/compressible/decompress_context.rs b/sdk-libs/macros/src/rentfree/traits/decompress_context.rs similarity index 99% rename from sdk-libs/macros/src/compressible/decompress_context.rs rename to sdk-libs/macros/src/rentfree/traits/decompress_context.rs index cdccdfe8a3..44690279f5 100644 --- a/sdk-libs/macros/src/compressible/decompress_context.rs +++ b/sdk-libs/macros/src/rentfree/traits/decompress_context.rs @@ -5,7 +5,7 @@ use quote::{format_ident, quote}; use syn::{Ident, Result}; // Re-export from variant_enum for convenience -pub use crate::compressible::variant_enum::PdaCtxSeedInfo; +pub use crate::rentfree::program::variant_enum::PdaCtxSeedInfo; pub fn generate_decompress_context_trait_impl( pda_ctx_seeds: Vec, diff --git a/sdk-libs/macros/src/compressible/light_compressible.rs b/sdk-libs/macros/src/rentfree/traits/light_compressible.rs similarity index 94% rename from sdk-libs/macros/src/compressible/light_compressible.rs rename to sdk-libs/macros/src/rentfree/traits/light_compressible.rs index 95f38f8fe5..bc0863f072 100644 --- a/sdk-libs/macros/src/compressible/light_compressible.rs +++ b/sdk-libs/macros/src/rentfree/traits/light_compressible.rs @@ -11,9 +11,9 @@ use quote::quote; use syn::{DeriveInput, Fields, ItemStruct, Result}; use crate::{ - compressible::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, discriminator::discriminator, hasher::derive_light_hasher_sha, + rentfree::traits::{pack_unpack::derive_compressible_pack, traits::derive_compressible}, }; /// Derives all required traits for a compressible account. @@ -53,7 +53,7 @@ use crate::{ /// /// - The `compression_info` field is auto-detected and handled specially (no `#[skip]` needed) /// - SHA256 hashing serializes the entire struct, so `#[hash]` is not needed -pub fn derive_light_compressible(input: DeriveInput) -> Result { +pub fn derive_rentfree_account(input: DeriveInput) -> Result { // Convert DeriveInput to ItemStruct for macros that need it let item_struct = derive_input_to_item_struct(&input)?; @@ -128,7 +128,7 @@ mod tests { } }; - let result = derive_light_compressible(input); + let result = derive_rentfree_account(input); assert!(result.is_ok(), "LightCompressible should succeed"); let output = result.unwrap().to_string(); @@ -181,7 +181,7 @@ mod tests { } }; - let result = derive_light_compressible(input); + let result = derive_rentfree_account(input); assert!( result.is_ok(), "LightCompressible with compress_as should succeed" @@ -203,7 +203,7 @@ mod tests { } }; - let result = derive_light_compressible(input); + let result = derive_rentfree_account(input); assert!( result.is_ok(), "LightCompressible without Pubkey fields should succeed" @@ -235,7 +235,7 @@ mod tests { } }; - let result = derive_light_compressible(input); + let result = derive_rentfree_account(input); assert!(result.is_err(), "LightCompressible should fail for enums"); } @@ -248,7 +248,7 @@ mod tests { } }; - let result = derive_light_compressible(input); + let result = derive_rentfree_account(input); // Compressible derive validates compression_info field assert!( result.is_err(), diff --git a/sdk-libs/macros/src/rentfree/traits/mod.rs b/sdk-libs/macros/src/rentfree/traits/mod.rs new file mode 100644 index 0000000000..8669687ab9 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/traits/mod.rs @@ -0,0 +1,16 @@ +//! Shared trait derive macros for compressible accounts. +//! +//! This module provides: +//! - `anchor_seeds` - Seed extraction from Anchor account attributes +//! - `decompress_context` - Decompression context utilities +//! - `light_compressible` - Combined RentFreeAccount derive macro +//! - `pack_unpack` - Pack/Unpack trait implementations +//! - `traits` - HasCompressionInfo, Compressible, CompressAs traits +//! - `utils` - Shared utility functions + +pub mod anchor_seeds; +pub mod decompress_context; +pub mod light_compressible; +pub mod pack_unpack; +pub mod traits; +pub mod utils; diff --git a/sdk-libs/macros/src/compressible/pack_unpack.rs b/sdk-libs/macros/src/rentfree/traits/pack_unpack.rs similarity index 100% rename from sdk-libs/macros/src/compressible/pack_unpack.rs rename to sdk-libs/macros/src/rentfree/traits/pack_unpack.rs diff --git a/sdk-libs/macros/src/compressible/traits.rs b/sdk-libs/macros/src/rentfree/traits/traits.rs similarity index 100% rename from sdk-libs/macros/src/compressible/traits.rs rename to sdk-libs/macros/src/rentfree/traits/traits.rs diff --git a/sdk-libs/macros/src/compressible/utils.rs b/sdk-libs/macros/src/rentfree/traits/utils.rs similarity index 100% rename from sdk-libs/macros/src/compressible/utils.rs rename to sdk-libs/macros/src/rentfree/traits/utils.rs From 70e3c99786be1892453698ea44f8cf3028888983 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 02:29:05 +0000 Subject: [PATCH 4/9] unify light mint --- .../macros/src/rentfree/accounts/codegen.rs | 421 +++--------------- .../src/rentfree/accounts/light_mint.rs | 357 +++++++++++++++ sdk-libs/macros/src/rentfree/accounts/mod.rs | 5 +- .../macros/src/rentfree/accounts/parse.rs | 186 ++------ .../src/rentfree/program/instructions.rs | 30 +- .../src/rentfree/program/seed_providers.rs | 298 +++++-------- 6 files changed, 578 insertions(+), 719 deletions(-) create mode 100644 sdk-libs/macros/src/rentfree/accounts/light_mint.rs diff --git a/sdk-libs/macros/src/rentfree/accounts/codegen.rs b/sdk-libs/macros/src/rentfree/accounts/codegen.rs index 3c747b3557..98b27527c1 100644 --- a/sdk-libs/macros/src/rentfree/accounts/codegen.rs +++ b/sdk-libs/macros/src/rentfree/accounts/codegen.rs @@ -15,13 +15,16 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use super::parse::{LightMintField, ParsedRentFreeStruct, RentFreeField}; - -/// 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; +use super::light_mint::{generate_mint_action_invocation, MintActionConfig}; +use super::parse::{ParsedRentFreeStruct, RentFreeField}; + +/// 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 both trait implementations. /// @@ -47,17 +50,13 @@ pub(super) fn generate_rentfree_impl( )); } - // Get the params type from instruction args (first arg) - let params_type = parsed - .instruction_args - .as_ref() - .and_then(|args| args.first()) - .map(|arg| &arg.ty); - - let params_type = match params_type { - Some(ty) => ty, + // 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 + // 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 { @@ -85,58 +84,24 @@ pub(super) fn generate_rentfree_impl( } }; - let params_ident = parsed - .instruction_args - .as_ref() - .and_then(|args| args.first()) - .map(|arg| &arg.name) - .ok_or_else(|| { - syn::Error::new( - parsed.struct_name.span(), - "internal error: instruction params type exists but params ident is missing", - ) - })?; + 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(); - // Get fee payer field - let fee_payer = parsed - .fee_payer_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { fee_payer }); - - let compression_config = parsed - .compression_config_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { compression_config }); - - // CToken accounts for decompress - let ctoken_config = parsed - .ctoken_config_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { ctoken_compressible_config }); - - let ctoken_rent_sponsor = parsed - .ctoken_rent_sponsor_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { ctoken_rent_sponsor }); - - let light_token_program = parsed - .ctoken_program_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { light_token_program }); - - let ctoken_cpi_authority = parsed - .ctoken_cpi_authority_field - .as_ref() - .map(|f| quote! { #f }) - .unwrap_or_else(|| quote! { ctoken_cpi_authority }); + // 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 @@ -216,8 +181,7 @@ fn generate_pre_init_pdas_and_mints( light_token_program: &TokenStream, ctoken_cpi_authority: &TokenStream, ) -> TokenStream { - let (compress_blocks, new_addr_idents) = - generate_pda_compress_blocks(&parsed.rentfree_fields); + 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(); @@ -228,45 +192,22 @@ fn generate_pre_init_pdas_and_mints( // 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]; - let mint_field_ident = &mint.field_ident; - let mint_signer = &mint.mint_signer; - let authority = &mint.authority; - let decimals = &mint.decimals; - let address_tree_info = &mint.address_tree_info; - - // Use explicit signer_seeds if provided, otherwise empty - let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { - quote! { #seeds } - } else { - quote! { &[] as &[&[u8]] } - }; - - // Build freeze_authority expression - let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { - quote! { Some(*self.#freeze_auth.to_account_info().key) } - } else { - quote! { None } - }; - - // rent_payment defaults to DEFAULT_RENT_PAYMENT_EPOCHS - let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { - quote! { #rent } - } else { - let default = DEFAULT_RENT_PAYMENT_EPOCHS; - quote! { #default } - }; - - // write_top_up defaults to DEFAULT_WRITE_TOP_UP_LAMPORTS - let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { - quote! { #top_up } - } else { - let default = DEFAULT_WRITE_TOP_UP_LAMPORTS; - quote! { #default } - }; // 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( @@ -305,110 +246,7 @@ fn generate_pre_init_pdas_and_mints( .invoke_write_to_cpi_context_first(cpi_context_accounts)?; // Step 2: Build and invoke mint_action with decompress + CPI context - { - let __tree_info = &#address_tree_info; - let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; - // Output queue is the state tree queue (same as the PDAs' output tree) - let __output_tree_index = #first_pda_output_tree; - let output_queue = cpi_accounts.get_tree_account_info(__output_tree_index as usize)?; - let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); - - let mint_signer_key = self.#mint_signer.to_account_info().key; - let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); - - let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() - .expect("proof is required for mint creation"); - - let __freeze_authority: Option = #freeze_authority_tokens; - - // Build compressed mint instruction data - let compressed_mint_data = light_token_interface::instructions::mint_action::MintInstructionData { - supply: 0, - decimals: #decimals, - metadata: light_token_interface::state::MintMetadata { - version: 3, - mint: mint_pda.to_bytes().into(), - mint_decompressed: false, - mint_signer: mint_signer_key.to_bytes(), - bump: _cmint_bump, - }, - mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), - freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), - extensions: None, - }; - - // Build mint action instruction data with decompress - let mut instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( - __tree_info.root_index, - __proof, - compressed_mint_data, - ) - .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { - rent_payment: #rent_payment_tokens, - write_top_up: #write_top_up_tokens, - }) - .with_cpi_context(light_token_interface::instructions::mint_action::CpiContext { - address_tree_pubkey: __tree_pubkey.to_bytes(), - set_context: false, - first_set_context: false, // PDAs already wrote to context - // in_tree_index is 1-indexed and points to the state queue (for CPI context validation) - // The Light System Program does `in_tree_index - 1` and uses queue's associated_merkle_tree - in_tree_index: __output_tree_index + 1, // +1 because 1-indexed - in_queue_index: __output_tree_index, - out_queue_index: __output_tree_index, // Output state queue - token_out_queue_index: 0, - assigned_account_index: #mint_assigned_index, - read_only_address_trees: [0; 4], - }); - - // Build account metas with compressible CMint - let mut meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( - *self.#fee_payer.to_account_info().key, - *self.#authority.to_account_info().key, - *mint_signer_key, - __tree_pubkey, - *output_queue.key, - ) - .with_compressible_mint( - mint_pda, - *self.#ctoken_config.to_account_info().key, - *self.#ctoken_rent_sponsor.to_account_info().key, - ); - - meta_config.cpi_context = Some(*cpi_accounts.cpi_context()?.key); - - let account_metas = meta_config.to_account_metas(); - - use light_compressed_account::instruction_data::traits::LightInstructionData; - let ix_data = instruction_data.data() - .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; - - let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { - program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), - accounts: account_metas, - data: ix_data, - }; - - // Build account infos and invoke - // Include all accounts needed for mint_action with decompress - let mut account_infos = cpi_accounts.to_account_infos(); - // Add ctoken-specific accounts that aren't in the Light System CPI accounts - account_infos.push(self.#light_token_program.to_account_info()); - account_infos.push(self.#ctoken_cpi_authority.to_account_info()); - account_infos.push(self.#mint_field_ident.to_account_info()); - account_infos.push(self.#ctoken_config.to_account_info()); - account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); - account_infos.push(self.#authority.to_account_info()); - account_infos.push(self.#mint_signer.to_account_info()); - account_infos.push(self.#fee_payer.to_account_info()); - - let signer_seeds: &[&[u8]] = #signer_seeds_tokens; - if signer_seeds.is_empty() { - anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; - } else { - anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; - } - } + #mint_invocation Ok(true) } @@ -431,41 +269,18 @@ fn generate_pre_init_mints_only( // 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]; - let mint_field_ident = &mint.field_ident; - let mint_signer = &mint.mint_signer; - let authority = &mint.authority; - let decimals = &mint.decimals; - let address_tree_info = &mint.address_tree_info; - - // Use explicit signer_seeds if provided, otherwise empty - let signer_seeds_tokens = if let Some(seeds) = &mint.signer_seeds { - quote! { #seeds } - } else { - quote! { &[] as &[&[u8]] } - }; - - // Build freeze_authority expression - let freeze_authority_tokens = if let Some(freeze_auth) = &mint.freeze_authority { - quote! { Some(*self.#freeze_auth.to_account_info().key) } - } else { - quote! { None } - }; - // rent_payment defaults to DEFAULT_RENT_PAYMENT_EPOCHS - let rent_payment_tokens = if let Some(rent) = &mint.rent_payment { - quote! { #rent } - } else { - let default = DEFAULT_RENT_PAYMENT_EPOCHS; - quote! { #default } - }; - - // write_top_up defaults to DEFAULT_WRITE_TOP_UP_LAMPORTS - let write_top_up_tokens = if let Some(top_up) = &mint.write_top_up { - quote! { #top_up } - } else { - let default = DEFAULT_WRITE_TOP_UP_LAMPORTS; - quote! { #default } - }; + // 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) @@ -476,92 +291,7 @@ fn generate_pre_init_mints_only( ); // Build and invoke mint_action with decompress - { - let __tree_info = &#address_tree_info; - let address_tree = cpi_accounts.get_tree_account_info(__tree_info.address_merkle_tree_pubkey_index as usize)?; - let output_queue = cpi_accounts.get_tree_account_info(__tree_info.address_queue_pubkey_index as usize)?; - let __tree_pubkey: solana_pubkey::Pubkey = light_sdk::light_account_checks::AccountInfoTrait::pubkey(address_tree); - - let mint_signer_key = self.#mint_signer.to_account_info().key; - let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); - - let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() - .expect("proof is required for mint creation"); - - let __freeze_authority: Option = #freeze_authority_tokens; - - // Build compressed mint instruction data - let compressed_mint_data = light_token_interface::instructions::mint_action::MintInstructionData { - supply: 0, - decimals: #decimals, - metadata: light_token_interface::state::MintMetadata { - version: 3, - mint: mint_pda.to_bytes().into(), - mint_decompressed: false, - mint_signer: mint_signer_key.to_bytes(), - bump: _cmint_bump, - }, - mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), - freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), - extensions: None, - }; - - // Build mint action instruction data with decompress (no CPI context) - let instruction_data = light_token_interface::instructions::mint_action::MintActionCompressedInstructionData::new_mint( - __tree_info.root_index, - __proof, - compressed_mint_data, - ) - .with_decompress_mint(light_token_interface::instructions::mint_action::DecompressMintAction { - rent_payment: #rent_payment_tokens, - write_top_up: #write_top_up_tokens, - }); - - // Build account metas with compressible CMint - let meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( - *self.#fee_payer.to_account_info().key, - *self.#authority.to_account_info().key, - *mint_signer_key, - __tree_pubkey, - *output_queue.key, - ) - .with_compressible_mint( - mint_pda, - *self.#ctoken_config.to_account_info().key, - *self.#ctoken_rent_sponsor.to_account_info().key, - ); - - let account_metas = meta_config.to_account_metas(); - - use light_compressed_account::instruction_data::traits::LightInstructionData; - let ix_data = instruction_data.data() - .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; - - let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { - program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), - accounts: account_metas, - data: ix_data, - }; - - // Build account infos and invoke - let mut account_infos = cpi_accounts.to_account_infos(); - // Add ctoken-specific accounts - account_infos.push(self.#light_token_program.to_account_info()); - account_infos.push(self.#ctoken_cpi_authority.to_account_info()); - account_infos.push(self.#mint_field_ident.to_account_info()); - account_infos.push(self.#ctoken_config.to_account_info()); - account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); - account_infos.push(self.#authority.to_account_info()); - account_infos.push(self.#mint_signer.to_account_info()); - account_infos.push(self.#fee_payer.to_account_info()); - - let signer_seeds: &[&[u8]] = #signer_seeds_tokens; - if signer_seeds.is_empty() { - anchor_lang::solana_program::program::invoke(&mint_action_ix, &account_infos)?; - } else { - anchor_lang::solana_program::program::invoke_signed(&mint_action_ix, &account_infos, &[signer_seeds])?; - } - } + #mint_invocation Ok(true) } @@ -575,8 +305,7 @@ fn generate_pre_init_pdas_only( fee_payer: &TokenStream, compression_config: &TokenStream, ) -> TokenStream { - let (compress_blocks, new_addr_idents) = - generate_pda_compress_blocks(&parsed.rentfree_fields); + let (compress_blocks, new_addr_idents) = generate_pda_compress_blocks(&parsed.rentfree_fields); let rentfree_count = parsed.rentfree_fields.len() as u8; quote! { @@ -621,7 +350,7 @@ fn generate_pda_compress_blocks(fields: &[RentFreeField]) -> (Vec, let ident = &field.ident; let addr_tree_info = &field.address_tree_info; let output_tree = &field.output_tree; - let acc_ty_path = extract_inner_account_type(&field.ty); + let 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); @@ -669,7 +398,7 @@ fn generate_pda_compress_blocks(fields: &[RentFreeField]) -> (Vec, // Get mutable reference to inner account data let #account_data_ident = #deref_expr; - let #compressed_infos_ident = light_sdk::compressible::prepare_compressed_account_on_init::<#acc_ty_path>( + let #compressed_infos_ident = light_sdk::compressible::prepare_compressed_account_on_init::<#inner_type>( &#account_info_ident, #account_data_ident, &compression_config_data, @@ -686,35 +415,3 @@ fn generate_pda_compress_blocks(fields: &[RentFreeField]) -> (Vec, (blocks, addr_idents) } - -/// Extract the inner type T from Account<'info, T> or Box> -fn extract_inner_account_type(ty: &syn::Type) -> TokenStream { - match ty { - syn::Type::Path(type_path) => { - let path = &type_path.path; - if let Some(segment) = path.segments.last() { - let ident_str = segment.ident.to_string(); - - if ident_str == "Account" { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - for arg in &args.args { - if let syn::GenericArgument::Type(inner_ty) = arg { - return quote! { #inner_ty }; - } - } - } - } - - if ident_str == "Box" { - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - if let Some(syn::GenericArgument::Type(inner)) = args.args.first() { - return extract_inner_account_type(inner); - } - } - } - } - quote! { #ty } - } - _ => quote! { #ty }, - } -} diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs new file mode 100644 index 0000000000..f2ded25a22 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -0,0 +1,357 @@ +//! Light mint parsing and code generation. +//! +//! This module handles: +//! - Parsing of #[light_mint(...)] attributes +//! - Code generation for mint_action CPI invocations + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Expr, Ident, Token, +}; + +use super::parse::KeyValueArg; + +// ============================================================================ +// Parsing +// ============================================================================ + +/// A field marked with #[light_mint(...)] +pub(super) struct LightMintField { + /// The field name where #[light_mint] is attached (CMint account) + pub field_ident: Ident, + /// The mint_signer field (AccountInfo that seeds the mint PDA) + pub mint_signer: Expr, + /// The authority for mint operations + pub authority: Expr, + /// Decimals for the mint + pub decimals: Expr, + /// Address tree info expression + pub address_tree_info: Expr, + /// Optional freeze authority + pub freeze_authority: Option, + /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) + pub signer_seeds: Option, + /// Rent payment epochs for decompression (default: 2) + pub rent_payment: Option, + /// Write top-up lamports for decompression (default: 0) + pub write_top_up: Option, +} + +/// Arguments inside #[light_mint(...)] +struct LightMintArgs { + mint_signer: Option, + authority: Option, + decimals: Option, + address_tree_info: Option, + freeze_authority: Option, + signer_seeds: Option, + rent_payment: Option, + write_top_up: Option, +} + +impl Parse for LightMintArgs { + fn parse(input: ParseStream) -> syn::Result { + let mut args = LightMintArgs { + mint_signer: None, + authority: None, + decimals: None, + address_tree_info: None, + freeze_authority: None, + signer_seeds: None, + rent_payment: None, + write_top_up: None, + }; + + let content: Punctuated = Punctuated::parse_terminated(input)?; + + for arg in content { + match arg.name.to_string().as_str() { + "mint_signer" => args.mint_signer = Some(arg.value), + "authority" => args.authority = Some(arg.value), + "decimals" => args.decimals = Some(arg.value), + "address_tree_info" => args.address_tree_info = Some(arg.value), + "freeze_authority" => args.freeze_authority = Some(arg.value), + "signer_seeds" => args.signer_seeds = Some(arg.value), + "rent_payment" => args.rent_payment = Some(arg.value), + "write_top_up" => args.write_top_up = Some(arg.value), + other => { + return Err(syn::Error::new( + arg.name.span(), + format!("unknown light_mint attribute: {}", other), + )) + } + } + } + + Ok(args) + } +} + +/// Parse #[light_mint(...)] attribute from a field. +/// Returns None if no light_mint attribute, Some(LightMintField) if found. +pub(super) fn parse_light_mint_attr( + field: &syn::Field, + field_ident: &Ident, +) -> Result, syn::Error> { + for attr in &field.attrs { + if attr.path().is_ident("light_mint") { + let args: LightMintArgs = attr.parse_args()?; + + // Validate required fields + let mint_signer = args + .mint_signer + .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires mint_signer"))?; + let authority = args + .authority + .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires authority"))?; + let decimals = args + .decimals + .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires decimals"))?; + + // address_tree_info defaults to params.create_accounts_proof.address_tree_info + let address_tree_info = args.address_tree_info.unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + + return Ok(Some(LightMintField { + field_ident: field_ident.clone(), + mint_signer, + authority, + decimals, + address_tree_info, + freeze_authority: args.freeze_authority, + signer_seeds: args.signer_seeds, + rent_payment: args.rent_payment, + write_top_up: args.write_top_up, + })); + } + } + Ok(None) +} + +// ============================================================================ +// 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]] } + } +} + +/// 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 } + } +} + +/// 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 } + } +} + +/// 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 } + } +} + +/// 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; + + let mint_field_ident = &mint.field_ident; + let mint_signer = &mint.mint_signer; + let authority = &mint.authority; + let decimals = &mint.decimals; + let address_tree_info = &mint.address_tree_info; + + let 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); + + // 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 } + }; + + quote! { + { + 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 + 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); + + let mint_signer_key = self.#mint_signer.to_account_info().key; + let (mint_pda, _cmint_bump) = light_token_sdk::token::find_mint_address(mint_signer_key); + + let __proof: light_token_sdk::CompressedProof = #params_ident.create_accounts_proof.proof.0.clone() + .expect("proof is required for mint creation"); + + let __freeze_authority: Option = #freeze_authority_tokens; + + let compressed_mint_data = light_token_interface::instructions::mint_action::MintInstructionData { + supply: 0, + decimals: #decimals, + metadata: light_token_interface::state::MintMetadata { + version: 3, + mint: mint_pda.to_bytes().into(), + mint_decompressed: false, + mint_signer: mint_signer_key.to_bytes(), + bump: _cmint_bump, + }, + mint_authority: Some((*self.#authority.to_account_info().key).to_bytes().into()), + freeze_authority: __freeze_authority.map(|a| a.to_bytes().into()), + extensions: None, + }; + + #instruction_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, + }) + #cpi_context_chain; + + let mut meta_config = light_token_sdk::compressed_token::mint_action::MintActionMetaConfig::new_create_mint( + *self.#fee_payer.to_account_info().key, + *self.#authority.to_account_info().key, + *mint_signer_key, + __tree_pubkey, + *output_queue.key, + ) + .with_compressible_mint( + mint_pda, + *self.#ctoken_config.to_account_info().key, + *self.#ctoken_rent_sponsor.to_account_info().key, + ); + + #meta_cpi_context + + let account_metas = meta_config.to_account_metas(); + + use light_compressed_account::instruction_data::traits::LightInstructionData; + let ix_data = instruction_data.data() + .map_err(|_| light_sdk::error::LightSdkError::Borsh)?; + + let mint_action_ix = anchor_lang::solana_program::instruction::Instruction { + program_id: solana_pubkey::Pubkey::new_from_array(light_token_interface::LIGHT_TOKEN_PROGRAM_ID), + accounts: account_metas, + data: ix_data, + }; + + let mut account_infos = cpi_accounts.to_account_infos(); + account_infos.push(self.#light_token_program.to_account_info()); + account_infos.push(self.#ctoken_cpi_authority.to_account_info()); + account_infos.push(self.#mint_field_ident.to_account_info()); + account_infos.push(self.#ctoken_config.to_account_info()); + account_infos.push(self.#ctoken_rent_sponsor.to_account_info()); + account_infos.push(self.#authority.to_account_info()); + account_infos.push(self.#mint_signer.to_account_info()); + account_infos.push(self.#fee_payer.to_account_info()); + + 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])?; + } + } + } +} diff --git a/sdk-libs/macros/src/rentfree/accounts/mod.rs b/sdk-libs/macros/src/rentfree/accounts/mod.rs index 42e597fb99..e603168812 100644 --- a/sdk-libs/macros/src/rentfree/accounts/mod.rs +++ b/sdk-libs/macros/src/rentfree/accounts/mod.rs @@ -5,8 +5,9 @@ //! - `LightFinalize` trait implementation for post-instruction cleanup //! - Supports rent-free PDAs, rent-free token accounts, and light mints -pub mod codegen; -pub mod parse; +mod codegen; +mod light_mint; +mod parse; use proc_macro2::TokenStream; use syn::DeriveInput; diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index ae44eb2ccf..78c2963e79 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -1,4 +1,4 @@ -//! Parsing logic for #[rentfree(...)] and #[light_mint(...)] attributes. +//! Parsing logic for #[rentfree(...)] attributes. use syn::{ parse::{Parse, ParseStream}, @@ -9,6 +9,9 @@ use syn::{ // Import shared types from anchor_seeds module pub(super) use crate::rentfree::traits::anchor_seeds::extract_account_inner_type; +// Import LightMintField and parsing from light_mint module +use super::light_mint::{parse_light_mint_attr, LightMintField}; + /// Parsed representation of a struct with rentfree and light_mint fields. pub(super) struct ParsedRentFreeStruct { pub struct_name: Ident, @@ -31,35 +34,14 @@ pub(super) struct ParsedRentFreeStruct { /// A field marked with #[rentfree(...)] pub(super) struct RentFreeField { pub ident: Ident, - pub ty: Type, + /// The inner type T from Account<'info, T> or Box> + pub inner_type: Ident, pub address_tree_info: Expr, pub output_tree: Expr, /// True if the field is Box>, false if Account pub is_boxed: bool, } -/// A field marked with #[light_mint(...)] -pub(super) struct LightMintField { - /// The field name where #[light_mint] is attached (CMint account) - pub field_ident: Ident, - /// The mint_signer field (AccountInfo that seeds the mint PDA) - pub mint_signer: Expr, - /// The authority for mint operations - pub authority: Expr, - /// Decimals for the mint - pub decimals: Expr, - /// Address tree info expression - pub address_tree_info: Expr, - /// Optional freeze authority - pub freeze_authority: Option, - /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) - pub signer_seeds: Option, - /// Rent payment epochs for decompression (default: 2) - pub rent_payment: Option, - /// Write top-up lamports for decompression (default: 0) - pub write_top_up: Option, -} - /// Instruction argument from #[instruction(...)] pub(super) struct InstructionArg { pub name: Ident, @@ -107,60 +89,10 @@ impl Parse for RentFreeArgs { } } -/// Arguments inside #[light_mint(...)] -struct LightMintArgs { - mint_signer: Option, - authority: Option, - decimals: Option, - address_tree_info: Option, - freeze_authority: Option, - signer_seeds: Option, - rent_payment: Option, - write_top_up: Option, -} - -impl Parse for LightMintArgs { - fn parse(input: ParseStream) -> syn::Result { - let mut args = LightMintArgs { - mint_signer: None, - authority: None, - decimals: None, - address_tree_info: None, - freeze_authority: None, - signer_seeds: None, - rent_payment: None, - write_top_up: None, - }; - - let content: Punctuated = Punctuated::parse_terminated(input)?; - - for arg in content { - match arg.name.to_string().as_str() { - "mint_signer" => args.mint_signer = Some(arg.value), - "authority" => args.authority = Some(arg.value), - "decimals" => args.decimals = Some(arg.value), - "address_tree_info" => args.address_tree_info = Some(arg.value), - "freeze_authority" => args.freeze_authority = Some(arg.value), - "signer_seeds" => args.signer_seeds = Some(arg.value), - "rent_payment" => args.rent_payment = Some(arg.value), - "write_top_up" => args.write_top_up = Some(arg.value), - other => { - return Err(Error::new( - arg.name.span(), - format!("unknown light_mint attribute: {}", other), - )) - } - } - } - - Ok(args) - } -} - /// Generic key = value argument parser -struct KeyValueArg { - name: Ident, - value: Expr, +pub(super) struct KeyValueArg { + pub name: Ident, + pub value: Expr, } impl Parse for KeyValueArg { @@ -240,34 +172,39 @@ pub(super) fn parse_rentfree_struct( // "compress_token_program_cpi_authority" // // Fields not matching these names will use defaults in code generation. - if field_name == "fee_payer" || field_name == "payer" || field_name == "creator" { - fee_payer_field = Some(field_ident.clone()); - } - if field_name == "compression_config" { - compression_config_field = Some(field_ident.clone()); - } - if field_name == "ctoken_compressible_config" - || field_name == "ctoken_config" - || field_name == "light_token_config_account" - { - ctoken_config_field = Some(field_ident.clone()); - } - if field_name == "ctoken_rent_sponsor" || field_name == "light_token_rent_sponsor" { - ctoken_rent_sponsor_field = Some(field_ident.clone()); - } - if field_name == "ctoken_program" || field_name == "light_token_program" { - ctoken_program_field = Some(field_ident.clone()); - } - if field_name == "ctoken_cpi_authority" - || field_name == "light_token_program_cpi_authority" - || field_name == "compress_token_program_cpi_authority" - { - ctoken_cpi_authority_field = Some(field_ident.clone()); + match field_name.as_str() { + "fee_payer" | "payer" | "creator" => { + 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 if this field already has a compression attribute let mut has_compression_attr = false; + // Check for #[light_mint(...)] attribute first (delegated to light_mint module) + if let Some(mint_field) = parse_light_mint_attr(field, &field_ident)? { + has_compression_attr = true; + light_mint_fields.push(mint_field); + } + // Look for #[rentfree] or #[rentfree(...)] attribute for attr in &field.attrs { if attr.path().is_ident("rentfree") { @@ -279,7 +216,6 @@ pub(super) fn parse_rentfree_struct( Only one is allowed per field.", )); } - has_compression_attr = true; // Handle both #[rentfree] and #[rentfree(...)] let args: RentFreeArgs = match &attr.meta { syn::Meta::Path(_) => { @@ -310,7 +246,7 @@ pub(super) fn parse_rentfree_struct( }); // Validate this is an Account type (or Box) - let (is_boxed, _inner_type) = + let (is_boxed, inner_type) = extract_account_inner_type(&field.ty).ok_or_else(|| { Error::new_spanned( &field.ty, @@ -321,7 +257,7 @@ pub(super) fn parse_rentfree_struct( rentfree_fields.push(RentFreeField { ident: field_ident.clone(), - ty: field.ty.clone(), + inner_type, address_tree_info, output_tree, is_boxed, @@ -331,50 +267,6 @@ pub(super) fn parse_rentfree_struct( // TODO(diff-pr): Add parsing for #[rentfree_token(...)] attribute for token accounts and ATAs. // Would need RentFreeTokenField struct with: field_ident, authority_seeds, mint field ref. - - // Look for #[light_mint(...)] attribute - if attr.path().is_ident("light_mint") { - // Check for duplicate compression attributes on same field - if has_compression_attr { - return Err(Error::new_spanned( - attr, - "Field already has a compression attribute (#[rentfree] or #[light_mint]). \ - Only one is allowed per field.", - )); - } - has_compression_attr = true; - - let args: LightMintArgs = attr.parse_args()?; - - // Validate required fields - let mint_signer = args - .mint_signer - .ok_or_else(|| Error::new_spanned(attr, "light_mint requires mint_signer"))?; - let authority = args - .authority - .ok_or_else(|| Error::new_spanned(attr, "light_mint requires authority"))?; - let decimals = args - .decimals - .ok_or_else(|| Error::new_spanned(attr, "light_mint requires decimals"))?; - - // address_tree_info defaults to params.create_accounts_proof.address_tree_info - let address_tree_info = args.address_tree_info.unwrap_or_else(|| { - syn::parse_quote!(params.create_accounts_proof.address_tree_info) - }); - - light_mint_fields.push(LightMintField { - field_ident: field_ident.clone(), - mint_signer, - authority, - decimals, - address_tree_info, - freeze_authority: args.freeze_authority, - signer_seeds: args.signer_seeds, - rent_payment: args.rent_payment, - write_top_up: args.write_top_up, - }); - break; - } } } diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 83d477c58a..268b9d108a 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -936,9 +936,7 @@ fn generate_from_extracted_seeds( let content = module.content.as_mut().unwrap(); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { - super::seed_providers::generate_ctoken_account_variant_enum( - token_seed_specs, - )? + super::seed_providers::generate_ctoken_account_variant_enum(token_seed_specs)? } else { crate::rentfree::traits::utils::generate_empty_ctoken_enum() } @@ -965,21 +963,17 @@ fn generate_from_extracted_seeds( .iter() .map(|spec| { let ctx_fields = extract_ctx_seed_fields(&spec.seeds); - super::variant_enum::PdaCtxSeedInfo::new( - spec.variant.clone(), - ctx_fields, - ) + super::variant_enum::PdaCtxSeedInfo::new(spec.variant.clone(), 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, - )?; + let enum_and_traits = super::variant_enum::compressed_account_variant_with_ctx_seeds( + &account_type_refs, + &pda_ctx_seeds, + )?; let seed_params_struct = quote! { #[derive(anchor_lang::AnchorSerialize, anchor_lang::AnchorDeserialize, Clone, Debug, Default)] @@ -1312,9 +1306,7 @@ fn generate_from_extracted_seeds( if let Some(ref seeds) = token_seeds { if !seeds.is_empty() { let impl_code = - super::seed_providers::generate_ctoken_seed_provider_implementation( - seeds, - )?; + super::seed_providers::generate_ctoken_seed_provider_implementation(seeds)?; let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; content.1.push(Item::Impl(ctoken_impl)); } @@ -1350,10 +1342,10 @@ fn generate_from_extracted_seeds( /// pub mod my_program { /// pub mod instruction_accounts; // Macro reads this file! /// pub mod state; -/// +/// /// use instruction_accounts::*; /// use state::*; -/// +/// /// // No #[light_instruction] needed - auto-wrapped! /// pub fn create_user(ctx: Context, params: Params) -> Result<()> { /// // Your business logic @@ -1448,8 +1440,10 @@ fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> s #[inline(never)] pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { - use crate::rentfree::traits::anchor_seeds::{extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec}; use super::crate_context::CrateContext; + use crate::rentfree::traits::anchor_seeds::{ + extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec, + }; if module.content.is_none() { return Err(macro_error!(&module, "Module must have a body")); diff --git a/sdk-libs/macros/src/rentfree/program/seed_providers.rs b/sdk-libs/macros/src/rentfree/program/seed_providers.rs index 298c0e761e..bc91976d82 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_providers.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_providers.rs @@ -5,6 +5,19 @@ use quote::{format_ident, quote}; use syn::{Ident, Result}; use super::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; +use crate::rentfree::traits::utils::is_pubkey_type; + +/// Helper to add a Pubkey parameter and its .as_ref() expression. +/// This is the default fallback for ctx.accounts.field and similar patterns. +#[inline] +fn push_pubkey_param( + field_name: &syn::Ident, + parameters: &mut Vec, + expressions: &mut Vec, +) { + parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); + expressions.push(quote! { #field_name.as_ref() }); +} /// Extract ctx.* field names from seed elements (both token seeds and authority seeds) fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { @@ -249,6 +262,51 @@ pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Re }) } +/// Convert a SeedElement to a TokenStream representing the seed reference expression. +/// Used by generate_ctoken_seed_provider_implementation for both token and authority seeds. +fn seed_element_to_ref_expr(seed: &SeedElement) -> TokenStream { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes() } + } + SeedElement::Expression(expr) => { + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; + } + } + + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if ident_str + .chars() + .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) + { + if ident_str == "LIGHT_CPI_SIGNER" { + return quote! { crate::#ident.cpi_signer.as_ref() }; + } else { + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; + } + } + } + } + + // Handle ctx.accounts.field or ctx.field - use the destructured field directly + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; + } + + // Fallback + quote! { (#expr).as_ref() } + } + } +} + /// Phase 8: Generate TokenSeedProvider impl that uses self.field instead of ctx.accounts.field pub fn generate_ctoken_seed_provider_implementation( token_seeds: &[TokenSeedSpec], @@ -269,45 +327,8 @@ pub fn generate_ctoken_seed_provider_implementation( }; // Build seed refs for get_seeds - use self.field directly for ctx.* seeds - let token_seed_refs: Vec = spec.seeds.iter().map(|seed| { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - quote! { #value.as_bytes() } - } - SeedElement::Expression(expr) => { - // Handle byte string literals - if let syn::Expr::Lit(lit_expr) = &**expr { - if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { - let bytes = byte_str.value(); - return quote! { &[#(#bytes),*] }; - } - } - - // Handle uppercase constants - if let syn::Expr::Path(path_expr) = &**expr { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { - if ident_str == "LIGHT_CPI_SIGNER" { - return quote! { crate::#ident.cpi_signer.as_ref() }; - } else { - return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; - } - } - } - } - - // Handle ctx.accounts.field or ctx.field - use the destructured field directly - if let Some(field_name) = extract_ctx_field_name(expr) { - return quote! { #field_name.as_ref() }; - } - - // Fallback - quote! { (#expr).as_ref() } - } - } - }).collect(); + let token_seed_refs: Vec = + spec.seeds.iter().map(seed_element_to_ref_expr).collect(); let get_seeds_arm = quote! { #pattern => { @@ -323,45 +344,8 @@ pub fn generate_ctoken_seed_provider_implementation( // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let auth_seed_refs: Vec = authority_seeds.iter().map(|seed| { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - quote! { #value.as_bytes() } - } - SeedElement::Expression(expr) => { - // Handle byte string literals - if let syn::Expr::Lit(lit_expr) = &**expr { - if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { - let bytes = byte_str.value(); - return quote! { &[#(#bytes),*] }; - } - } - - // Handle uppercase constants - if let syn::Expr::Path(path_expr) = &**expr { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) { - if ident_str == "LIGHT_CPI_SIGNER" { - return quote! { crate::#ident.cpi_signer.as_ref() }; - } else { - return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; - } - } - } - } - - // Handle ctx.accounts.field or ctx.field - use the destructured field directly - if let Some(field_name) = extract_ctx_field_name(expr) { - return quote! { #field_name.as_ref() }; - } - - // Fallback - quote! { (#expr).as_ref() } - } - } - }).collect(); + let auth_seed_refs: Vec = + authority_seeds.iter().map(seed_element_to_ref_expr).collect(); let authority_arm = quote! { #pattern => { @@ -441,6 +425,25 @@ fn extract_ctx_field_name(expr: &syn::Expr) -> Option { None } +/// Generate the body of a seed function that computes a PDA address. +/// `program_id_expr` should be either `&crate::ID` or a variable like `_program_id`. +fn generate_seed_fn_body( + seed_count: usize, + seed_expressions: &[TokenStream], + program_id_expr: TokenStream, +) -> TokenStream { + quote! { + let mut seed_values = Vec::with_capacity(#seed_count + 1); + #( + seed_values.push((#seed_expressions).to_vec()); + )* + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, #program_id_expr); + seed_values.push(vec![bump]); + (seed_values, pda) + } +} + #[inline(never)] pub fn generate_client_seed_functions( _account_types: &[Ident], @@ -460,16 +463,10 @@ pub fn generate_client_seed_functions( analyze_seed_spec_for_client(spec, instruction_data)?; let seed_count = seed_expressions.len(); + let fn_body = generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { - let mut seed_values = Vec::with_capacity(#seed_count + 1); - #( - seed_values.push((#seed_expressions).to_vec()); - )* - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(vec![bump]); - (seed_values, pda) + #fn_body } }; functions.push(function); @@ -487,16 +484,10 @@ pub fn generate_client_seed_functions( analyze_seed_spec_for_client(spec, instruction_data)?; let seed_count = seed_expressions.len(); + let fn_body = generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { - let mut seed_values = Vec::with_capacity(#seed_count + 1); - #( - seed_values.push((#seed_expressions).to_vec()); - )* - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(vec![bump]); - (seed_values, pda) + #fn_body } }; functions.push(function); @@ -526,30 +517,12 @@ pub fn generate_client_seed_functions( let (fn_params, fn_body) = if auth_parameters.is_empty() { ( quote! { _program_id: &solana_pubkey::Pubkey }, - quote! { - let mut seed_values = Vec::with_capacity(#auth_seed_count + 1); - #( - seed_values.push((#auth_seed_expressions).to_vec()); - )* - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, _program_id); - seed_values.push(vec![bump]); - (seed_values, pda) - }, + generate_seed_fn_body(auth_seed_count, &auth_seed_expressions, quote! { _program_id }), ) } else { ( quote! { #(#auth_parameters),* }, - quote! { - let mut seed_values = Vec::with_capacity(#auth_seed_count + 1); - #( - seed_values.push((#auth_seed_expressions).to_vec()); - )* - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, &crate::ID); - seed_values.push(vec![bump]); - (seed_values, pda) - }, + generate_seed_fn_body(auth_seed_count, &auth_seed_expressions, quote! { &crate::ID }), ) }; let authority_function = quote! { @@ -590,76 +563,34 @@ fn analyze_seed_spec_for_client( match &**expr { syn::Expr::Field(field_expr) => { if let syn::Member::Named(field_name) = &field_expr.member { - match &*field_expr.base { - syn::Expr::Field(nested_field) => { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(_segment) = path.path.segments.first() { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions - .push(quote! { #field_name.as_ref() }); - } else { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions - .push(quote! { #field_name.as_ref() }); - } + // Check for data.field pattern which uses instruction_data types + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + if let Some(data_spec) = instruction_data + .iter() + .find(|d| d.field_name == *field_name) + { + let param_type = &data_spec.field_type; + let param_with_ref = if is_pubkey_type(param_type) { + quote! { #field_name: &#param_type } } else { - parameters.push( - quote! { #field_name: &solana_pubkey::Pubkey }, - ); - expressions.push(quote! { #field_name.as_ref() }); - } - } else { - parameters.push( - quote! { #field_name: &solana_pubkey::Pubkey }, - ); + quote! { #field_name: #param_type } + }; + parameters.push(param_with_ref); expressions.push(quote! { #field_name.as_ref() }); - } - } else { - parameters - .push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name.as_ref() }); - } - } - syn::Expr::Path(path) => { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - if let Some(data_spec) = instruction_data - .iter() - .find(|d| d.field_name == *field_name) - { - let param_type = &data_spec.field_type; - let param_with_ref = if is_pubkey_type(param_type) { - quote! { #field_name: &#param_type } - } else { - quote! { #field_name: #param_type } - }; - parameters.push(param_with_ref); - expressions.push(quote! { #field_name.as_ref() }); - } else { - return Err(syn::Error::new_spanned( - field_name, - format!("data.{} used in seeds but no type specified", field_name), - )); - } + continue; } else { - parameters.push( - quote! { #field_name: &solana_pubkey::Pubkey }, - ); - expressions.push(quote! { #field_name.as_ref() }); + return Err(syn::Error::new_spanned( + field_name, + format!("data.{} used in seeds but no type specified", field_name), + )); } - } else { - parameters - .push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name.as_ref() }); } } - _ => { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name.as_ref() }); - } } + // Default: ctx.accounts.field, ctx.field, or other field patterns + push_pubkey_param(field_name, &mut parameters, &mut expressions); } } syn::Expr::MethodCall(method_call) => { @@ -949,16 +880,3 @@ fn camel_to_snake_case(s: &str) -> String { } result } - -fn is_pubkey_type(ty: &syn::Type) -> bool { - if let syn::Type::Path(type_path) = ty { - if let Some(segment) = type_path.path.segments.last() { - let type_name = segment.ident.to_string(); - type_name == "Pubkey" || type_name.contains("Pubkey") - } else { - false - } - } else { - false - } -} From 01dc5bcc2fd972ecc5fc7a93755c246c0637fcda Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 03:07:18 +0000 Subject: [PATCH 5/9] simplify pda --- Cargo.lock | 1 + Cargo.toml | 1 + sdk-libs/macros/Cargo.toml | 1 + .../src/rentfree/accounts/light_mint.rs | 140 ++++++++---------- sdk-libs/macros/src/rentfree/accounts/mod.rs | 4 +- .../macros/src/rentfree/accounts/parse.rs | 119 ++++++--------- .../rentfree/accounts/{codegen.rs => pda.rs} | 0 sdk-libs/macros/src/rentfree/traits/traits.rs | 69 +++++---- 8 files changed, 150 insertions(+), 185 deletions(-) rename sdk-libs/macros/src/rentfree/accounts/{codegen.rs => pda.rs} (100%) diff --git a/Cargo.lock b/Cargo.lock index ddcc2a7f08..e356f741f3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4027,6 +4027,7 @@ name = "light-sdk-macros" version = "0.17.1" dependencies = [ "borsh 0.10.4", + "darling", "light-account-checks", "light-compressed-account", "light-hasher", diff --git a/Cargo.toml b/Cargo.toml index a498d7a733..32a0953ac4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -143,6 +143,7 @@ serde_json = "1.0" proc-macro2 = "1.0" quote = "1.0" syn = { version = "2.0", features = ["visit-mut", "full"] } +darling = "0.21" # Async ecosystem futures = "0.3.31" diff --git a/sdk-libs/macros/Cargo.toml b/sdk-libs/macros/Cargo.toml index 57e64941c4..dba703dfe0 100644 --- a/sdk-libs/macros/Cargo.toml +++ b/sdk-libs/macros/Cargo.toml @@ -14,6 +14,7 @@ anchor-discriminator = [] proc-macro2 = { workspace = true } quote = { workspace = true } syn = { workspace = true } +darling = { workspace = true } solana-pubkey = { workspace = true, features = ["curve25519", "sha2"] } light-hasher = { workspace = true, features = ["sha256"] } diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index f2ded25a22..89128e30fa 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -1,23 +1,36 @@ //! Light mint parsing and code generation. //! //! This module handles: -//! - Parsing of #[light_mint(...)] attributes +//! - Parsing of #[light_mint(...)] attributes using darling //! - Code generation for mint_action CPI invocations +use darling::FromMeta; use proc_macro2::TokenStream; use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Expr, Ident, Token, -}; - -use super::parse::KeyValueArg; +use syn::{Expr, Ident}; // ============================================================================ -// Parsing +// 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) @@ -40,54 +53,33 @@ pub(super) struct LightMintField { pub write_top_up: Option, } -/// Arguments inside #[light_mint(...)] +/// 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 +#[derive(FromMeta)] struct LightMintArgs { - mint_signer: Option, - authority: Option, - decimals: Option, - address_tree_info: Option, - freeze_authority: Option, - signer_seeds: Option, - rent_payment: Option, - write_top_up: Option, -} - -impl Parse for LightMintArgs { - fn parse(input: ParseStream) -> syn::Result { - let mut args = LightMintArgs { - mint_signer: None, - authority: None, - decimals: None, - address_tree_info: None, - freeze_authority: None, - signer_seeds: None, - rent_payment: None, - write_top_up: None, - }; - - let content: Punctuated = Punctuated::parse_terminated(input)?; - - for arg in content { - match arg.name.to_string().as_str() { - "mint_signer" => args.mint_signer = Some(arg.value), - "authority" => args.authority = Some(arg.value), - "decimals" => args.decimals = Some(arg.value), - "address_tree_info" => args.address_tree_info = Some(arg.value), - "freeze_authority" => args.freeze_authority = Some(arg.value), - "signer_seeds" => args.signer_seeds = Some(arg.value), - "rent_payment" => args.rent_payment = Some(arg.value), - "write_top_up" => args.write_top_up = Some(arg.value), - other => { - return Err(syn::Error::new( - arg.name.span(), - format!("unknown light_mint attribute: {}", other), - )) - } - } - } - - Ok(args) - } + /// The mint_signer field (AccountInfo that seeds the mint PDA) - REQUIRED + mint_signer: MetaExpr, + /// The authority for mint operations - REQUIRED + authority: MetaExpr, + /// Decimals for the mint - REQUIRED + decimals: MetaExpr, + /// Address tree info expression (defaults to params.create_accounts_proof.address_tree_info) + #[darling(default)] + address_tree_info: Option, + /// Optional freeze authority + #[darling(default)] + freeze_authority: Option, + /// Signer seeds for the mint_signer PDA (required if mint_signer is a PDA) + #[darling(default)] + signer_seeds: Option, + /// Rent payment epochs for decompression + #[darling(default)] + rent_payment: Option, + /// Write top-up lamports for decompression + #[darling(default)] + write_top_up: Option, } /// Parse #[light_mint(...)] attribute from a field. @@ -98,34 +90,26 @@ pub(super) fn parse_light_mint_attr( ) -> Result, syn::Error> { for attr in &field.attrs { if attr.path().is_ident("light_mint") { - let args: LightMintArgs = attr.parse_args()?; - - // Validate required fields - let mint_signer = args - .mint_signer - .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires mint_signer"))?; - let authority = args - .authority - .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires authority"))?; - let decimals = args - .decimals - .ok_or_else(|| syn::Error::new_spanned(attr, "light_mint requires decimals"))?; + // Use darling to parse the attribute arguments + let args = LightMintArgs::from_meta(&attr.meta) + .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; // address_tree_info defaults to params.create_accounts_proof.address_tree_info - let address_tree_info = args.address_tree_info.unwrap_or_else(|| { - syn::parse_quote!(params.create_accounts_proof.address_tree_info) - }); + let address_tree_info = args + .address_tree_info + .map(Into::into) + .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.address_tree_info)); return Ok(Some(LightMintField { field_ident: field_ident.clone(), - mint_signer, - authority, - decimals, + mint_signer: args.mint_signer.into(), + authority: args.authority.into(), + decimals: args.decimals.into(), address_tree_info, - freeze_authority: args.freeze_authority, - signer_seeds: args.signer_seeds, - rent_payment: args.rent_payment, - write_top_up: args.write_top_up, + freeze_authority: args.freeze_authority.map(Into::into), + 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), })); } } diff --git a/sdk-libs/macros/src/rentfree/accounts/mod.rs b/sdk-libs/macros/src/rentfree/accounts/mod.rs index e603168812..5fe9275770 100644 --- a/sdk-libs/macros/src/rentfree/accounts/mod.rs +++ b/sdk-libs/macros/src/rentfree/accounts/mod.rs @@ -5,14 +5,14 @@ //! - `LightFinalize` trait implementation for post-instruction cleanup //! - Supports rent-free PDAs, rent-free token accounts, and light mints -mod codegen; mod light_mint; mod parse; +mod pda; use proc_macro2::TokenStream; use syn::DeriveInput; pub fn derive_rentfree(input: DeriveInput) -> Result { let parsed = parse::parse_rentfree_struct(&input)?; - codegen::generate_rentfree_impl(&parsed) + pda::generate_rentfree_impl(&parsed) } diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index 78c2963e79..f8ab0834f8 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -1,5 +1,6 @@ -//! Parsing logic for #[rentfree(...)] attributes. +//! Parsing logic for #[rentfree(...)] attributes using darling. +use darling::FromMeta; use syn::{ parse::{Parse, ParseStream}, punctuated::Punctuated, @@ -12,6 +13,27 @@ pub(super) use crate::rentfree::traits::anchor_seeds::extract_account_inner_type // Import LightMintField and parsing from light_mint module use super::light_mint::{parse_light_mint_attr, LightMintField}; +// ============================================================================ +// 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, @@ -57,51 +79,21 @@ impl Parse for InstructionArg { } } -/// Arguments inside #[rentfree(...)] -struct RentFreeArgs { - address_tree_info: Option, - output_tree: Option, -} - -impl Parse for RentFreeArgs { - fn parse(input: ParseStream) -> syn::Result { - let mut args = RentFreeArgs { - address_tree_info: None, - output_tree: None, - }; - - let content: Punctuated = Punctuated::parse_terminated(input)?; - - for arg in content { - match arg.name.to_string().as_str() { - "address_tree_info" => args.address_tree_info = Some(arg.value), - "output_tree" => args.output_tree = Some(arg.value), - other => { - return Err(Error::new( - arg.name.span(), - format!("unknown rentfree attribute: {}", other), - )) - } - } - } - - Ok(args) - } -} - -/// Generic key = value argument parser -pub(super) struct KeyValueArg { - pub name: Ident, - pub value: Expr, +fn rentfree_args_default() -> darling::Result { + Ok(RentFreeArgs::default()) } -impl Parse for KeyValueArg { - fn parse(input: ParseStream) -> syn::Result { - let name: Ident = input.parse()?; - input.parse::()?; - let value: Expr = input.parse()?; - Ok(KeyValueArg { name, value }) - } +/// Arguments inside #[rentfree(...)] parsed by darling. +/// +/// Supports both `#[rentfree]` (word format) and `#[rentfree(...)]` (list format). +/// All fields default to None if not specified. +#[derive(FromMeta, Default)] +#[darling(default, from_word = rentfree_args_default)] +struct RentFreeArgs { + /// Address tree info expression + address_tree_info: Option, + /// Output tree index expression + output_tree: Option, } /// Parse #[instruction(...)] attribute from struct. @@ -216,34 +208,21 @@ pub(super) fn parse_rentfree_struct( Only one is allowed per field.", )); } - // Handle both #[rentfree] and #[rentfree(...)] - let args: RentFreeArgs = match &attr.meta { - syn::Meta::Path(_) => { - // No parentheses: #[rentfree] - RentFreeArgs { - address_tree_info: None, - output_tree: None, - } - } - syn::Meta::List(_) => { - // Has parentheses: #[rentfree(...)] - attr.parse_args()? - } - syn::Meta::NameValue(_) => { - return Err(Error::new_spanned( - attr, - "expected #[rentfree] or #[rentfree(...)]", - )); - } - }; + + // Use darling to parse the attribute arguments + // Handles both #[rentfree] and #[rentfree(...)] + let args = RentFreeArgs::from_meta(&attr.meta) + .map_err(|e| Error::new_spanned(attr, e.to_string()))?; // Use defaults if not specified - let address_tree_info = args.address_tree_info.unwrap_or_else(|| { - syn::parse_quote!(params.create_accounts_proof.address_tree_info) - }); - let output_tree = args.output_tree.unwrap_or_else(|| { - syn::parse_quote!(params.create_accounts_proof.output_state_tree_index) - }); + let address_tree_info = args + .address_tree_info + .map(Into::into) + .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.address_tree_info)); + let output_tree = args + .output_tree + .map(Into::into) + .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.output_state_tree_index)); // Validate this is an Account type (or Box) let (is_boxed, inner_type) = diff --git a/sdk-libs/macros/src/rentfree/accounts/codegen.rs b/sdk-libs/macros/src/rentfree/accounts/pda.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/accounts/codegen.rs rename to sdk-libs/macros/src/rentfree/accounts/pda.rs diff --git a/sdk-libs/macros/src/rentfree/traits/traits.rs b/sdk-libs/macros/src/rentfree/traits/traits.rs index 4118b5b17e..27b3dfa272 100644 --- a/sdk-libs/macros/src/rentfree/traits/traits.rs +++ b/sdk-libs/macros/src/rentfree/traits/traits.rs @@ -1,40 +1,42 @@ //! Trait derivation for compressible accounts. +use darling::FromMeta; use proc_macro2::TokenStream; use quote::quote; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - DeriveInput, Expr, Field, Ident, ItemStruct, Result, Token, -}; +use syn::{punctuated::Punctuated, DeriveInput, Expr, Field, Ident, ItemStruct, Result, Token}; use super::utils::{ extract_fields_from_derive_input, extract_fields_from_item_struct, is_copy_type, }; -struct CompressAsFields { - fields: Punctuated, -} - +/// A single field override in #[compress_as(field = expr)] struct CompressAsField { name: Ident, value: Expr, } -impl Parse for CompressAsField { - fn parse(input: ParseStream) -> Result { - let name: Ident = input.parse()?; - input.parse::()?; - let value: Expr = input.parse()?; - Ok(CompressAsField { name, value }) - } +/// Collection of field overrides parsed from #[compress_as(...)] +/// Uses darling's FromMeta to collect arbitrary name=value pairs. +#[derive(Default)] +struct CompressAsFields { + fields: Vec, } -impl Parse for CompressAsFields { - fn parse(input: ParseStream) -> Result { - Ok(CompressAsFields { - fields: Punctuated::parse_terminated(input)?, - }) +impl FromMeta for CompressAsFields { + fn from_list(items: &[darling::ast::NestedMeta]) -> darling::Result { + items + .iter() + .map(|item| match item { + darling::ast::NestedMeta::Meta(syn::Meta::NameValue(nv)) => { + let name = nv.path.get_ident().cloned().ok_or_else(|| { + darling::Error::custom("expected field identifier").with_span(&nv.path) + })?; + Ok(CompressAsField { name, value: nv.value.clone() }) + } + other => Err(darling::Error::custom("expected field = expr").with_span(other)), + }) + .collect::>>() + .map(|fields| CompressAsFields { fields }) } } @@ -105,18 +107,11 @@ fn generate_compress_as_field_assignments( continue; } - let has_override = compress_as_fields + let override_field = compress_as_fields .as_ref() - .is_some_and(|cas| cas.fields.iter().any(|f| &f.name == field_name)); - - if has_override { - let override_value = compress_as_fields - .as_ref() - .unwrap() - .fields - .iter() - .find(|f| &f.name == field_name) - .unwrap(); + .and_then(|cas| cas.fields.iter().find(|f| &f.name == field_name)); + + if let Some(override_value) = override_field { let value = &override_value.value; field_assignments.push(quote! { #field_name: #value, @@ -213,7 +208,9 @@ pub fn derive_compress_as(input: ItemStruct) -> Result { .find(|attr| attr.path().is_ident("compress_as")); let compress_as_fields = if let Some(attr) = compress_as_attr { - Some(attr.parse_args::()?) + let parsed = CompressAsFields::from_meta(&attr.meta) + .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; + Some(parsed) } else { None }; @@ -234,14 +231,16 @@ pub fn derive_compressible(input: DeriveInput) -> Result { let struct_name = &input.ident; let fields = extract_fields_from_derive_input(&input)?; - // Extract compress_as attribute + // Extract compress_as attribute using darling let compress_as_attr = input .attrs .iter() .find(|attr| attr.path().is_ident("compress_as")); let compress_as_fields = if let Some(attr) = compress_as_attr { - Some(attr.parse_args::()?) + let parsed = CompressAsFields::from_meta(&attr.meta) + .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; + Some(parsed) } else { None }; From 7beeb5be6ea67920b29be0d31bd5825a1c324958 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 03:17:23 +0000 Subject: [PATCH 6/9] split up instructions --- .../macros/src/rentfree/program/compress.rs | 253 ++++ .../macros/src/rentfree/program/decompress.rs | 423 +++++++ .../src/rentfree/program/instructions.rs | 1107 +---------------- sdk-libs/macros/src/rentfree/program/mod.rs | 3 + .../macros/src/rentfree/program/parsing.rs | 504 ++++++++ 5 files changed, 1214 insertions(+), 1076 deletions(-) create mode 100644 sdk-libs/macros/src/rentfree/program/compress.rs create mode 100644 sdk-libs/macros/src/rentfree/program/decompress.rs create mode 100644 sdk-libs/macros/src/rentfree/program/parsing.rs diff --git a/sdk-libs/macros/src/rentfree/program/compress.rs b/sdk-libs/macros/src/rentfree/program/compress.rs new file mode 100644 index 0000000000..85c9bca4eb --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/compress.rs @@ -0,0 +1,253 @@ +//! Compress code generation. + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{Ident, Result}; + +use super::parsing::InstructionVariant; + +// ============================================================================= +// COMPRESS CONTEXT IMPL +// ============================================================================= + +pub fn generate_compress_context_impl( + _variant: InstructionVariant, + account_types: Vec, +) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); + + let compress_arms: Vec<_> = account_types.iter().map(|name| { + quote! { + d if d == #name::LIGHT_DISCRIMINATOR => { + drop(data); + let data_borrow = account_info.try_borrow_data().map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + drop(data_borrow); + + let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( + program_id, + account_info, + &mut account_data, + meta, + cpi_accounts, + &compression_config.address_space, + )?; + // Lamport transfers are handled by close() in process_compress_pda_accounts_idempotent + // All lamports go to rent_sponsor for simplicity + Ok(Some(compressed_info)) + } + } + }).collect(); + + Ok(syn::parse_quote! { + mod __compress_context_impl { + use super::*; + use light_sdk::LightDiscriminator; + use light_sdk::compressible::HasCompressionInfo; + + impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { + fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &*self.fee_payer + } + + fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.config + } + + fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.rent_sponsor + } + + fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { + &self.compression_authority + } + + fn compress_pda_account( + &self, + account_info: &solana_account_info::AccountInfo<#lifetime>, + meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, + cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, + compression_config: &light_sdk::compressible::CompressibleConfig, + program_id: &solana_pubkey::Pubkey, + ) -> std::result::Result, solana_program_error::ProgramError> { + let data = account_info.try_borrow_data().map_err(|e| { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + })?; + let discriminator = &data[0..8]; + + match discriminator { + #(#compress_arms)* + _ => { + let err: anchor_lang::error::Error = anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + Err(solana_program_error::ProgramError::Custom(code)) + } + } + } + } + } + }) +} + +// ============================================================================= +// COMPRESS PROCESSOR +// ============================================================================= + +pub fn generate_process_compress_accounts_idempotent( + _variant: InstructionVariant, +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_compress_accounts_idempotent<'info>( + accounts: &CompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) +} + +// ============================================================================= +// COMPRESS INSTRUCTION ENTRYPOINT +// ============================================================================= + +pub fn generate_compress_instruction_entrypoint( + _variant: InstructionVariant, +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + #[allow(clippy::too_many_arguments)] + pub fn compress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_compress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + compressed_accounts, + system_accounts_offset, + ) + } + }) +} + +// ============================================================================= +// COMPRESS ACCOUNTS STRUCT +// ============================================================================= + +pub fn generate_compress_accounts_struct(variant: InstructionVariant) -> Result { + match variant { + InstructionVariant::PdaOnly => unreachable!(), + InstructionVariant::TokenOnly => unreachable!(), + InstructionVariant::Mixed => Ok(syn::parse_quote! { + #[derive(Accounts)] + pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub compression_authority: AccountInfo<'info>, + } + }), + } +} + +// ============================================================================= +// VALIDATION AND ERROR CODES +// ============================================================================= + +#[inline(never)] +pub fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result { + let size_checks: Vec<_> = account_types.iter().map(|account_type| { + quote! { + const _: () = { + const COMPRESSED_SIZE: usize = 8 + <#account_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; + if COMPRESSED_SIZE > 800 { + panic!(concat!( + "Compressed account '", stringify!(#account_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" + )); + } + }; + } + }).collect(); + + Ok(quote! { #(#size_checks)* }) +} + +#[inline(never)] +pub fn generate_error_codes(variant: InstructionVariant) -> Result { + let base_errors = quote! { + #[msg("Rent sponsor mismatch")] + InvalidRentSponsor, + #[msg("Missing seed account")] + MissingSeedAccount, + #[msg("Seed value does not match account data")] + SeedMismatch, + }; + + let variant_specific_errors = match variant { + InstructionVariant::PdaOnly => unreachable!(), + InstructionVariant::TokenOnly => unreachable!(), + InstructionVariant::Mixed => quote! { + #[msg("Not implemented")] + CTokenDecompressionNotImplemented, + #[msg("Not implemented")] + PdaDecompressionNotImplemented, + #[msg("Not implemented")] + TokenCompressionNotImplemented, + #[msg("Not implemented")] + PdaCompressionNotImplemented, + }, + }; + + Ok(quote! { + #[error_code] + pub enum RentFreeInstructionError { + #base_errors + #variant_specific_errors + } + }) +} diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs new file mode 100644 index 0000000000..e2568f2de5 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -0,0 +1,423 @@ +//! Decompress code generation. + +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Ident, Result}; + +use super::parsing::{InstructionDataSpec, InstructionVariant, SeedElement, TokenSeedSpec}; +use super::variant_enum::PdaCtxSeedInfo; + +// ============================================================================= +// DECOMPRESS CONTEXT IMPL +// ============================================================================= + +pub fn generate_decompress_context_impl( + _variant: InstructionVariant, + pda_ctx_seeds: Vec, + token_variant_ident: Ident, +) -> Result { + let lifetime: syn::Lifetime = syn::parse_quote!('info); + + let trait_impl = + crate::rentfree::traits::decompress_context::generate_decompress_context_trait_impl( + pda_ctx_seeds, + token_variant_ident, + lifetime, + )?; + + Ok(syn::parse_quote! { + mod __decompress_context_impl { + use super::*; + + #trait_impl + } + }) +} + +// ============================================================================= +// DECOMPRESS PROCESSOR +// ============================================================================= + +pub fn generate_process_decompress_accounts_idempotent( + _variant: InstructionVariant, + _instruction_data: &[InstructionDataSpec], +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn process_decompress_accounts_idempotent<'info>( + accounts: &DecompressAccountsIdempotent<'info>, + remaining_accounts: &[solana_account_info::AccountInfo<'info>], + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + light_sdk::compressible::process_decompress_accounts_idempotent( + accounts, + remaining_accounts, + compressed_accounts, + proof, + system_accounts_offset, + LIGHT_CPI_SIGNER, + &crate::ID, + std::option::Option::None::<&()>, + ) + .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) + } + }) +} + +// ============================================================================= +// DECOMPRESS INSTRUCTION ENTRYPOINT +// ============================================================================= + +pub fn generate_decompress_instruction_entrypoint( + _variant: InstructionVariant, + _instruction_data: &[InstructionDataSpec], +) -> Result { + Ok(syn::parse_quote! { + #[inline(never)] + pub fn decompress_accounts_idempotent<'info>( + ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, + proof: light_sdk::instruction::ValidityProof, + compressed_accounts: Vec, + system_accounts_offset: u8, + ) -> Result<()> { + __processor_functions::process_decompress_accounts_idempotent( + &ctx.accounts, + &ctx.remaining_accounts, + proof, + compressed_accounts, + system_accounts_offset, + ) + } + }) +} + +// ============================================================================= +// DECOMPRESS ACCOUNTS STRUCT +// ============================================================================= + +#[inline(never)] +pub fn generate_decompress_accounts_struct( + required_accounts: &[String], + variant: InstructionVariant, +) -> Result { + let mut account_fields = vec![ + quote! { + #[account(mut)] + pub fee_payer: Signer<'info> + }, + quote! { + /// CHECK: Checked by SDK + pub config: AccountInfo<'info> + }, + ]; + + match variant { + InstructionVariant::PdaOnly => { + unreachable!() + } + InstructionVariant::TokenOnly => { + unreachable!() + } + InstructionVariant::Mixed => { + account_fields.extend(vec![ + quote! { + /// CHECK: anyone can pay + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info> + }, + quote! { + /// CHECK: optional - only needed if decompressing tokens + #[account(mut)] + pub ctoken_rent_sponsor: Option> + }, + ]); + } + } + + match variant { + InstructionVariant::TokenOnly => { + unreachable!() + } + InstructionVariant::Mixed => { + account_fields.extend(vec![ + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] + pub light_token_program: Option> + }, + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] + pub ctoken_cpi_authority: Option> + }, + quote! { + /// CHECK: Checked by SDK + pub ctoken_config: Option> + }, + ]); + } + InstructionVariant::PdaOnly => { + unreachable!() + } + } + + let standard_fields = [ + "fee_payer", + "rent_sponsor", + "ctoken_rent_sponsor", + "config", + "light_token_program", + "ctoken_cpi_authority", + "ctoken_config", + ]; + + for account_name in required_accounts { + if !standard_fields.contains(&account_name.as_str()) { + let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); + // Mark seed accounts as writable to support CPI calls that may need them writable + account_fields.push(quote! { + /// CHECK: optional seed account - may be used in CPIs + #[account(mut)] + pub #account_ident: Option> + }); + } + } + + let struct_def = quote! { + #[derive(Accounts)] + pub struct DecompressAccountsIdempotent<'info> { + #(#account_fields,)* + } + }; + + syn::parse2(struct_def) +} + +// ============================================================================= +// PDA SEED DERIVATION +// ============================================================================= + +/// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. +/// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) +#[inline(never)] +pub fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( + spec: &TokenSeedSpec, + _instruction_data: &[InstructionDataSpec], + ctx_seed_fields: &[syn::Ident], +) -> Result { + let mut bindings: Vec = Vec::new(); + let mut seed_refs = Vec::new(); + + // Convert ctx_seed_fields to a set for quick lookup + let ctx_field_names: std::collections::HashSet = + ctx_seed_fields.iter().map(|f| f.to_string()).collect(); + + // Recursively rewrite expressions: + // - `data.` -> `self.` (from unpacked compressed account data) + // - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + // - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) + fn map_pda_expr_to_ctx_seeds( + expr: &syn::Expr, + ctx_field_names: &std::collections::HashSet, + ) -> syn::Expr { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Handle nested field access: ctx.accounts.field_name -> ctx_seeds.field_name + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "ctx" { + // ctx.accounts.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } + } + } + } + // Handle direct field access + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + // data.field -> self.field (from unpacked compressed account data) + return syn::parse_quote! { self.#field_name }; + } else if segment.ident == "ctx" { + let field_str = field_name.to_string(); + if ctx_field_names.contains(&field_str) { + // ctx.field -> ctx_seeds.field (direct Pubkey) + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } + } + } + expr.clone() + } + syn::Expr::MethodCall(method_call) => { + let mut new_method_call = method_call.clone(); + new_method_call.receiver = Box::new(map_pda_expr_to_ctx_seeds( + &method_call.receiver, + ctx_field_names, + )); + new_method_call.args = method_call + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::MethodCall(new_method_call) + } + syn::Expr::Call(call_expr) => { + let mut new_call_expr = call_expr.clone(); + new_call_expr.args = call_expr + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::Call(new_call_expr) + } + syn::Expr::Reference(ref_expr) => { + let mut new_ref_expr = ref_expr.clone(); + new_ref_expr.expr = + Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); + syn::Expr::Reference(new_ref_expr) + } + _ => expr.clone(), + } + } + + for (i, seed) in spec.seeds.iter().enumerate() { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + seed_refs.push(quote! { #value.as_bytes() }); + } + SeedElement::Expression(expr) => { + // Handle byte string literals: b"seed" -> use directly (no .as_bytes()) + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + seed_refs.push(quote! { &[#(#bytes),*] }); + continue; + } + } + + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { + seed_refs.push( + quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }, + ); + continue; + } + } + } + + let binding_name = + syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); + let mapped_expr = map_pda_expr_to_ctx_seeds(expr, &ctx_field_names); + bindings.push(quote! { + let #binding_name = #mapped_expr; + }); + seed_refs.push(quote! { (#binding_name).as_ref() }); + } + } + } + + let indices: Vec = (0..seed_refs.len()).collect(); + + Ok(quote! { + #(#bindings)* + let seeds: &[&[u8]] = &[#(#seed_refs,)*]; + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); + let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); + #( + seeds_vec.push(seeds[#indices].to_vec()); + )* + seeds_vec.push(vec![bump]); + Ok((seeds_vec, pda)) + }) +} + +// ============================================================================= +// PDA SEED PROVIDER IMPLS +// ============================================================================= + +#[inline(never)] +pub fn generate_pda_seed_provider_impls( + account_types: &[Ident], + pda_ctx_seeds: &[PdaCtxSeedInfo], + pda_seeds: &Option>, + instruction_data: &[InstructionDataSpec], +) -> Result> { + account_types + .iter() + .zip(pda_ctx_seeds.iter()) + .map(|(name, ctx_info)| { + let name_str = name.to_string(); + let spec = if let Some(ref pda_seed_specs) = pda_seeds { + pda_seed_specs + .iter() + .find(|s| s.variant == name_str) + .ok_or_else(|| { + super::parsing::macro_error!( + name, + "No seed specification for account type '{}'", + name_str + ) + })? + } else { + return Err(super::parsing::macro_error!( + name, + "No seed specifications provided" + )); + }; + + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_fields_decl: Vec<_> = ctx_fields + .iter() + .map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }) + .collect(); + + let ctx_seeds_struct = if ctx_fields.is_empty() { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name; + } + } else { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name { + #(#ctx_fields_decl),* + } + } + }; + + let seed_derivation = + generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, instruction_data, ctx_fields)?; + Ok(quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &(), + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation + } + } + }) + }) + .collect() +} diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 268b9d108a..825c568057 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -1,926 +1,38 @@ -//! Compressible instructions generation. +//! Compressible instructions generation - orchestration module. use crate::utils::to_snake_case; use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{ - parse::{Parse, ParseStream}, - punctuated::Punctuated, - Expr, Ident, Item, ItemMod, LitStr, Result, Token, -}; - -macro_rules! macro_error { - ($span:expr, $msg:expr) => { - syn::Error::new_spanned( - $span, - format!( - "{}\n --> macro location: {}:{}", - $msg, - file!(), - line!() - ) - ) - }; - ($span:expr, $fmt:expr, $($arg:tt)*) => { - syn::Error::new_spanned( - $span, - format!( - concat!($fmt, "\n --> macro location: {}:{}"), - $($arg)*, - file!(), - line!() - ) - ) - }; -} - -#[derive(Debug, Clone, Copy)] -pub enum InstructionVariant { - PdaOnly, - TokenOnly, - Mixed, -} - -#[derive(Clone)] -pub struct TokenSeedSpec { - pub variant: Ident, - pub _eq: Token![=], - pub is_token: Option, - pub seeds: Punctuated, - pub authority: Option>, -} - -impl Parse for TokenSeedSpec { - fn parse(input: ParseStream) -> Result { - let variant: Ident = input.parse()?; - let _eq: Token![=] = input.parse()?; - - let content; - syn::parenthesized!(content in input); - - // New explicit syntax: - // PDA: TypeName = (seeds = (...)) - // Token: TypeName = (is_token, seeds = (...), authority = (...)) - let mut is_token = None; - let mut seeds = Punctuated::new(); - let mut authority = None; - - while !content.is_empty() { - if content.peek(Ident) { - let ident: Ident = content.parse()?; - let ident_str = ident.to_string(); - - match ident_str.as_str() { - "is_token" | "true" => { - is_token = Some(true); - } - "is_pda" | "false" => { - is_token = Some(false); - } - "seeds" => { - let _eq: Token![=] = content.parse()?; - let seeds_content; - syn::parenthesized!(seeds_content in content); - seeds = parse_seed_elements(&seeds_content)?; - } - "authority" => { - let _eq: Token![=] = content.parse()?; - authority = Some(parse_authority_seeds(&content)?); - } - _ => { - return Err(syn::Error::new_spanned( - &ident, - format!( - "Unknown keyword '{}'. Expected: is_token, seeds, or authority.\n\ - Use explicit syntax: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ - For tokens: TypeName = (is_token, seeds = (...), authority = (...))", - ident_str - ), - )); - } - } - } else { - return Err(syn::Error::new( - content.span(), - "Expected keyword (is_token, seeds, or authority). Use explicit syntax:\n\ - - PDA: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ - - Token: TypeName = (is_token, seeds = (...), authority = (...))", - )); - } - - if content.peek(Token![,]) { - let _comma: Token![,] = content.parse()?; - } else { - break; - } - } - - if seeds.is_empty() { - return Err(syn::Error::new_spanned( - &variant, - format!( - "Missing seeds for '{}'. Use: {} = (seeds = (\"seed\", ctx.account, ...))", - variant, variant - ), - )); - } - - Ok(TokenSeedSpec { - variant, - _eq, - is_token, - seeds, - authority, - }) - } -} - -/// Parse seed elements from within seeds = (...) -fn parse_seed_elements(content: ParseStream) -> Result> { - let mut seeds = Punctuated::new(); - - while !content.is_empty() { - seeds.push(content.parse::()?); - - if content.peek(Token![,]) { - let _: Token![,] = content.parse()?; - if content.is_empty() { - break; - } - } else { - break; - } - } - - Ok(seeds) -} - -/// Parse authority seeds - either parenthesized tuple or single expression -fn parse_authority_seeds(content: ParseStream) -> Result> { - if content.peek(syn::token::Paren) { - let auth_content; - syn::parenthesized!(auth_content in content); - let mut auth_seeds = Vec::new(); - - while !auth_content.is_empty() { - auth_seeds.push(auth_content.parse::()?); - if auth_content.peek(Token![,]) { - let _: Token![,] = auth_content.parse()?; - } else { - break; - } - } - Ok(auth_seeds) - } else { - // Single expression (e.g., LIGHT_CPI_SIGNER) - Ok(vec![content.parse::()?]) - } -} - -#[derive(Clone)] -pub enum SeedElement { - Literal(LitStr), - Expression(Box), -} - -impl Parse for SeedElement { - fn parse(input: ParseStream) -> Result { - if input.peek(LitStr) { - Ok(SeedElement::Literal(input.parse()?)) - } else { - Ok(SeedElement::Expression(input.parse()?)) - } - } -} - -/// Recursively extract field names from expressions matching `base.field` or `base.nested.field`. -/// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) -/// -/// Parameters: -/// - `base_ident`: The base identifier to match (e.g., "ctx" or "data") -/// - `nested_prefix`: Optional nested field name (e.g., "accounts" for ctx.accounts.XXX) -fn extract_fields_by_base( - expr: &syn::Expr, - base_ident: &str, - nested_prefix: Option<&str>, - fields: &mut Vec, -) { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for base.XXX pattern (direct field access) - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == base_ident { - fields.push(field_name.clone()); - return; - } - } - } - // Check for base.nested.XXX pattern (nested field access) if nested_prefix is provided - if let Some(nested) = nested_prefix { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == nested { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == base_ident { - fields.push(field_name.clone()); - return; - } - } - } - } - } - } - } - } - // Recurse into base expression - extract_fields_by_base(&field_expr.base, base_ident, nested_prefix, fields); - } - syn::Expr::MethodCall(method) => { - // Recurse into receiver and args - extract_fields_by_base(&method.receiver, base_ident, nested_prefix, fields); - for arg in &method.args { - extract_fields_by_base(arg, base_ident, nested_prefix, fields); - } - } - syn::Expr::Call(call) => { - // Recurse into function args - for arg in &call.args { - extract_fields_by_base(arg, base_ident, nested_prefix, fields); - } - } - syn::Expr::Reference(ref_expr) => { - extract_fields_by_base(&ref_expr.expr, base_ident, nested_prefix, fields); - } - syn::Expr::Paren(paren) => { - extract_fields_by_base(&paren.expr, base_ident, nested_prefix, fields); - } - _ => {} - } -} - -/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. -fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { - extract_fields_by_base(expr, "ctx", Some("accounts"), fields); -} - -/// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. -fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { - let mut fields = Vec::new(); - if let SeedElement::Expression(expr) = seed { - extract_ctx_fields_from_expr(expr, &mut fields); - } - fields -} - -/// Extract all ctx.accounts.XXX field names from a list of seed elements. -/// Deduplicates the fields. -pub fn extract_ctx_seed_fields( - seeds: &syn::punctuated::Punctuated, -) -> Vec { - let mut all_fields = Vec::new(); - for seed in seeds { - all_fields.extend(extract_ctx_account_fields(seed)); - } - // Deduplicate while preserving order - let mut seen = std::collections::HashSet::new(); - all_fields - .into_iter() - .filter(|f| seen.insert(f.to_string())) - .collect() -} - -/// Extract data.XXX field names from an expression recursively. -fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { - extract_fields_by_base(expr, "data", None, fields); -} - -/// Phase 5: Extract all data.XXX field names from a list of seed elements. -pub fn extract_data_seed_fields( - seeds: &syn::punctuated::Punctuated, -) -> Vec { - let mut all_fields = Vec::new(); - for seed in seeds { - if let SeedElement::Expression(expr) = seed { - extract_data_fields_from_expr(expr, &mut all_fields); - } - } - // Deduplicate while preserving order - let mut seen = std::collections::HashSet::new(); - all_fields - .into_iter() - .filter(|f| seen.insert(f.to_string())) - .collect() -} - -pub struct InstructionDataSpec { - pub field_name: Ident, - pub field_type: syn::Type, -} - -impl Parse for InstructionDataSpec { - fn parse(input: ParseStream) -> Result { - let field_name: Ident = input.parse()?; - let _eq: Token![=] = input.parse()?; - let field_type: syn::Type = input.parse()?; - - Ok(InstructionDataSpec { - field_name, - field_type, - }) - } -} - -pub fn generate_decompress_context_impl( - _variant: InstructionVariant, - pda_ctx_seeds: Vec, - token_variant_ident: Ident, -) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); - - let trait_impl = - crate::rentfree::traits::decompress_context::generate_decompress_context_trait_impl( - pda_ctx_seeds, - token_variant_ident, - lifetime, - )?; - - Ok(syn::parse_quote! { - mod __decompress_context_impl { - use super::*; - - #trait_impl - } - }) -} - -pub fn generate_process_decompress_accounts_idempotent( - _variant: InstructionVariant, - _instruction_data: &[InstructionDataSpec], -) -> Result { - // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_decompress_accounts_idempotent<'info>( - accounts: &DecompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - light_sdk::compressible::process_decompress_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - proof, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - std::option::Option::None::<&()>, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} - -pub fn generate_decompress_instruction_entrypoint( - _variant: InstructionVariant, - _instruction_data: &[InstructionDataSpec], -) -> Result { - // Phase 4: seed_data removed - data.* seeds come from unpacked account data, ctx.* from variant idx - - Ok(syn::parse_quote! { - #[inline(never)] - pub fn decompress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, DecompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - __processor_functions::process_decompress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - proof, - compressed_accounts, - system_accounts_offset, - ) - } - }) -} - -pub fn generate_compress_context_impl( - _variant: InstructionVariant, - account_types: Vec, -) -> Result { - let lifetime: syn::Lifetime = syn::parse_quote!('info); - - let compress_arms: Vec<_> = account_types.iter().map(|name| { - quote! { - d if d == #name::LIGHT_DISCRIMINATOR => { - drop(data); - let data_borrow = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - drop(data_borrow); - - let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( - program_id, - account_info, - &mut account_data, - meta, - cpi_accounts, - &compression_config.address_space, - )?; - // Lamport transfers are handled by close() in process_compress_pda_accounts_idempotent - // All lamports go to rent_sponsor for simplicity - Ok(Some(compressed_info)) - } - } - }).collect(); - - Ok(syn::parse_quote! { - mod __compress_context_impl { - use super::*; - use light_sdk::LightDiscriminator; - use light_sdk::compressible::HasCompressionInfo; - - impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { - fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &*self.fee_payer - } - - fn config(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.config - } - - fn rent_sponsor(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.rent_sponsor - } - - fn compression_authority(&self) -> &solana_account_info::AccountInfo<#lifetime> { - &self.compression_authority - } - - fn compress_pda_account( - &self, - account_info: &solana_account_info::AccountInfo<#lifetime>, - meta: &light_sdk::instruction::account_meta::CompressedAccountMetaNoLamportsNoAddress, - cpi_accounts: &light_sdk::cpi::v2::CpiAccounts<'_, #lifetime>, - compression_config: &light_sdk::compressible::CompressibleConfig, - program_id: &solana_pubkey::Pubkey, - ) -> std::result::Result, solana_program_error::ProgramError> { - let data = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - let discriminator = &data[0..8]; - - match discriminator { - #(#compress_arms)* - _ => { - let err: anchor_lang::error::Error = anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - Err(solana_program_error::ProgramError::Custom(code)) - } - } - } - } - } - }) -} +use syn::{Ident, Item, ItemMod, Result}; -pub fn generate_process_compress_accounts_idempotent( - _variant: InstructionVariant, -) -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - pub fn process_compress_accounts_idempotent<'info>( - accounts: &CompressAccountsIdempotent<'info>, - remaining_accounts: &[solana_account_info::AccountInfo<'info>], - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - light_sdk::compressible::compress_runtime::process_compress_pda_accounts_idempotent( - accounts, - remaining_accounts, - compressed_accounts, - system_accounts_offset, - LIGHT_CPI_SIGNER, - &crate::ID, - ) - .map_err(|e: solana_program_error::ProgramError| -> anchor_lang::error::Error { e.into() }) - } - }) -} - -pub fn generate_compress_instruction_entrypoint( - _variant: InstructionVariant, -) -> Result { - Ok(syn::parse_quote! { - #[inline(never)] - #[allow(clippy::too_many_arguments)] - pub fn compress_accounts_idempotent<'info>( - ctx: Context<'_, '_, '_, 'info, CompressAccountsIdempotent<'info>>, - proof: light_sdk::instruction::ValidityProof, - compressed_accounts: Vec, - system_accounts_offset: u8, - ) -> Result<()> { - __processor_functions::process_compress_accounts_idempotent( - &ctx.accounts, - &ctx.remaining_accounts, - compressed_accounts, - system_accounts_offset, - ) - } - }) -} - -/// Phase 3: Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. -/// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) -#[inline(never)] -fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( - spec: &TokenSeedSpec, - _instruction_data: &[InstructionDataSpec], - ctx_seed_fields: &[syn::Ident], -) -> Result { - let mut bindings: Vec = Vec::new(); - let mut seed_refs = Vec::new(); - - // Convert ctx_seed_fields to a set for quick lookup - let ctx_field_names: std::collections::HashSet = - ctx_seed_fields.iter().map(|f| f.to_string()).collect(); - - // Recursively rewrite expressions: - // - `data.` -> `self.` (from unpacked compressed account data - Phase 4) - // - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) - // - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) - fn map_pda_expr_to_ctx_seeds( - expr: &syn::Expr, - ctx_field_names: &std::collections::HashSet, - ) -> syn::Expr { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Handle nested field access: ctx.accounts.field_name -> ctx_seeds.field_name - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - // ctx.accounts.field -> ctx_seeds.field (direct Pubkey) - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - } - } - } - // Handle direct field access - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - // Phase 4: data.field -> self.field (from unpacked compressed account data) - return syn::parse_quote! { self.#field_name }; - } else if segment.ident == "ctx" { - let field_str = field_name.to_string(); - if ctx_field_names.contains(&field_str) { - // ctx.field -> ctx_seeds.field (direct Pubkey) - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - } - } - expr.clone() - } - syn::Expr::MethodCall(method_call) => { - let mut new_method_call = method_call.clone(); - new_method_call.receiver = Box::new(map_pda_expr_to_ctx_seeds( - &method_call.receiver, - ctx_field_names, - )); - new_method_call.args = method_call - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::MethodCall(new_method_call) - } - syn::Expr::Call(call_expr) => { - let mut new_call_expr = call_expr.clone(); - new_call_expr.args = call_expr - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::Call(new_call_expr) - } - syn::Expr::Reference(ref_expr) => { - let mut new_ref_expr = ref_expr.clone(); - new_ref_expr.expr = - Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); - syn::Expr::Reference(new_ref_expr) - } - _ => expr.clone(), - } - } - - for (i, seed) in spec.seeds.iter().enumerate() { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - seed_refs.push(quote! { #value.as_bytes() }); - } - SeedElement::Expression(expr) => { - // Handle byte string literals: b"seed" -> use directly (no .as_bytes()) - if let syn::Expr::Lit(lit_expr) = &**expr { - if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { - let bytes = byte_str.value(); - seed_refs.push(quote! { &[#(#bytes),*] }); - continue; - } - } - - // Handle uppercase constants - if let syn::Expr::Path(path_expr) = &**expr { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { - seed_refs.push( - quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }, - ); - continue; - } - } - } - - let binding_name = - syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = map_pda_expr_to_ctx_seeds(expr, &ctx_field_names); - bindings.push(quote! { - let #binding_name = #mapped_expr; - }); - seed_refs.push(quote! { (#binding_name).as_ref() }); - } - } - } - - let indices: Vec = (0..seed_refs.len()).collect(); - - Ok(quote! { - #(#bindings)* - let seeds: &[&[u8]] = &[#(#seed_refs,)*]; - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(seeds, program_id); - let mut seeds_vec = Vec::with_capacity(seeds.len() + 1); - #( - seeds_vec.push(seeds[#indices].to_vec()); - )* - seeds_vec.push(vec![bump]); - Ok((seeds_vec, pda)) - }) -} - -#[inline(never)] -fn generate_decompress_accounts_struct( - required_accounts: &[String], - variant: InstructionVariant, -) -> Result { - let mut account_fields = vec![ - quote! { - #[account(mut)] - pub fee_payer: Signer<'info> - }, - quote! { - /// CHECK: Checked by SDK - pub config: AccountInfo<'info> - }, - ]; - - match variant { - InstructionVariant::PdaOnly => { - unreachable!() - } - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: anyone can pay - #[account(mut)] - pub rent_sponsor: UncheckedAccount<'info> - }, - quote! { - /// CHECK: optional - only needed if decompressing tokens - #[account(mut)] - pub ctoken_rent_sponsor: Option> - }, - ]); - } - } - - match variant { - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub light_token_program: Option> - }, - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option> - }, - quote! { - /// CHECK: Checked by SDK - pub ctoken_config: Option> - }, - ]); - } - InstructionVariant::PdaOnly => { - unreachable!() - } - } - - let standard_fields = [ - "fee_payer", - "rent_sponsor", - "ctoken_rent_sponsor", - "config", - "light_token_program", - "ctoken_cpi_authority", - "ctoken_config", - ]; - - for account_name in required_accounts { - if !standard_fields.contains(&account_name.as_str()) { - let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); - // Mark seed accounts as writable to support CPI calls that may need them writable - account_fields.push(quote! { - /// CHECK: optional seed account - may be used in CPIs - #[account(mut)] - pub #account_ident: Option> - }); - } - } - - let struct_def = quote! { - #[derive(Accounts)] - pub struct DecompressAccountsIdempotent<'info> { - #(#account_fields,)* - } - }; - - syn::parse2(struct_def) -} - -#[inline(never)] -fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result { - let size_checks: Vec<_> = account_types.iter().map(|account_type| { - quote! { - const _: () = { - const COMPRESSED_SIZE: usize = 8 + <#account_type as light_sdk::compressible::compression_info::CompressedInitSpace>::COMPRESSED_INIT_SPACE; - if COMPRESSED_SIZE > 800 { - panic!(concat!( - "Compressed account '", stringify!(#account_type), "' exceeds 800-byte compressible account size limit. If you need support for larger accounts, send a message to team@lightprotocol.com" - )); - } - }; - } - }).collect(); - - Ok(quote! { #(#size_checks)* }) -} - -#[inline(never)] -fn generate_error_codes(variant: InstructionVariant) -> Result { - let base_errors = quote! { - #[msg("Rent sponsor mismatch")] - InvalidRentSponsor, - #[msg("Missing seed account")] - MissingSeedAccount, - #[msg("Seed value does not match account data")] - SeedMismatch, - }; +// Re-export types from parsing for external use +pub use super::parsing::{ + extract_ctx_seed_fields, extract_data_seed_fields, InstructionDataSpec, InstructionVariant, + SeedElement, TokenSeedSpec, +}; - let variant_specific_errors = match variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => quote! { - #[msg("Not implemented")] - CTokenDecompressionNotImplemented, - #[msg("Not implemented")] - PdaDecompressionNotImplemented, - #[msg("Not implemented")] - TokenCompressionNotImplemented, - #[msg("Not implemented")] - PdaCompressionNotImplemented, - }, - }; +use super::parsing::{ + convert_classified_to_seed_elements, convert_classified_to_seed_elements_vec, + extract_context_and_params, macro_error, wrap_function_with_rentfree, +}; - Ok(quote! { - #[error_code] - pub enum RentFreeInstructionError { - #base_errors - #variant_specific_errors - } - }) -} +use super::compress::{ + generate_compress_accounts_struct, generate_compress_context_impl, + generate_compress_instruction_entrypoint, generate_error_codes, + generate_process_compress_accounts_idempotent, validate_compressed_account_sizes, +}; -/// Convert ClassifiedSeed to SeedElement (Punctuated) -fn convert_classified_to_seed_elements( - seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], -) -> Punctuated { - use crate::rentfree::traits::anchor_seeds::ClassifiedSeed; +use super::decompress::{ + generate_decompress_accounts_struct, generate_decompress_context_impl, + generate_decompress_instruction_entrypoint, generate_pda_seed_provider_impls, + generate_process_decompress_accounts_idempotent, +}; - let mut result = Punctuated::new(); - for seed in seeds { - let elem = match seed { - ClassifiedSeed::Literal(bytes) => { - // Convert to string literal - if let Ok(s) = std::str::from_utf8(bytes) { - SeedElement::Literal(syn::LitStr::new(s, proc_macro2::Span::call_site())) - } else { - // Byte array - use expression - let byte_values: Vec<_> = bytes.iter().map(|b| quote!(#b)).collect(); - let expr: Expr = syn::parse_quote!(&[#(#byte_values),*]); - SeedElement::Expression(Box::new(expr)) - } - } - ClassifiedSeed::Constant(path) => { - let expr: Expr = syn::parse_quote!(#path); - SeedElement::Expression(Box::new(expr)) - } - ClassifiedSeed::CtxAccount(ident) => { - let expr: Expr = syn::parse_quote!(ctx.#ident); - SeedElement::Expression(Box::new(expr)) - } - ClassifiedSeed::DataField { - field_name, - conversion: None, - } => { - let expr: Expr = syn::parse_quote!(data.#field_name); - SeedElement::Expression(Box::new(expr)) - } - ClassifiedSeed::DataField { - field_name, - conversion: Some(method), - } => { - let expr: Expr = syn::parse_quote!(data.#field_name.#method()); - SeedElement::Expression(Box::new(expr)) - } - ClassifiedSeed::FunctionCall { func, ctx_args } => { - let args: Vec = ctx_args - .iter() - .map(|arg| syn::parse_quote!(&ctx.#arg.key())) - .collect(); - let expr: Expr = syn::parse_quote!(#func(#(#args),*)); - SeedElement::Expression(Box::new(expr)) - } - }; - result.push(elem); - } - result -} +use super::variant_enum::PdaCtxSeedInfo; -fn convert_classified_to_seed_elements_vec( - seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], -) -> Vec { - convert_classified_to_seed_elements(seeds) - .into_iter() - .collect() -} +// ============================================================================= +// MAIN ASSEMBLY FUNCTION +// ============================================================================= /// Generate all code from extracted seeds (shared logic with add_compressible_instructions) #[inline(never)] @@ -956,14 +68,14 @@ fn generate_from_extracted_seeds( } } - let pda_ctx_seeds: Vec = pda_seeds + let pda_ctx_seeds: Vec = pda_seeds .as_ref() .map(|specs| { specs .iter() .map(|spec| { let ctx_fields = extract_ctx_seed_fields(&spec.seeds); - super::variant_enum::PdaCtxSeedInfo::new(spec.variant.clone(), ctx_fields) + PdaCtxSeedInfo::new(spec.variant.clone(), ctx_fields) }) .collect() }) @@ -1065,60 +177,8 @@ fn generate_from_extracted_seeds( let error_codes = generate_error_codes(instruction_variant)?; let decompress_accounts = generate_decompress_accounts_struct(&[], instruction_variant)?; - let pda_seed_provider_impls: Result> = account_types - .iter() - .zip(pda_ctx_seeds.iter()) - .map(|(name, ctx_info)| { - let name_str = name.to_string(); - let spec = if let Some(ref pda_seed_specs) = pda_seeds { - pda_seed_specs - .iter() - .find(|s| s.variant == name_str) - .ok_or_else(|| { - macro_error!(name, "No seed specification for account type '{}'", name_str) - })? - } else { - return Err(macro_error!(name, "No seed specifications provided")); - }; - - let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); - let ctx_fields = &ctx_info.ctx_seed_fields; - let ctx_fields_decl: Vec<_> = ctx_fields.iter().map(|field| { - quote! { pub #field: solana_pubkey::Pubkey } - }).collect(); - - let ctx_seeds_struct = if ctx_fields.is_empty() { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name; - } - } else { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name { - #(#ctx_fields_decl),* - } - } - }; - - let seed_derivation = generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, &instruction_data, ctx_fields)?; - Ok(quote! { - #ctx_seeds_struct - - impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - ctx_seeds: &#ctx_seeds_struct_name, - _seed_params: &(), - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - }) - }) - .collect(); - let pda_seed_provider_impls = pda_seed_provider_impls?; + let pda_seed_provider_impls = + generate_pda_seed_provider_impls(&account_types, &pda_ctx_seeds, &pda_seeds, &instruction_data)?; let trait_impls: syn::ItemMod = syn::parse_quote! { mod __trait_impls { @@ -1144,26 +204,7 @@ fn generate_from_extracted_seeds( let decompress_instruction = generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; - let compress_accounts: syn::ItemStruct = match instruction_variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => syn::parse_quote! { - #[derive(Accounts)] - pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, - } - }, - }; - + let compress_accounts = generate_compress_accounts_struct(instruction_variant)?; let compress_context_impl = generate_compress_context_impl(instruction_variant, account_types.clone())?; let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; @@ -1326,7 +367,7 @@ fn generate_from_extracted_seeds( } // ============================================================================= -// COMPRESSIBLE_PROGRAM: Auto-discovers seeds from external module files +// MAIN ENTRY POINT // ============================================================================= /// Main entry point for #[rentfree_program] macro. @@ -1352,92 +393,6 @@ fn generate_from_extracted_seeds( /// } /// } /// ``` -/// Extract the Context type name from a function's parameters. -/// Returns (struct_name, params_ident) if found. -fn extract_context_and_params(fn_item: &syn::ItemFn) -> Option<(String, Ident)> { - let mut context_type = None; - let mut params_ident = None; - - for input in &fn_item.sig.inputs { - if let syn::FnArg::Typed(pat_type) = input { - if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { - // Check if this is a Context parameter - if let syn::Type::Path(type_path) = &*pat_type.ty { - if let Some(segment) = type_path.path.segments.last() { - if segment.ident == "Context" { - // Extract T from Context<'_, '_, '_, 'info, T<'info>> or Context - if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { - // Find the last type argument (T or T<'info>) - for arg in args.args.iter().rev() { - if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = - arg - { - if let Some(inner_seg) = inner_path.path.segments.last() { - context_type = Some(inner_seg.ident.to_string()); - break; - } - } - } - } - } - } - } - - // Track potential params argument (not ctx, not signer-like names) - let name = pat_ident.ident.to_string(); - if name != "ctx" && !name.contains("signer") && !name.contains("bump") { - // Prefer "params" but accept others - if name == "params" || params_ident.is_none() { - params_ident = Some(pat_ident.ident.clone()); - } - } - } - } - } - - match (context_type, params_ident) { - (Some(ctx), Some(params)) => Some((ctx, params)), - _ => None, - } -} - -/// Wrap a function with pre_init/finalize logic. -fn wrap_function_with_rentfree(fn_item: &syn::ItemFn, params_ident: &Ident) -> syn::ItemFn { - let fn_vis = &fn_item.vis; - let fn_sig = &fn_item.sig; - let fn_block = &fn_item.block; - let fn_attrs = &fn_item.attrs; - - let wrapped: syn::ItemFn = syn::parse_quote! { - #(#fn_attrs)* - #fn_vis #fn_sig { - // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) - use light_sdk::compressible::{LightPreInit, LightFinalize}; - let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe - })?; - - // Execute the original handler body in a closure - let __light_handler_result = (|| #fn_block)(); - - // Phase 2: On success, finalize compression - if __light_handler_result.is_ok() { - ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) - .map_err(|e| { - let pe: solana_program_error::ProgramError = e.into(); - pe - })?; - } - - __light_handler_result - } - }; - - wrapped -} - #[inline(never)] pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { use super::crate_context::CrateContext; diff --git a/sdk-libs/macros/src/rentfree/program/mod.rs b/sdk-libs/macros/src/rentfree/program/mod.rs index 4f2b929448..ce3085bf9d 100644 --- a/sdk-libs/macros/src/rentfree/program/mod.rs +++ b/sdk-libs/macros/src/rentfree/program/mod.rs @@ -6,6 +6,9 @@ //! - Generates all necessary types, enums, and instruction handlers pub mod crate_context; +mod parsing; +mod decompress; +mod compress; pub mod instructions; pub mod seed_providers; pub mod variant_enum; diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs new file mode 100644 index 0000000000..729625b300 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -0,0 +1,504 @@ +//! Parsing types, expression analysis, seed conversion, and function wrapping. + +use quote::quote; +use syn::{ + parse::{Parse, ParseStream}, + punctuated::Punctuated, + Expr, Ident, ItemFn, LitStr, Result, Token, +}; + +// ============================================================================= +// MACRO ERROR HELPER +// ============================================================================= + +macro_rules! macro_error { + ($span:expr, $msg:expr) => { + syn::Error::new_spanned( + $span, + format!( + "{}\n --> macro location: {}:{}", + $msg, + file!(), + line!() + ) + ) + }; + ($span:expr, $fmt:expr, $($arg:tt)*) => { + syn::Error::new_spanned( + $span, + format!( + concat!($fmt, "\n --> macro location: {}:{}"), + $($arg)*, + file!(), + line!() + ) + ) + }; +} + +pub(crate) use macro_error; + +// ============================================================================= +// CORE TYPES +// ============================================================================= + +#[derive(Debug, Clone, Copy)] +pub enum InstructionVariant { + PdaOnly, + TokenOnly, + Mixed, +} + +#[derive(Clone)] +pub struct TokenSeedSpec { + pub variant: Ident, + pub _eq: Token![=], + pub is_token: Option, + pub seeds: Punctuated, + pub authority: Option>, +} + +impl Parse for TokenSeedSpec { + fn parse(input: ParseStream) -> Result { + let variant: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; + + let content; + syn::parenthesized!(content in input); + + // New explicit syntax: + // PDA: TypeName = (seeds = (...)) + // Token: TypeName = (is_token, seeds = (...), authority = (...)) + let mut is_token = None; + let mut seeds = Punctuated::new(); + let mut authority = None; + + while !content.is_empty() { + if content.peek(Ident) { + let ident: Ident = content.parse()?; + let ident_str = ident.to_string(); + + match ident_str.as_str() { + "is_token" | "true" => { + is_token = Some(true); + } + "is_pda" | "false" => { + is_token = Some(false); + } + "seeds" => { + let _eq: Token![=] = content.parse()?; + let seeds_content; + syn::parenthesized!(seeds_content in content); + seeds = parse_seed_elements(&seeds_content)?; + } + "authority" => { + let _eq: Token![=] = content.parse()?; + authority = Some(parse_authority_seeds(&content)?); + } + _ => { + return Err(syn::Error::new_spanned( + &ident, + format!( + "Unknown keyword '{}'. Expected: is_token, seeds, or authority.\n\ + Use explicit syntax: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + For tokens: TypeName = (is_token, seeds = (...), authority = (...))", + ident_str + ), + )); + } + } + } else { + return Err(syn::Error::new( + content.span(), + "Expected keyword (is_token, seeds, or authority). Use explicit syntax:\n\ + - PDA: TypeName = (seeds = (\"seed\", ctx.account, ...))\n\ + - Token: TypeName = (is_token, seeds = (...), authority = (...))", + )); + } + + if content.peek(Token![,]) { + let _comma: Token![,] = content.parse()?; + } else { + break; + } + } + + if seeds.is_empty() { + return Err(syn::Error::new_spanned( + &variant, + format!( + "Missing seeds for '{}'. Use: {} = (seeds = (\"seed\", ctx.account, ...))", + variant, variant + ), + )); + } + + Ok(TokenSeedSpec { + variant, + _eq, + is_token, + seeds, + authority, + }) + } +} + +/// Parse seed elements from within seeds = (...) +fn parse_seed_elements(content: ParseStream) -> Result> { + let mut seeds = Punctuated::new(); + + while !content.is_empty() { + seeds.push(content.parse::()?); + + if content.peek(Token![,]) { + let _: Token![,] = content.parse()?; + if content.is_empty() { + break; + } + } else { + break; + } + } + + Ok(seeds) +} + +/// Parse authority seeds - either parenthesized tuple or single expression +fn parse_authority_seeds(content: ParseStream) -> Result> { + if content.peek(syn::token::Paren) { + let auth_content; + syn::parenthesized!(auth_content in content); + let mut auth_seeds = Vec::new(); + + while !auth_content.is_empty() { + auth_seeds.push(auth_content.parse::()?); + if auth_content.peek(Token![,]) { + let _: Token![,] = auth_content.parse()?; + } else { + break; + } + } + Ok(auth_seeds) + } else { + // Single expression (e.g., LIGHT_CPI_SIGNER) + Ok(vec![content.parse::()?]) + } +} + +#[derive(Clone)] +pub enum SeedElement { + Literal(LitStr), + Expression(Box), +} + +impl Parse for SeedElement { + fn parse(input: ParseStream) -> Result { + if input.peek(LitStr) { + Ok(SeedElement::Literal(input.parse()?)) + } else { + Ok(SeedElement::Expression(input.parse()?)) + } + } +} + +pub struct InstructionDataSpec { + pub field_name: Ident, + pub field_type: syn::Type, +} + +impl Parse for InstructionDataSpec { + fn parse(input: ParseStream) -> Result { + let field_name: Ident = input.parse()?; + let _eq: Token![=] = input.parse()?; + let field_type: syn::Type = input.parse()?; + + Ok(InstructionDataSpec { + field_name, + field_type, + }) + } +} + +// ============================================================================= +// EXPRESSION ANALYSIS +// ============================================================================= + +/// Recursively extract field names from expressions matching `base.field` or `base.nested.field`. +/// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) +/// +/// Parameters: +/// - `base_ident`: The base identifier to match (e.g., "ctx" or "data") +/// - `nested_prefix`: Optional nested field name (e.g., "accounts" for ctx.accounts.XXX) +fn extract_fields_by_base( + expr: &syn::Expr, + base_ident: &str, + nested_prefix: Option<&str>, + fields: &mut Vec, +) { + match expr { + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for base.XXX pattern (direct field access) + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == base_ident { + fields.push(field_name.clone()); + return; + } + } + } + // Check for base.nested.XXX pattern (nested field access) if nested_prefix is provided + if let Some(nested) = nested_prefix { + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == nested { + if let syn::Expr::Path(path) = &*nested_field.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == base_ident { + fields.push(field_name.clone()); + return; + } + } + } + } + } + } + } + } + // Recurse into base expression + extract_fields_by_base(&field_expr.base, base_ident, nested_prefix, fields); + } + syn::Expr::MethodCall(method) => { + // Recurse into receiver and args + extract_fields_by_base(&method.receiver, base_ident, nested_prefix, fields); + for arg in &method.args { + extract_fields_by_base(arg, base_ident, nested_prefix, fields); + } + } + syn::Expr::Call(call) => { + // Recurse into function args + for arg in &call.args { + extract_fields_by_base(arg, base_ident, nested_prefix, fields); + } + } + syn::Expr::Reference(ref_expr) => { + extract_fields_by_base(&ref_expr.expr, base_ident, nested_prefix, fields); + } + syn::Expr::Paren(paren) => { + extract_fields_by_base(&paren.expr, base_ident, nested_prefix, fields); + } + _ => {} + } +} + +/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. +fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + extract_fields_by_base(expr, "ctx", Some("accounts"), fields); +} + +/// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. +fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { + let mut fields = Vec::new(); + if let SeedElement::Expression(expr) = seed { + extract_ctx_fields_from_expr(expr, &mut fields); + } + fields +} + +/// Extract all ctx.accounts.XXX field names from a list of seed elements. +/// Deduplicates the fields. +pub fn extract_ctx_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + all_fields.extend(extract_ctx_account_fields(seed)); + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() +} + +/// Extract data.XXX field names from an expression recursively. +fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { + extract_fields_by_base(expr, "data", None, fields); +} + +/// Extract all data.XXX field names from a list of seed elements. +pub fn extract_data_seed_fields( + seeds: &syn::punctuated::Punctuated, +) -> Vec { + let mut all_fields = Vec::new(); + for seed in seeds { + if let SeedElement::Expression(expr) = seed { + extract_data_fields_from_expr(expr, &mut all_fields); + } + } + // Deduplicate while preserving order + let mut seen = std::collections::HashSet::new(); + all_fields + .into_iter() + .filter(|f| seen.insert(f.to_string())) + .collect() +} + +// ============================================================================= +// SEED CONVERSION +// ============================================================================= + +/// Convert ClassifiedSeed to SeedElement (Punctuated) +pub fn convert_classified_to_seed_elements( + seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], +) -> Punctuated { + use crate::rentfree::traits::anchor_seeds::ClassifiedSeed; + + let mut result = Punctuated::new(); + for seed in seeds { + let elem = match seed { + ClassifiedSeed::Literal(bytes) => { + // Convert to string literal + if let Ok(s) = std::str::from_utf8(bytes) { + SeedElement::Literal(syn::LitStr::new(s, proc_macro2::Span::call_site())) + } else { + // Byte array - use expression + let byte_values: Vec<_> = bytes.iter().map(|b| quote!(#b)).collect(); + let expr: Expr = syn::parse_quote!(&[#(#byte_values),*]); + SeedElement::Expression(Box::new(expr)) + } + } + ClassifiedSeed::Constant(path) => { + let expr: Expr = syn::parse_quote!(#path); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::CtxAccount(ident) => { + let expr: Expr = syn::parse_quote!(ctx.#ident); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { + field_name, + conversion: None, + } => { + let expr: Expr = syn::parse_quote!(data.#field_name); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::DataField { + field_name, + conversion: Some(method), + } => { + let expr: Expr = syn::parse_quote!(data.#field_name.#method()); + SeedElement::Expression(Box::new(expr)) + } + ClassifiedSeed::FunctionCall { func, ctx_args } => { + let args: Vec = ctx_args + .iter() + .map(|arg| syn::parse_quote!(&ctx.#arg.key())) + .collect(); + let expr: Expr = syn::parse_quote!(#func(#(#args),*)); + SeedElement::Expression(Box::new(expr)) + } + }; + result.push(elem); + } + result +} + +pub fn convert_classified_to_seed_elements_vec( + seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], +) -> Vec { + convert_classified_to_seed_elements(seeds) + .into_iter() + .collect() +} + +// ============================================================================= +// FUNCTION WRAPPING +// ============================================================================= + +/// Extract the Context type name from a function's parameters. +/// Returns (struct_name, params_ident) if found. +pub fn extract_context_and_params(fn_item: &ItemFn) -> Option<(String, Ident)> { + let mut context_type = None; + let mut params_ident = None; + + for input in &fn_item.sig.inputs { + if let syn::FnArg::Typed(pat_type) = input { + if let syn::Pat::Ident(pat_ident) = &*pat_type.pat { + // Check if this is a Context parameter + if let syn::Type::Path(type_path) = &*pat_type.ty { + if let Some(segment) = type_path.path.segments.last() { + if segment.ident == "Context" { + // Extract T from Context<'_, '_, '_, 'info, T<'info>> or Context + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + // Find the last type argument (T or T<'info>) + for arg in args.args.iter().rev() { + if let syn::GenericArgument::Type(syn::Type::Path(inner_path)) = + arg + { + if let Some(inner_seg) = inner_path.path.segments.last() { + context_type = Some(inner_seg.ident.to_string()); + break; + } + } + } + } + } + } + } + + // Track potential params argument (not ctx, not signer-like names) + let name = pat_ident.ident.to_string(); + if name != "ctx" && !name.contains("signer") && !name.contains("bump") { + // Prefer "params" but accept others + if name == "params" || params_ident.is_none() { + params_ident = Some(pat_ident.ident.clone()); + } + } + } + } + } + + match (context_type, params_ident) { + (Some(ctx), Some(params)) => Some((ctx, params)), + _ => None, + } +} + +/// Wrap a function with pre_init/finalize logic. +pub fn wrap_function_with_rentfree(fn_item: &ItemFn, params_ident: &Ident) -> ItemFn { + let fn_vis = &fn_item.vis; + let fn_sig = &fn_item.sig; + let fn_block = &fn_item.block; + let fn_attrs = &fn_item.attrs; + + let wrapped: ItemFn = syn::parse_quote! { + #(#fn_attrs)* + #fn_vis #fn_sig { + // Phase 1: Pre-init (creates mints via CPI context write, registers compressed addresses) + use light_sdk::compressible::{LightPreInit, LightFinalize}; + let __has_pre_init = ctx.accounts.light_pre_init(ctx.remaining_accounts, &#params_ident) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + + // Execute the original handler body in a closure + let __light_handler_result = (|| #fn_block)(); + + // Phase 2: On success, finalize compression + if __light_handler_result.is_ok() { + ctx.accounts.light_finalize(ctx.remaining_accounts, &#params_ident, __has_pre_init) + .map_err(|e| { + let pe: solana_program_error::ProgramError = e.into(); + pe + })?; + } + + __light_handler_result + } + }; + + wrapped +} From e955dec1ec0826b91dcee0fdb80bebb8498351ed Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 03:33:21 +0000 Subject: [PATCH 7/9] cleanup --- .../macros/src/rentfree/accounts/parse.rs | 4 +- .../macros/src/rentfree/program/compress.rs | 139 +++---- .../macros/src/rentfree/program/decompress.rs | 392 ++++++++---------- .../src/rentfree/program/instructions.rs | 36 +- sdk-libs/macros/src/rentfree/program/mod.rs | 2 +- .../macros/src/rentfree/program/parsing.rs | 6 +- .../{seed_providers.rs => seed_codegen.rs} | 29 +- sdk-libs/macros/src/rentfree/traits/mod.rs | 4 +- .../{anchor_seeds.rs => seed_extraction.rs} | 0 9 files changed, 271 insertions(+), 341 deletions(-) rename sdk-libs/macros/src/rentfree/program/{seed_providers.rs => seed_codegen.rs} (97%) rename sdk-libs/macros/src/rentfree/traits/{anchor_seeds.rs => seed_extraction.rs} (100%) diff --git a/sdk-libs/macros/src/rentfree/accounts/parse.rs b/sdk-libs/macros/src/rentfree/accounts/parse.rs index f8ab0834f8..c0c6d29d32 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -7,8 +7,8 @@ use syn::{ DeriveInput, Error, Expr, Ident, Token, Type, }; -// Import shared types from anchor_seeds module -pub(super) use crate::rentfree::traits::anchor_seeds::extract_account_inner_type; +// Import shared types from seed_extraction module +pub(super) use crate::rentfree::traits::seed_extraction::extract_account_inner_type; // Import LightMintField and parsing from light_mint module use super::light_mint::{parse_light_mint_attr, LightMintField}; diff --git a/sdk-libs/macros/src/rentfree/program/compress.rs b/sdk-libs/macros/src/rentfree/program/compress.rs index 85c9bca4eb..02c180c5b4 100644 --- a/sdk-libs/macros/src/rentfree/program/compress.rs +++ b/sdk-libs/macros/src/rentfree/program/compress.rs @@ -10,34 +10,16 @@ use super::parsing::InstructionVariant; // COMPRESS CONTEXT IMPL // ============================================================================= -pub fn generate_compress_context_impl( - _variant: InstructionVariant, - 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| { quote! { d if d == #name::LIGHT_DISCRIMINATOR => { drop(data); - let data_borrow = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; - let mut account_data = #name::try_deserialize(&mut &data_borrow[..]).map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; + let data_borrow = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; + let mut account_data = #name::try_deserialize(&mut &data_borrow[..]) + .map_err(__anchor_to_program_error)?; drop(data_borrow); let compressed_info = light_sdk::compressible::compress_account::prepare_account_for_compression::<#name>( @@ -48,8 +30,6 @@ pub fn generate_compress_context_impl( cpi_accounts, &compression_config.address_space, )?; - // Lamport transfers are handled by close() in process_compress_pda_accounts_idempotent - // All lamports go to rent_sponsor for simplicity Ok(Some(compressed_info)) } } @@ -61,6 +41,17 @@ pub fn generate_compress_context_impl( use light_sdk::LightDiscriminator; use light_sdk::compressible::HasCompressionInfo; + #[inline(always)] + fn __anchor_to_program_error>(e: E) -> solana_program_error::ProgramError { + let err: anchor_lang::error::Error = e.into(); + let program_error: anchor_lang::prelude::ProgramError = err.into(); + let code = match program_error { + anchor_lang::prelude::ProgramError::Custom(code) => code, + _ => 0, + }; + solana_program_error::ProgramError::Custom(code) + } + impl<#lifetime> light_sdk::compressible::CompressContext<#lifetime> for CompressAccountsIdempotent<#lifetime> { fn fee_payer(&self) -> &solana_account_info::AccountInfo<#lifetime> { &*self.fee_payer @@ -86,28 +77,12 @@ pub fn generate_compress_context_impl( compression_config: &light_sdk::compressible::CompressibleConfig, program_id: &solana_pubkey::Pubkey, ) -> std::result::Result, solana_program_error::ProgramError> { - let data = account_info.try_borrow_data().map_err(|e| { - let err: anchor_lang::error::Error = e.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - solana_program_error::ProgramError::Custom(code) - })?; + let data = account_info.try_borrow_data().map_err(__anchor_to_program_error)?; let discriminator = &data[0..8]; match discriminator { #(#compress_arms)* - _ => { - let err: anchor_lang::error::Error = anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch.into(); - let program_error: anchor_lang::prelude::ProgramError = err.into(); - let code = match program_error { - anchor_lang::prelude::ProgramError::Custom(code) => code, - _ => 0, - }; - Err(solana_program_error::ProgramError::Custom(code)) - } + _ => Err(__anchor_to_program_error(anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch)) } } } @@ -119,9 +94,7 @@ pub fn generate_compress_context_impl( // COMPRESS PROCESSOR // ============================================================================= -pub fn generate_process_compress_accounts_idempotent( - _variant: InstructionVariant, -) -> Result { +pub fn generate_process_compress_accounts_idempotent() -> Result { Ok(syn::parse_quote! { #[inline(never)] pub fn process_compress_accounts_idempotent<'info>( @@ -147,9 +120,7 @@ pub fn generate_process_compress_accounts_idempotent( // COMPRESS INSTRUCTION ENTRYPOINT // ============================================================================= -pub fn generate_compress_instruction_entrypoint( - _variant: InstructionVariant, -) -> Result { +pub fn generate_compress_instruction_entrypoint() -> Result { Ok(syn::parse_quote! { #[inline(never)] #[allow(clippy::too_many_arguments)] @@ -174,25 +145,29 @@ pub fn generate_compress_instruction_entrypoint( // ============================================================================= pub fn generate_compress_accounts_struct(variant: InstructionVariant) -> Result { + // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented match variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => Ok(syn::parse_quote! { - #[derive(Accounts)] - pub struct CompressAccountsIdempotent<'info> { - #[account(mut)] - pub fee_payer: Signer<'info>, - /// CHECK: Checked by SDK - pub config: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub rent_sponsor: AccountInfo<'info>, - /// CHECK: Checked by SDK - #[account(mut)] - pub compression_authority: AccountInfo<'info>, - } - }), + InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { + unreachable!("compress_accounts_struct only supports Mixed variant") + } + InstructionVariant::Mixed => {} } + + Ok(syn::parse_quote! { + #[derive(Accounts)] + pub struct CompressAccountsIdempotent<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + /// CHECK: Checked by SDK + pub config: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + /// CHECK: Checked by SDK + #[account(mut)] + pub compression_authority: AccountInfo<'info>, + } + }) } // ============================================================================= @@ -219,19 +194,23 @@ pub fn validate_compressed_account_sizes(account_types: &[Ident]) -> Result Result { - let base_errors = quote! { + // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented + match variant { + InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { + unreachable!("generate_error_codes only supports Mixed variant") + } + InstructionVariant::Mixed => {} + } + + Ok(quote! { + #[error_code] + pub enum RentFreeInstructionError { #[msg("Rent sponsor mismatch")] InvalidRentSponsor, - #[msg("Missing seed account")] - MissingSeedAccount, - #[msg("Seed value does not match account data")] - SeedMismatch, - }; - - let variant_specific_errors = match variant { - InstructionVariant::PdaOnly => unreachable!(), - InstructionVariant::TokenOnly => unreachable!(), - InstructionVariant::Mixed => quote! { + #[msg("Missing seed account")] + MissingSeedAccount, + #[msg("Seed value does not match account data")] + SeedMismatch, #[msg("Not implemented")] CTokenDecompressionNotImplemented, #[msg("Not implemented")] @@ -240,14 +219,6 @@ pub fn generate_error_codes(variant: InstructionVariant) -> Result TokenCompressionNotImplemented, #[msg("Not implemented")] PdaCompressionNotImplemented, - }, - }; - - Ok(quote! { - #[error_code] - pub enum RentFreeInstructionError { - #base_errors - #variant_specific_errors } }) } diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index e2568f2de5..5f01b079b8 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -4,7 +4,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; -use super::parsing::{InstructionDataSpec, InstructionVariant, SeedElement, TokenSeedSpec}; +use super::parsing::{InstructionVariant, SeedElement, TokenSeedSpec}; use super::variant_enum::PdaCtxSeedInfo; // ============================================================================= @@ -12,7 +12,6 @@ use super::variant_enum::PdaCtxSeedInfo; // ============================================================================= pub fn generate_decompress_context_impl( - _variant: InstructionVariant, pda_ctx_seeds: Vec, token_variant_ident: Ident, ) -> Result { @@ -38,10 +37,7 @@ pub fn generate_decompress_context_impl( // DECOMPRESS PROCESSOR // ============================================================================= -pub fn generate_process_decompress_accounts_idempotent( - _variant: InstructionVariant, - _instruction_data: &[InstructionDataSpec], -) -> Result { +pub fn generate_process_decompress_accounts_idempotent() -> Result { Ok(syn::parse_quote! { #[inline(never)] pub fn process_decompress_accounts_idempotent<'info>( @@ -70,10 +66,7 @@ pub fn generate_process_decompress_accounts_idempotent( // DECOMPRESS INSTRUCTION ENTRYPOINT // ============================================================================= -pub fn generate_decompress_instruction_entrypoint( - _variant: InstructionVariant, - _instruction_data: &[InstructionDataSpec], -) -> Result { +pub fn generate_decompress_instruction_entrypoint() -> Result { Ok(syn::parse_quote! { #[inline(never)] pub fn decompress_accounts_idempotent<'info>( @@ -99,10 +92,17 @@ pub fn generate_decompress_instruction_entrypoint( #[inline(never)] pub fn generate_decompress_accounts_struct( - required_accounts: &[String], variant: InstructionVariant, ) -> Result { - let mut account_fields = vec![ + // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented + match variant { + InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { + unreachable!("decompress_accounts_struct only supports Mixed variant") + } + InstructionVariant::Mixed => {} + } + + let account_fields = vec![ quote! { #[account(mut)] pub fee_payer: Signer<'info> @@ -111,80 +111,32 @@ pub fn generate_decompress_accounts_struct( /// CHECK: Checked by SDK pub config: AccountInfo<'info> }, + quote! { + /// CHECK: anyone can pay + #[account(mut)] + pub rent_sponsor: UncheckedAccount<'info> + }, + quote! { + /// CHECK: optional - only needed if decompressing tokens + #[account(mut)] + pub ctoken_rent_sponsor: Option> + }, + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] + pub light_token_program: Option> + }, + quote! { + /// CHECK: + #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] + pub ctoken_cpi_authority: Option> + }, + quote! { + /// CHECK: Checked by SDK + pub ctoken_config: Option> + }, ]; - match variant { - InstructionVariant::PdaOnly => { - unreachable!() - } - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: anyone can pay - #[account(mut)] - pub rent_sponsor: UncheckedAccount<'info> - }, - quote! { - /// CHECK: optional - only needed if decompressing tokens - #[account(mut)] - pub ctoken_rent_sponsor: Option> - }, - ]); - } - } - - match variant { - InstructionVariant::TokenOnly => { - unreachable!() - } - InstructionVariant::Mixed => { - account_fields.extend(vec![ - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("cTokenmWW8bLPjZEBAUgYy3zKxQZW6VKi7bqNFEVv3m"))] - pub light_token_program: Option> - }, - quote! { - /// CHECK: - #[account(address = solana_pubkey::pubkey!("GXtd2izAiMJPwMEjfgTRH3d7k9mjn4Jq3JrWFv9gySYy"))] - pub ctoken_cpi_authority: Option> - }, - quote! { - /// CHECK: Checked by SDK - pub ctoken_config: Option> - }, - ]); - } - InstructionVariant::PdaOnly => { - unreachable!() - } - } - - let standard_fields = [ - "fee_payer", - "rent_sponsor", - "ctoken_rent_sponsor", - "config", - "light_token_program", - "ctoken_cpi_authority", - "ctoken_config", - ]; - - for account_name in required_accounts { - if !standard_fields.contains(&account_name.as_str()) { - let account_ident = syn::Ident::new(account_name, proc_macro2::Span::call_site()); - // Mark seed accounts as writable to support CPI calls that may need them writable - account_fields.push(quote! { - /// CHECK: optional seed account - may be used in CPIs - #[account(mut)] - pub #account_ident: Option> - }); - } - } - let struct_def = quote! { #[derive(Accounts)] pub struct DecompressAccountsIdempotent<'info> { @@ -199,12 +151,81 @@ pub fn generate_decompress_accounts_struct( // PDA SEED DERIVATION // ============================================================================= +/// Recursively rewrite PDA seed expressions: +/// - `data.` -> `self.` (from unpacked compressed account data) +/// - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) +/// - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) +fn map_pda_expr_to_ctx_seeds( + expr: &syn::Expr, + ctx_field_names: &std::collections::HashSet, +) -> syn::Expr { + match expr { + syn::Expr::Field(field_expr) => { + let syn::Member::Named(field_name) = &field_expr.member else { + return expr.clone(); + }; + + // Check for ctx.accounts.field -> ctx_seeds.field + if let syn::Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" { + if let syn::Expr::Path(path) = &*nested_field.base { + if path.path.segments.first().is_some_and(|s| s.ident == "ctx") { + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } + } + } + + // Check for data.field or ctx.field + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + return syn::parse_quote! { self.#field_name }; + } + if segment.ident == "ctx" && ctx_field_names.contains(&field_name.to_string()) { + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } + expr.clone() + } + syn::Expr::MethodCall(method_call) => { + let mut new_method_call = method_call.clone(); + new_method_call.receiver = + Box::new(map_pda_expr_to_ctx_seeds(&method_call.receiver, ctx_field_names)); + new_method_call.args = method_call + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::MethodCall(new_method_call) + } + syn::Expr::Call(call_expr) => { + let mut new_call_expr = call_expr.clone(); + new_call_expr.args = call_expr + .args + .iter() + .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) + .collect(); + syn::Expr::Call(new_call_expr) + } + syn::Expr::Reference(ref_expr) => { + let mut new_ref_expr = ref_expr.clone(); + new_ref_expr.expr = + Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); + syn::Expr::Reference(new_ref_expr) + } + _ => expr.clone(), + } +} + /// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. /// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) #[inline(never)] -pub fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( +fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( spec: &TokenSeedSpec, - _instruction_data: &[InstructionDataSpec], ctx_seed_fields: &[syn::Ident], ) -> Result { let mut bindings: Vec = Vec::new(); @@ -214,82 +235,6 @@ pub fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( let ctx_field_names: std::collections::HashSet = ctx_seed_fields.iter().map(|f| f.to_string()).collect(); - // Recursively rewrite expressions: - // - `data.` -> `self.` (from unpacked compressed account data) - // - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) - // - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) - fn map_pda_expr_to_ctx_seeds( - expr: &syn::Expr, - ctx_field_names: &std::collections::HashSet, - ) -> syn::Expr { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Handle nested field access: ctx.accounts.field_name -> ctx_seeds.field_name - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - // ctx.accounts.field -> ctx_seeds.field (direct Pubkey) - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - } - } - } - // Handle direct field access - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - // data.field -> self.field (from unpacked compressed account data) - return syn::parse_quote! { self.#field_name }; - } else if segment.ident == "ctx" { - let field_str = field_name.to_string(); - if ctx_field_names.contains(&field_str) { - // ctx.field -> ctx_seeds.field (direct Pubkey) - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - } - } - expr.clone() - } - syn::Expr::MethodCall(method_call) => { - let mut new_method_call = method_call.clone(); - new_method_call.receiver = Box::new(map_pda_expr_to_ctx_seeds( - &method_call.receiver, - ctx_field_names, - )); - new_method_call.args = method_call - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::MethodCall(new_method_call) - } - syn::Expr::Call(call_expr) => { - let mut new_call_expr = call_expr.clone(); - new_call_expr.args = call_expr - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::Call(new_call_expr) - } - syn::Expr::Reference(ref_expr) => { - let mut new_ref_expr = ref_expr.clone(); - new_ref_expr.expr = - Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); - syn::Expr::Reference(new_ref_expr) - } - _ => expr.clone(), - } - } - for (i, seed) in spec.seeds.iter().enumerate() { match seed { SeedElement::Literal(lit) => { @@ -354,70 +299,73 @@ pub fn generate_pda_seed_provider_impls( account_types: &[Ident], pda_ctx_seeds: &[PdaCtxSeedInfo], pda_seeds: &Option>, - instruction_data: &[InstructionDataSpec], ) -> Result> { - account_types - .iter() - .zip(pda_ctx_seeds.iter()) - .map(|(name, ctx_info)| { - let name_str = name.to_string(); - let spec = if let Some(ref pda_seed_specs) = pda_seeds { - pda_seed_specs - .iter() - .find(|s| s.variant == name_str) - .ok_or_else(|| { - super::parsing::macro_error!( - name, - "No seed specification for account type '{}'", - name_str - ) - })? - } else { - return Err(super::parsing::macro_error!( + 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" + ) + })?; + + let mut results = Vec::with_capacity(account_types.len()); + + for (name, ctx_info) in account_types.iter().zip(pda_ctx_seeds.iter()) { + let name_str = name.to_string(); + let spec = pda_seed_specs + .iter() + .find(|s| s.variant == name_str) + .ok_or_else(|| { + super::parsing::macro_error!( name, - "No seed specifications provided" - )); - }; - - let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); - let ctx_fields = &ctx_info.ctx_seed_fields; - let ctx_fields_decl: Vec<_> = ctx_fields - .iter() - .map(|field| { - quote! { pub #field: solana_pubkey::Pubkey } - }) - .collect(); + "No seed specification for account type '{}'", + name_str + ) + })?; + + let ctx_seeds_struct_name = format_ident!("{}CtxSeeds", name); + let ctx_fields = &ctx_info.ctx_seed_fields; + let ctx_fields_decl: Vec<_> = ctx_fields + .iter() + .map(|field| { + quote! { pub #field: solana_pubkey::Pubkey } + }) + .collect(); - let ctx_seeds_struct = if ctx_fields.is_empty() { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name; + let ctx_seeds_struct = if ctx_fields.is_empty() { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name; + } + } else { + quote! { + #[derive(Default)] + pub struct #ctx_seeds_struct_name { + #(#ctx_fields_decl),* } - } else { - quote! { - #[derive(Default)] - pub struct #ctx_seeds_struct_name { - #(#ctx_fields_decl),* - } + } + }; + + let seed_derivation = + generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, ctx_fields)?; + + results.push(quote! { + #ctx_seeds_struct + + impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { + fn derive_pda_seeds_with_accounts( + &self, + program_id: &solana_pubkey::Pubkey, + ctx_seeds: &#ctx_seeds_struct_name, + _seed_params: &(), + ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { + #seed_derivation } - }; + } + }); + } - let seed_derivation = - generate_pda_seed_derivation_for_trait_with_ctx_seeds(spec, instruction_data, ctx_fields)?; - Ok(quote! { - #ctx_seeds_struct - - impl light_sdk::compressible::PdaSeedDerivation<#ctx_seeds_struct_name, ()> for #name { - fn derive_pda_seeds_with_accounts( - &self, - program_id: &solana_pubkey::Pubkey, - ctx_seeds: &#ctx_seeds_struct_name, - _seed_params: &(), - ) -> std::result::Result<(Vec>, solana_pubkey::Pubkey), solana_program_error::ProgramError> { - #seed_derivation - } - } - }) - }) - .collect() + Ok(results) } diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 825c568057..dc84dc0b2f 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -31,12 +31,12 @@ use super::decompress::{ use super::variant_enum::PdaCtxSeedInfo; // ============================================================================= -// MAIN ASSEMBLY FUNCTION +// MAIN CODEGEN // ============================================================================= -/// Generate all code from extracted seeds (shared logic with add_compressible_instructions) +/// Orchestrates all code generation for the rentfree module. #[inline(never)] -fn generate_from_extracted_seeds( +fn codegen( module: &mut ItemMod, account_types: Vec, pda_seeds: Option>, @@ -48,7 +48,7 @@ fn generate_from_extracted_seeds( let content = module.content.as_mut().unwrap(); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { - super::seed_providers::generate_ctoken_account_variant_enum(token_seed_specs)? + super::seed_codegen::generate_ctoken_account_variant_enum(token_seed_specs)? } else { crate::rentfree::traits::utils::generate_empty_ctoken_enum() } @@ -175,10 +175,10 @@ fn generate_from_extracted_seeds( }; let error_codes = generate_error_codes(instruction_variant)?; - let decompress_accounts = generate_decompress_accounts_struct(&[], instruction_variant)?; + let decompress_accounts = generate_decompress_accounts_struct(instruction_variant)?; let pda_seed_provider_impls = - generate_pda_seed_provider_impls(&account_types, &pda_ctx_seeds, &pda_seeds, &instruction_data)?; + generate_pda_seed_provider_impls(&account_types, &pda_ctx_seeds, &pda_seeds)?; let trait_impls: syn::ItemMod = syn::parse_quote! { mod __trait_impls { @@ -195,20 +195,16 @@ fn generate_from_extracted_seeds( let token_variant_name = format_ident!("TokenAccountVariant"); let decompress_context_impl = generate_decompress_context_impl( - instruction_variant, pda_ctx_seeds.clone(), token_variant_name, )?; - let decompress_processor_fn = - generate_process_decompress_accounts_idempotent(instruction_variant, &instruction_data)?; - let decompress_instruction = - generate_decompress_instruction_entrypoint(instruction_variant, &instruction_data)?; + let decompress_processor_fn = generate_process_decompress_accounts_idempotent()?; + let decompress_instruction = generate_decompress_instruction_entrypoint()?; let compress_accounts = generate_compress_accounts_struct(instruction_variant)?; - let compress_context_impl = - generate_compress_context_impl(instruction_variant, account_types.clone())?; - let compress_processor_fn = generate_process_compress_accounts_idempotent(instruction_variant)?; - let compress_instruction = generate_compress_instruction_entrypoint(instruction_variant)?; + let compress_context_impl = generate_compress_context_impl(account_types.clone())?; + let compress_processor_fn = generate_process_compress_accounts_idempotent()?; + let compress_instruction = generate_compress_instruction_entrypoint()?; let module_tokens = quote! { mod __processor_functions { @@ -300,7 +296,7 @@ fn generate_from_extracted_seeds( } }; - let client_functions = super::seed_providers::generate_client_seed_functions( + let client_functions = super::seed_codegen::generate_client_seed_functions( &account_types, &pda_seeds, &token_seeds, @@ -347,7 +343,7 @@ fn generate_from_extracted_seeds( if let Some(ref seeds) = token_seeds { if !seeds.is_empty() { let impl_code = - super::seed_providers::generate_ctoken_seed_provider_implementation(seeds)?; + super::seed_codegen::generate_ctoken_seed_provider_implementation(seeds)?; let ctoken_impl: syn::ItemImpl = syn::parse2(impl_code)?; content.1.push(Item::Impl(ctoken_impl)); } @@ -396,7 +392,7 @@ fn generate_from_extracted_seeds( #[inline(never)] pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result { use super::crate_context::CrateContext; - use crate::rentfree::traits::anchor_seeds::{ + use crate::rentfree::traits::seed_extraction::{ extract_from_accounts_struct, get_data_fields, ExtractedSeedSpec, ExtractedTokenSpec, }; @@ -446,7 +442,7 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< } } - // Convert extracted specs to the format expected by generate_from_extracted_seeds + // Convert extracted specs to the format expected by codegen let mut found_pda_seeds: Vec = Vec::new(); let mut found_data_fields: Vec = Vec::new(); let mut account_types: Vec = Vec::new(); @@ -512,7 +508,7 @@ pub fn rentfree_program_impl(_args: TokenStream, mut module: ItemMod) -> Result< }; // Use the shared generation logic - generate_from_extracted_seeds( + codegen( &mut module, account_types, pda_seeds, diff --git a/sdk-libs/macros/src/rentfree/program/mod.rs b/sdk-libs/macros/src/rentfree/program/mod.rs index ce3085bf9d..76ed606e9d 100644 --- a/sdk-libs/macros/src/rentfree/program/mod.rs +++ b/sdk-libs/macros/src/rentfree/program/mod.rs @@ -10,7 +10,7 @@ mod parsing; mod decompress; mod compress; pub mod instructions; -pub mod seed_providers; +pub mod seed_codegen; pub mod variant_enum; pub use instructions::rentfree_program_impl; diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index 729625b300..cd34117a6d 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -351,9 +351,9 @@ pub fn extract_data_seed_fields( /// Convert ClassifiedSeed to SeedElement (Punctuated) pub fn convert_classified_to_seed_elements( - seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], + seeds: &[crate::rentfree::traits::seed_extraction::ClassifiedSeed], ) -> Punctuated { - use crate::rentfree::traits::anchor_seeds::ClassifiedSeed; + use crate::rentfree::traits::seed_extraction::ClassifiedSeed; let mut result = Punctuated::new(); for seed in seeds { @@ -406,7 +406,7 @@ pub fn convert_classified_to_seed_elements( } pub fn convert_classified_to_seed_elements_vec( - seeds: &[crate::rentfree::traits::anchor_seeds::ClassifiedSeed], + seeds: &[crate::rentfree::traits::seed_extraction::ClassifiedSeed], ) -> Vec { convert_classified_to_seed_elements(seeds) .into_iter() diff --git a/sdk-libs/macros/src/rentfree/program/seed_providers.rs b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs similarity index 97% rename from sdk-libs/macros/src/rentfree/program/seed_providers.rs rename to sdk-libs/macros/src/rentfree/program/seed_codegen.rs index bc91976d82..8acde054fc 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_providers.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs @@ -344,8 +344,10 @@ pub fn generate_ctoken_seed_provider_implementation( // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let auth_seed_refs: Vec = - authority_seeds.iter().map(seed_element_to_ref_expr).collect(); + let auth_seed_refs: Vec = authority_seeds + .iter() + .map(seed_element_to_ref_expr) + .collect(); let authority_arm = quote! { #pattern => { @@ -463,7 +465,8 @@ pub fn generate_client_seed_functions( analyze_seed_spec_for_client(spec, instruction_data)?; let seed_count = seed_expressions.len(); - let fn_body = generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); + let fn_body = + generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -484,7 +487,8 @@ pub fn generate_client_seed_functions( analyze_seed_spec_for_client(spec, instruction_data)?; let seed_count = seed_expressions.len(); - let fn_body = generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); + let fn_body = + generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -517,12 +521,20 @@ pub fn generate_client_seed_functions( let (fn_params, fn_body) = if auth_parameters.is_empty() { ( quote! { _program_id: &solana_pubkey::Pubkey }, - generate_seed_fn_body(auth_seed_count, &auth_seed_expressions, quote! { _program_id }), + generate_seed_fn_body( + auth_seed_count, + &auth_seed_expressions, + quote! { _program_id }, + ), ) } else { ( quote! { #(#auth_parameters),* }, - generate_seed_fn_body(auth_seed_count, &auth_seed_expressions, quote! { &crate::ID }), + generate_seed_fn_body( + auth_seed_count, + &auth_seed_expressions, + quote! { &crate::ID }, + ), ) }; let authority_function = quote! { @@ -583,7 +595,10 @@ fn analyze_seed_spec_for_client( } else { return Err(syn::Error::new_spanned( field_name, - format!("data.{} used in seeds but no type specified", field_name), + format!( + "data.{} used in seeds but no type specified", + field_name + ), )); } } diff --git a/sdk-libs/macros/src/rentfree/traits/mod.rs b/sdk-libs/macros/src/rentfree/traits/mod.rs index 8669687ab9..643cf6e251 100644 --- a/sdk-libs/macros/src/rentfree/traits/mod.rs +++ b/sdk-libs/macros/src/rentfree/traits/mod.rs @@ -1,14 +1,14 @@ //! Shared trait derive macros for compressible accounts. //! //! This module provides: -//! - `anchor_seeds` - Seed extraction from Anchor account attributes +//! - `seed_extraction` - Seed extraction from Anchor account attributes //! - `decompress_context` - Decompression context utilities //! - `light_compressible` - Combined RentFreeAccount derive macro //! - `pack_unpack` - Pack/Unpack trait implementations //! - `traits` - HasCompressionInfo, Compressible, CompressAs traits //! - `utils` - Shared utility functions -pub mod anchor_seeds; +pub mod seed_extraction; pub mod decompress_context; pub mod light_compressible; pub mod pack_unpack; diff --git a/sdk-libs/macros/src/rentfree/traits/anchor_seeds.rs b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs similarity index 100% rename from sdk-libs/macros/src/rentfree/traits/anchor_seeds.rs rename to sdk-libs/macros/src/rentfree/traits/seed_extraction.rs From da976f968a63539e23edf7b3dc1fa9621ac90ba7 Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 04:28:52 +0000 Subject: [PATCH 8/9] cleanup --- Cargo.toml | 2 +- sdk-libs/macros/src/rentfree/mod.rs | 2 + .../macros/src/rentfree/program/decompress.rs | 80 +--- .../src/rentfree/program/expr_traversal.rs | 76 ++++ .../src/rentfree/program/instructions.rs | 2 +- sdk-libs/macros/src/rentfree/program/mod.rs | 3 + .../macros/src/rentfree/program/parsing.rs | 128 ++---- .../src/rentfree/program/seed_codegen.rs | 380 +----------------- .../macros/src/rentfree/program/seed_utils.rs | 123 ++++++ .../src/rentfree/program/variant_enum.rs | 182 +++++++++ sdk-libs/macros/src/rentfree/shared_utils.rs | 82 ++++ .../src/rentfree/traits/seed_extraction.rs | 51 +-- 12 files changed, 520 insertions(+), 591 deletions(-) create mode 100644 sdk-libs/macros/src/rentfree/program/expr_traversal.rs create mode 100644 sdk-libs/macros/src/rentfree/program/seed_utils.rs create mode 100644 sdk-libs/macros/src/rentfree/shared_utils.rs diff --git a/Cargo.toml b/Cargo.toml index 32a0953ac4..f1c4aae7f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,7 @@ serde_json = "1.0" # Macro helpers proc-macro2 = "1.0" quote = "1.0" -syn = { version = "2.0", features = ["visit-mut", "full"] } +syn = { version = "2.0", features = ["visit", "visit-mut", "full"] } darling = "0.21" # Async ecosystem diff --git a/sdk-libs/macros/src/rentfree/mod.rs b/sdk-libs/macros/src/rentfree/mod.rs index 67d141cc2f..e10bea0ab6 100644 --- a/sdk-libs/macros/src/rentfree/mod.rs +++ b/sdk-libs/macros/src/rentfree/mod.rs @@ -4,7 +4,9 @@ //! - `program/` - `#[rentfree_program]` attribute macro for program-level auto-discovery //! - `accounts/` - `#[derive(RentFree)]` derive macro for Accounts structs //! - `traits/` - Shared trait derive macros (Compressible, Pack, HasCompressionInfo, etc.) +//! - `shared_utils` - Common utilities (constant detection, identifier extraction) pub mod accounts; pub mod program; +pub mod shared_utils; pub mod traits; diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index 5f01b079b8..16189fe0be 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -4,8 +4,11 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; +use super::expr_traversal::transform_expr_for_ctx_seeds; use super::parsing::{InstructionVariant, SeedElement, TokenSeedSpec}; +use super::seed_utils::ctx_fields_to_set; use super::variant_enum::PdaCtxSeedInfo; +use crate::rentfree::shared_utils::is_constant_identifier; // ============================================================================= // DECOMPRESS CONTEXT IMPL @@ -151,76 +154,6 @@ pub fn generate_decompress_accounts_struct( // PDA SEED DERIVATION // ============================================================================= -/// Recursively rewrite PDA seed expressions: -/// - `data.` -> `self.` (from unpacked compressed account data) -/// - `ctx.accounts.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) -/// - `ctx.` -> `ctx_seeds.` (direct Pubkey on CtxSeeds struct) -fn map_pda_expr_to_ctx_seeds( - expr: &syn::Expr, - ctx_field_names: &std::collections::HashSet, -) -> syn::Expr { - match expr { - syn::Expr::Field(field_expr) => { - let syn::Member::Named(field_name) = &field_expr.member else { - return expr.clone(); - }; - - // Check for ctx.accounts.field -> ctx_seeds.field - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if path.path.segments.first().is_some_and(|s| s.ident == "ctx") { - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - } - } - - // Check for data.field or ctx.field - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - return syn::parse_quote! { self.#field_name }; - } - if segment.ident == "ctx" && ctx_field_names.contains(&field_name.to_string()) { - return syn::parse_quote! { ctx_seeds.#field_name }; - } - } - } - expr.clone() - } - syn::Expr::MethodCall(method_call) => { - let mut new_method_call = method_call.clone(); - new_method_call.receiver = - Box::new(map_pda_expr_to_ctx_seeds(&method_call.receiver, ctx_field_names)); - new_method_call.args = method_call - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::MethodCall(new_method_call) - } - syn::Expr::Call(call_expr) => { - let mut new_call_expr = call_expr.clone(); - new_call_expr.args = call_expr - .args - .iter() - .map(|a| map_pda_expr_to_ctx_seeds(a, ctx_field_names)) - .collect(); - syn::Expr::Call(new_call_expr) - } - syn::Expr::Reference(ref_expr) => { - let mut new_ref_expr = ref_expr.clone(); - new_ref_expr.expr = - Box::new(map_pda_expr_to_ctx_seeds(&ref_expr.expr, ctx_field_names)); - syn::Expr::Reference(new_ref_expr) - } - _ => expr.clone(), - } -} - /// Generate PDA seed derivation that uses CtxSeeds struct instead of DecompressAccountsIdempotent. /// Maps ctx.field -> ctx_seeds.field (direct Pubkey access, no Option unwrapping needed) #[inline(never)] @@ -232,8 +165,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( let mut seed_refs = Vec::new(); // Convert ctx_seed_fields to a set for quick lookup - let ctx_field_names: std::collections::HashSet = - ctx_seed_fields.iter().map(|f| f.to_string()).collect(); + let ctx_field_names = ctx_fields_to_set(ctx_seed_fields); for (i, seed) in spec.seeds.iter().enumerate() { match seed { @@ -255,7 +187,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( if let syn::Expr::Path(path_expr) = &**expr { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); - if ident_str.chars().all(|c| c.is_uppercase() || c == '_') { + if is_constant_identifier(&ident_str) { seed_refs.push( quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }, ); @@ -266,7 +198,7 @@ fn generate_pda_seed_derivation_for_trait_with_ctx_seeds( let binding_name = syn::Ident::new(&format!("seed_{}", i), proc_macro2::Span::call_site()); - let mapped_expr = map_pda_expr_to_ctx_seeds(expr, &ctx_field_names); + let mapped_expr = transform_expr_for_ctx_seeds(expr, &ctx_field_names); bindings.push(quote! { let #binding_name = #mapped_expr; }); diff --git a/sdk-libs/macros/src/rentfree/program/expr_traversal.rs b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs new file mode 100644 index 0000000000..54b1d621e3 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs @@ -0,0 +1,76 @@ +//! AST expression transformation utilities. +//! +//! This module provides expression transformation for converting field access patterns +//! used in seed derivation code generation. + +use std::collections::HashSet; +use syn::Expr; + +use crate::rentfree::shared_utils::is_base_path; + +// ============================================================================= +// EXPRESSION TRANSFORMER +// ============================================================================= + +/// Transform expressions by replacing field access patterns. +/// +/// Used for converting: +/// - `data.field` -> `self.field` +/// - `ctx.field` -> `ctx_seeds.field` (if field is in ctx_field_names) +/// - `ctx.accounts.field` -> `ctx_seeds.field` +pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet) -> Expr { + match expr { + Expr::Field(field_expr) => { + let Some(syn::Member::Named(field_name)) = Some(&field_expr.member) else { + return expr.clone(); + }; + + // Check for ctx.accounts.field -> ctx_seeds.field + if let Expr::Field(nested_field) = &*field_expr.base { + if let syn::Member::Named(base_name) = &nested_field.member { + if base_name == "accounts" && is_base_path(&nested_field.base, "ctx") { + return syn::parse_quote! { ctx_seeds.#field_name }; + } + } + } + + // Check for data.field -> self.field or ctx.field -> ctx_seeds.field + if is_base_path(&field_expr.base, "data") { + return syn::parse_quote! { self.#field_name }; + } + if is_base_path(&field_expr.base, "ctx") + && ctx_field_names.contains(&field_name.to_string()) + { + return syn::parse_quote! { ctx_seeds.#field_name }; + } + + expr.clone() + } + Expr::MethodCall(method_call) => { + let mut new_call = method_call.clone(); + new_call.receiver = + Box::new(transform_expr_for_ctx_seeds(&method_call.receiver, ctx_field_names)); + new_call.args = method_call + .args + .iter() + .map(|a| transform_expr_for_ctx_seeds(a, ctx_field_names)) + .collect(); + Expr::MethodCall(new_call) + } + Expr::Call(call_expr) => { + let mut new_call = call_expr.clone(); + new_call.args = call_expr + .args + .iter() + .map(|a| transform_expr_for_ctx_seeds(a, ctx_field_names)) + .collect(); + Expr::Call(new_call) + } + Expr::Reference(ref_expr) => { + let mut new_ref = ref_expr.clone(); + new_ref.expr = Box::new(transform_expr_for_ctx_seeds(&ref_expr.expr, ctx_field_names)); + Expr::Reference(new_ref) + } + _ => expr.clone(), + } +} diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index dc84dc0b2f..99a5298ed5 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -48,7 +48,7 @@ fn codegen( let content = module.content.as_mut().unwrap(); let ctoken_enum = if let Some(ref token_seed_specs) = token_seeds { if !token_seed_specs.is_empty() { - super::seed_codegen::generate_ctoken_account_variant_enum(token_seed_specs)? + super::variant_enum::generate_ctoken_account_variant_enum(token_seed_specs)? } else { crate::rentfree::traits::utils::generate_empty_ctoken_enum() } diff --git a/sdk-libs/macros/src/rentfree/program/mod.rs b/sdk-libs/macros/src/rentfree/program/mod.rs index 76ed606e9d..77dc6cac88 100644 --- a/sdk-libs/macros/src/rentfree/program/mod.rs +++ b/sdk-libs/macros/src/rentfree/program/mod.rs @@ -6,11 +6,14 @@ //! - Generates all necessary types, enums, and instruction handlers pub mod crate_context; +pub mod expr_traversal; mod parsing; mod decompress; mod compress; pub mod instructions; pub mod seed_codegen; +pub mod seed_utils; pub mod variant_enum; +pub mod visitors; pub use instructions::rentfree_program_impl; diff --git a/sdk-libs/macros/src/rentfree/program/parsing.rs b/sdk-libs/macros/src/rentfree/program/parsing.rs index cd34117a6d..6100fa3d95 100644 --- a/sdk-libs/macros/src/rentfree/program/parsing.rs +++ b/sdk-libs/macros/src/rentfree/program/parsing.rs @@ -7,6 +7,8 @@ use syn::{ Expr, Ident, ItemFn, LitStr, Result, Token, }; +use super::visitors::FieldExtractor; + // ============================================================================= // MACRO ERROR HELPER // ============================================================================= @@ -223,126 +225,50 @@ impl Parse for InstructionDataSpec { // EXPRESSION ANALYSIS // ============================================================================= -/// Recursively extract field names from expressions matching `base.field` or `base.nested.field`. -/// Handles nested expressions like function calls: max_key(&ctx.user.key(), &ctx.authority.key()) -/// -/// Parameters: -/// - `base_ident`: The base identifier to match (e.g., "ctx" or "data") -/// - `nested_prefix`: Optional nested field name (e.g., "accounts" for ctx.accounts.XXX) -fn extract_fields_by_base( - expr: &syn::Expr, - base_ident: &str, - nested_prefix: Option<&str>, - fields: &mut Vec, -) { - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for base.XXX pattern (direct field access) - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == base_ident { - fields.push(field_name.clone()); - return; - } - } - } - // Check for base.nested.XXX pattern (nested field access) if nested_prefix is provided - if let Some(nested) = nested_prefix { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == nested { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == base_ident { - fields.push(field_name.clone()); - return; - } - } - } - } - } - } - } - } - // Recurse into base expression - extract_fields_by_base(&field_expr.base, base_ident, nested_prefix, fields); - } - syn::Expr::MethodCall(method) => { - // Recurse into receiver and args - extract_fields_by_base(&method.receiver, base_ident, nested_prefix, fields); - for arg in &method.args { - extract_fields_by_base(arg, base_ident, nested_prefix, fields); - } - } - syn::Expr::Call(call) => { - // Recurse into function args - for arg in &call.args { - extract_fields_by_base(arg, base_ident, nested_prefix, fields); - } - } - syn::Expr::Reference(ref_expr) => { - extract_fields_by_base(&ref_expr.expr, base_ident, nested_prefix, fields); - } - syn::Expr::Paren(paren) => { - extract_fields_by_base(&paren.expr, base_ident, nested_prefix, fields); - } - _ => {} - } -} - -/// Recursively extract all ctx.XXX or ctx.accounts.XXX field names from an expression. -fn extract_ctx_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { - extract_fields_by_base(expr, "ctx", Some("accounts"), fields); -} - -/// Extract ctx.XXX or ctx.accounts.XXX field names from a seed element. -fn extract_ctx_account_fields(seed: &SeedElement) -> Vec { - let mut fields = Vec::new(); - if let SeedElement::Expression(expr) = seed { - extract_ctx_fields_from_expr(expr, &mut fields); - } - fields -} - -/// Extract all ctx.accounts.XXX field names from a list of seed elements. -/// Deduplicates the fields. +/// Extract all ctx.accounts.XXX and ctx.XXX field names from a list of seed elements. +/// Deduplicates the fields using visitor-based extraction. pub fn extract_ctx_seed_fields( seeds: &syn::punctuated::Punctuated, ) -> Vec { let mut all_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for seed in seeds { - all_fields.extend(extract_ctx_account_fields(seed)); + if let SeedElement::Expression(expr) = seed { + let fields = FieldExtractor::ctx_fields(&[]).extract(expr); + for field in fields { + let name = field.to_string(); + if seen.insert(name) { + all_fields.push(field); + } + } + } } - // Deduplicate while preserving order - let mut seen = std::collections::HashSet::new(); - all_fields - .into_iter() - .filter(|f| seen.insert(f.to_string())) - .collect() -} -/// Extract data.XXX field names from an expression recursively. -fn extract_data_fields_from_expr(expr: &syn::Expr, fields: &mut Vec) { - extract_fields_by_base(expr, "data", None, fields); + all_fields } /// Extract all data.XXX field names from a list of seed elements. +/// Deduplicates the fields using visitor-based extraction. pub fn extract_data_seed_fields( seeds: &syn::punctuated::Punctuated, ) -> Vec { let mut all_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + for seed in seeds { if let SeedElement::Expression(expr) = seed { - extract_data_fields_from_expr(expr, &mut all_fields); + let fields = FieldExtractor::data_fields().extract(expr); + for field in fields { + let name = field.to_string(); + if seen.insert(name) { + all_fields.push(field); + } + } } } - // Deduplicate while preserving order - let mut seen = std::collections::HashSet::new(); + all_fields - .into_iter() - .filter(|f| seen.insert(f.to_string())) - .collect() } // ============================================================================= diff --git a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs index 8acde054fc..d7a0d94cf3 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs @@ -5,6 +5,9 @@ use quote::{format_ident, quote}; use syn::{Ident, Result}; use super::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; +use super::seed_utils::{generate_seed_derivation_body, seed_element_to_ref_expr, SeedConversionConfig}; +use super::variant_enum::extract_ctx_fields_from_token_spec; +use crate::rentfree::shared_utils::is_constant_identifier; use crate::rentfree::traits::utils::is_pubkey_type; /// Helper to add a Pubkey parameter and its .as_ref() expression. @@ -19,293 +22,6 @@ fn push_pubkey_param( expressions.push(quote! { #field_name.as_ref() }); } -/// Extract ctx.* field names from seed elements (both token seeds and authority seeds) -fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { - let mut ctx_fields = Vec::new(); - let mut seen = std::collections::HashSet::new(); - - // Helper to extract ctx.* from a SeedElement - fn extract_from_seed( - seed: &SeedElement, - ctx_fields: &mut Vec, - seen: &mut std::collections::HashSet, - ) { - if let SeedElement::Expression(expr) = seed { - extract_ctx_from_expr(expr, ctx_fields, seen); - } - } - - fn extract_ctx_from_expr( - expr: &syn::Expr, - ctx_fields: &mut Vec, - seen: &mut std::collections::HashSet, - ) { - if let syn::Expr::Field(field_expr) = expr { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for ctx.accounts.field pattern - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let field_name_str = field_name.to_string(); - // Skip standard fields - if !matches!( - field_name_str.as_str(), - "fee_payer" - | "rent_sponsor" - | "config" - | "compression_authority" - ) && seen.insert(field_name_str) - { - ctx_fields.push(field_name.clone()); - } - } - } - } - } - } - } - // Check for ctx.field pattern (shorthand) - else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - let field_name_str = field_name.to_string(); - if !matches!( - field_name_str.as_str(), - "fee_payer" | "rent_sponsor" | "config" | "compression_authority" - ) && seen.insert(field_name_str) - { - ctx_fields.push(field_name.clone()); - } - } - } - } - } - } - // Recursively check method calls like max_key(&ctx.field.key(), ...) - else if let syn::Expr::Call(call_expr) = expr { - for arg in &call_expr.args { - extract_ctx_from_expr(arg, ctx_fields, seen); - } - } else if let syn::Expr::Reference(ref_expr) = expr { - extract_ctx_from_expr(&ref_expr.expr, ctx_fields, seen); - } else if let syn::Expr::MethodCall(method_call) = expr { - extract_ctx_from_expr(&method_call.receiver, ctx_fields, seen); - } - } - - // Extract from seeds - for seed in &spec.seeds { - extract_from_seed(seed, &mut ctx_fields, &mut seen); - } - - // Extract from authority seeds too - if let Some(auth_seeds) = &spec.authority { - for seed in auth_seeds { - extract_from_seed(seed, &mut ctx_fields, &mut seen); - } - } - - ctx_fields -} - -pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { - // Phase 8: Generate struct variants with ctx.* seed fields - - // Unpacked variants (with Pubkeys) - let unpacked_variants = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); - - let fields = ctx_fields.iter().map(|field| { - quote! { #field: Pubkey } - }); - - if ctx_fields.is_empty() { - quote! { #variant_name, } - } else { - quote! { #variant_name { #(#fields,)* }, } - } - }); - - // Packed variants (with u8 indices) - let packed_variants = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); - - let fields = ctx_fields.iter().map(|field| { - let idx_field = format_ident!("{}_idx", field); - quote! { #idx_field: u8 } - }); - - if ctx_fields.is_empty() { - quote! { #variant_name, } - } else { - quote! { #variant_name { #(#fields,)* }, } - } - }); - - // Pack impl match arms - let pack_arms = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); - - if ctx_fields.is_empty() { - quote! { - TokenAccountVariant::#variant_name => PackedTokenAccountVariant::#variant_name, - } - } else { - let field_bindings: Vec<_> = ctx_fields.iter().collect(); - let idx_fields: Vec<_> = ctx_fields - .iter() - .map(|f| format_ident!("{}_idx", f)) - .collect(); - let pack_stmts: Vec<_> = ctx_fields - .iter() - .zip(idx_fields.iter()) - .map(|(field, idx)| { - quote! { let #idx = remaining_accounts.insert_or_get(*#field); } - }) - .collect(); - - quote! { - TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { - #(#pack_stmts)* - PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } - } - } - } - }); - - // Unpack impl match arms - let unpack_arms = token_seeds.iter().map(|spec| { - let variant_name = &spec.variant; - let ctx_fields = extract_ctx_fields_from_token_spec(spec); - - if ctx_fields.is_empty() { - quote! { - PackedTokenAccountVariant::#variant_name => Ok(TokenAccountVariant::#variant_name), - } - } else { - let idx_fields: Vec<_> = ctx_fields - .iter() - .map(|f| format_ident!("{}_idx", f)) - .collect(); - let unpack_stmts: Vec<_> = ctx_fields - .iter() - .zip(idx_fields.iter()) - .map(|(field, idx)| { - // Dereference idx since match pattern gives us &u8 - quote! { - let #field = *remaining_accounts - .get(*#idx as usize) - .ok_or(solana_program_error::ProgramError::InvalidAccountData)? - .key; - } - }) - .collect(); - let field_names: Vec<_> = ctx_fields.iter().collect(); - - quote! { - PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { - #(#unpack_stmts)* - Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) - } - } - } - }); - - Ok(quote! { - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum TokenAccountVariant { - #(#unpacked_variants)* - } - - #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] - pub enum PackedTokenAccountVariant { - #(#packed_variants)* - } - - impl light_token_sdk::pack::Pack for TokenAccountVariant { - type Packed = PackedTokenAccountVariant; - - fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { - match self { - #(#pack_arms)* - } - } - } - - impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { - type Unpacked = TokenAccountVariant; - - fn unpack( - &self, - remaining_accounts: &[solana_account_info::AccountInfo], - ) -> std::result::Result { - match self { - #(#unpack_arms)* - } - } - } - - impl light_sdk::compressible::IntoCTokenVariant for TokenAccountVariant { - fn into_ctoken_variant(self, token_data: light_token_sdk::compat::TokenData) -> RentFreeAccountVariant { - RentFreeAccountVariant::CTokenData(light_token_sdk::compat::CTokenData { - variant: self, - token_data, - }) - } - } - }) -} - -/// Convert a SeedElement to a TokenStream representing the seed reference expression. -/// Used by generate_ctoken_seed_provider_implementation for both token and authority seeds. -fn seed_element_to_ref_expr(seed: &SeedElement) -> TokenStream { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - quote! { #value.as_bytes() } - } - SeedElement::Expression(expr) => { - // Handle byte string literals - if let syn::Expr::Lit(lit_expr) = &**expr { - if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { - let bytes = byte_str.value(); - return quote! { &[#(#bytes),*] }; - } - } - - // Handle uppercase constants - if let syn::Expr::Path(path_expr) = &**expr { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if ident_str - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { - if ident_str == "LIGHT_CPI_SIGNER" { - return quote! { crate::#ident.cpi_signer.as_ref() }; - } else { - return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; - } - } - } - } - - // Handle ctx.accounts.field or ctx.field - use the destructured field directly - if let Some(field_name) = extract_ctx_field_name(expr) { - return quote! { #field_name.as_ref() }; - } - - // Fallback - quote! { (#expr).as_ref() } - } - } -} /// Phase 8: Generate TokenSeedProvider impl that uses self.field instead of ctx.accounts.field pub fn generate_ctoken_seed_provider_implementation( @@ -314,6 +30,8 @@ pub fn generate_ctoken_seed_provider_implementation( let mut get_seeds_match_arms = Vec::new(); let mut get_authority_seeds_match_arms = Vec::new(); + let config = SeedConversionConfig::for_ctoken_provider(); + for spec in token_seeds { let variant_name = &spec.variant; let ctx_fields = extract_ctx_fields_from_token_spec(spec); @@ -327,8 +45,7 @@ pub fn generate_ctoken_seed_provider_implementation( }; // Build seed refs for get_seeds - use self.field directly for ctx.* seeds - let token_seed_refs: Vec = - spec.seeds.iter().map(seed_element_to_ref_expr).collect(); + let token_seed_refs: Vec = spec.seeds.iter().map(|s| seed_element_to_ref_expr(s, &config)).collect(); let get_seeds_arm = quote! { #pattern => { @@ -344,10 +61,7 @@ pub fn generate_ctoken_seed_provider_implementation( // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let auth_seed_refs: Vec = authority_seeds - .iter() - .map(seed_element_to_ref_expr) - .collect(); + let auth_seed_refs: Vec = authority_seeds.iter().map(|s| seed_element_to_ref_expr(s, &config)).collect(); let authority_arm = quote! { #pattern => { @@ -396,55 +110,6 @@ pub fn generate_ctoken_seed_provider_implementation( }) } -/// Extract the field name from a ctx.field or ctx.accounts.field expression -fn extract_ctx_field_name(expr: &syn::Expr) -> Option { - if let syn::Expr::Field(field_expr) = expr { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for ctx.accounts.field pattern - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - if let syn::Expr::Path(path) = &*nested_field.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - return Some(field_name.clone()); - } - } - } - } - } - } - // Check for ctx.field pattern (shorthand) - else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - return Some(field_name.clone()); - } - } - } - } - } - None -} - -/// Generate the body of a seed function that computes a PDA address. -/// `program_id_expr` should be either `&crate::ID` or a variable like `_program_id`. -fn generate_seed_fn_body( - seed_count: usize, - seed_expressions: &[TokenStream], - program_id_expr: TokenStream, -) -> TokenStream { - quote! { - let mut seed_values = Vec::with_capacity(#seed_count + 1); - #( - seed_values.push((#seed_expressions).to_vec()); - )* - let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); - let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, #program_id_expr); - seed_values.push(vec![bump]); - (seed_values, pda) - } -} #[inline(never)] pub fn generate_client_seed_functions( @@ -464,9 +129,8 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let seed_count = seed_expressions.len(); let fn_body = - generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); + generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -486,9 +150,8 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let seed_count = seed_expressions.len(); let fn_body = - generate_seed_fn_body(seed_count, &seed_expressions, quote! { &crate::ID }); + generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -517,24 +180,15 @@ pub fn generate_client_seed_functions( let (auth_parameters, auth_seed_expressions) = analyze_seed_spec_for_client(&authority_spec, instruction_data)?; - let auth_seed_count = auth_seed_expressions.len(); let (fn_params, fn_body) = if auth_parameters.is_empty() { ( quote! { _program_id: &solana_pubkey::Pubkey }, - generate_seed_fn_body( - auth_seed_count, - &auth_seed_expressions, - quote! { _program_id }, - ), + generate_seed_derivation_body(&auth_seed_expressions, quote! { _program_id }), ) } else { ( quote! { #(#auth_parameters),* }, - generate_seed_fn_body( - auth_seed_count, - &auth_seed_expressions, - quote! { &crate::ID }, - ), + generate_seed_derivation_body(&auth_seed_expressions, quote! { &crate::ID }), ) }; let authority_function = quote! { @@ -666,10 +320,7 @@ fn analyze_seed_spec_for_client( syn::Expr::Path(path_expr) => { if let Some(ident) = path_expr.path.get_ident() { let ident_str = ident.to_string(); - if ident_str - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { + if is_constant_identifier(&ident_str) { if ident_str == "LIGHT_CPI_SIGNER" { expressions.push(quote! { crate::#ident.cpi_signer.as_ref() }); } else { @@ -869,12 +520,7 @@ fn analyze_seed_spec_for_client_expr( syn::Expr::Path(path_expr) => { if let Some(ident) = path_expr.path.get_ident() { let name = ident.to_string(); - if !(name == "ctx" - || name == "data" - || name - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit())) - { + if !(name == "ctx" || name == "data" || is_constant_identifier(&name)) { parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); } } diff --git a/sdk-libs/macros/src/rentfree/program/seed_utils.rs b/sdk-libs/macros/src/rentfree/program/seed_utils.rs new file mode 100644 index 0000000000..ff15f3951d --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/seed_utils.rs @@ -0,0 +1,123 @@ +//! Seed expression conversion and derivation utilities. +//! +//! This module provides reusable utilities for: +//! - Converting SeedElement to TokenStream expressions +//! - Generating seed derivation code +//! - Extracting context fields from seed specifications + +use proc_macro2::TokenStream; +use quote::quote; +use std::collections::HashSet; +use syn::Ident; + +use super::parsing::SeedElement; +use crate::rentfree::shared_utils::is_constant_identifier; + +// ============================================================================= +// SEED EXPRESSION CONVERSION +// ============================================================================= + +/// Configuration for seed expression conversion. +#[derive(Clone, Debug, Default)] +pub struct SeedConversionConfig { + /// Handle LIGHT_CPI_SIGNER specially with .cpi_signer.as_ref() + pub handle_light_cpi_signer: bool, + /// Map ctx.* to destructured field names (use field directly instead of ctx.field) + pub map_ctx_to_destructured: bool, +} + +impl SeedConversionConfig { + /// Config for ctoken seed provider (destructures ctx fields). + pub fn for_ctoken_provider() -> Self { + Self { + handle_light_cpi_signer: true, + map_ctx_to_destructured: true, + } + } +} + +/// Convert a SeedElement to a TokenStream representing the seed reference expression. +pub fn seed_element_to_ref_expr(seed: &SeedElement, config: &SeedConversionConfig) -> TokenStream { + match seed { + SeedElement::Literal(lit) => { + let value = lit.value(); + quote! { #value.as_bytes() } + } + SeedElement::Expression(expr) => { + // Handle byte string literals + if let syn::Expr::Lit(lit_expr) = &**expr { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + let bytes = byte_str.value(); + return quote! { &[#(#bytes),*] }; + } + } + + // Handle uppercase constants + if let syn::Expr::Path(path_expr) = &**expr { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if is_constant_identifier(&ident_str) { + if config.handle_light_cpi_signer && ident_str == "LIGHT_CPI_SIGNER" { + return quote! { crate::#ident.cpi_signer.as_ref() }; + } else { + return quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }; + } + } + } + } + + // Handle ctx.accounts.field or ctx.field + if config.map_ctx_to_destructured { + if let Some(field_name) = extract_ctx_field_name(expr) { + return quote! { #field_name.as_ref() }; + } + } + + // Fallback + quote! { (#expr).as_ref() } + } + } +} + +/// Extract the field name from a ctx.field or ctx.accounts.field expression. +/// +/// Uses the visitor-based FieldExtractor for clean pattern matching. +fn extract_ctx_field_name(expr: &syn::Expr) -> Option { + let fields = super::visitors::FieldExtractor::ctx_fields(&[]).extract(expr); + fields.into_iter().next() +} + +// ============================================================================= +// SEED DERIVATION GENERATION +// ============================================================================= + +/// Generate the body of a seed function that computes a PDA address. +/// +/// Returns code that: +/// 1. Builds seed_values Vec +/// 2. Computes PDA with find_program_address +/// 3. Appends bump to seeds +/// 4. Returns (seeds_vec, pda) +pub fn generate_seed_derivation_body( + seed_expressions: &[TokenStream], + program_id_expr: TokenStream, +) -> TokenStream { + let seed_count = seed_expressions.len(); + quote! { + let mut seed_values = Vec::with_capacity(#seed_count + 1); + #( + seed_values.push((#seed_expressions).to_vec()); + )* + let seed_slices: Vec<&[u8]> = seed_values.iter().map(|v| v.as_slice()).collect(); + let (pda, bump) = solana_pubkey::Pubkey::find_program_address(&seed_slices, #program_id_expr); + seed_values.push(vec![bump]); + (seed_values, pda) + } +} + + + +/// Build set of ctx field names from identifiers. +pub fn ctx_fields_to_set(fields: &[Ident]) -> HashSet { + fields.iter().map(|f| f.to_string()).collect() +} diff --git a/sdk-libs/macros/src/rentfree/program/variant_enum.rs b/sdk-libs/macros/src/rentfree/program/variant_enum.rs index 88aa578c36..be3d109e90 100644 --- a/sdk-libs/macros/src/rentfree/program/variant_enum.rs +++ b/sdk-libs/macros/src/rentfree/program/variant_enum.rs @@ -2,6 +2,8 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; +use super::parsing::{SeedElement, TokenSeedSpec}; + /// Info about ctx.* seeds for a PDA type #[derive(Clone, Debug)] pub struct PdaCtxSeedInfo { @@ -333,3 +335,183 @@ pub fn compressed_account_variant_with_ctx_seeds( Ok(expanded) } + +// ============================================================================= +// TOKEN ACCOUNT VARIANT +// ============================================================================= + +/// Extract ctx.* field names from seed elements (both token seeds and authority seeds). +/// +/// Uses the visitor-based FieldExtractor for clean AST traversal. +pub fn extract_ctx_fields_from_token_spec(spec: &TokenSeedSpec) -> Vec { + const EXCLUDED: &[&str] = &[ + "fee_payer", + "rent_sponsor", + "config", + "compression_authority", + ]; + + let mut all_fields = Vec::new(); + let mut seen = std::collections::HashSet::new(); + + for seed in spec.seeds.iter().chain(spec.authority.iter().flatten()) { + if let SeedElement::Expression(expr) = seed { + // Extract fields from this expression using the visitor + let fields = super::visitors::FieldExtractor::ctx_fields(EXCLUDED).extract(expr); + // Deduplicate across seeds + for field in fields { + let name = field.to_string(); + if seen.insert(name) { + all_fields.push(field); + } + } + } + } + + all_fields +} + +/// Generate TokenAccountVariant and PackedTokenAccountVariant enums with Pack/Unpack impls. +pub fn generate_ctoken_account_variant_enum(token_seeds: &[TokenSeedSpec]) -> Result { + let unpacked_variants = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let fields = ctx_fields.iter().map(|field| { + quote! { #field: Pubkey } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + let packed_variants = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + let fields = ctx_fields.iter().map(|field| { + let idx_field = format_ident!("{}_idx", field); + quote! { #idx_field: u8 } + }); + + if ctx_fields.is_empty() { + quote! { #variant_name, } + } else { + quote! { #variant_name { #(#fields,)* }, } + } + }); + + let pack_arms = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + if ctx_fields.is_empty() { + quote! { + TokenAccountVariant::#variant_name => PackedTokenAccountVariant::#variant_name, + } + } else { + let field_bindings: Vec<_> = ctx_fields.iter().collect(); + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let pack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + quote! { let #idx = remaining_accounts.insert_or_get(*#field); } + }) + .collect(); + + quote! { + TokenAccountVariant::#variant_name { #(#field_bindings,)* } => { + #(#pack_stmts)* + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } + } + } + } + }); + + let unpack_arms = token_seeds.iter().map(|spec| { + let variant_name = &spec.variant; + let ctx_fields = extract_ctx_fields_from_token_spec(spec); + + if ctx_fields.is_empty() { + quote! { + PackedTokenAccountVariant::#variant_name => Ok(TokenAccountVariant::#variant_name), + } + } else { + let idx_fields: Vec<_> = ctx_fields + .iter() + .map(|f| format_ident!("{}_idx", f)) + .collect(); + let unpack_stmts: Vec<_> = ctx_fields + .iter() + .zip(idx_fields.iter()) + .map(|(field, idx)| { + quote! { + let #field = *remaining_accounts + .get(*#idx as usize) + .ok_or(solana_program_error::ProgramError::InvalidAccountData)? + .key; + } + }) + .collect(); + let field_names: Vec<_> = ctx_fields.iter().collect(); + + quote! { + PackedTokenAccountVariant::#variant_name { #(#idx_fields,)* } => { + #(#unpack_stmts)* + Ok(TokenAccountVariant::#variant_name { #(#field_names,)* }) + } + } + } + }); + + Ok(quote! { + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum TokenAccountVariant { + #(#unpacked_variants)* + } + + #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy)] + pub enum PackedTokenAccountVariant { + #(#packed_variants)* + } + + impl light_token_sdk::pack::Pack for TokenAccountVariant { + type Packed = PackedTokenAccountVariant; + + fn pack(&self, remaining_accounts: &mut light_sdk::instruction::PackedAccounts) -> Self::Packed { + match self { + #(#pack_arms)* + } + } + } + + impl light_token_sdk::pack::Unpack for PackedTokenAccountVariant { + type Unpacked = TokenAccountVariant; + + fn unpack( + &self, + remaining_accounts: &[solana_account_info::AccountInfo], + ) -> std::result::Result { + match self { + #(#unpack_arms)* + } + } + } + + impl light_sdk::compressible::IntoCTokenVariant for TokenAccountVariant { + fn into_ctoken_variant(self, token_data: light_token_sdk::compat::TokenData) -> RentFreeAccountVariant { + RentFreeAccountVariant::CTokenData(light_token_sdk::compat::CTokenData { + variant: self, + token_data, + }) + } + } + }) +} diff --git a/sdk-libs/macros/src/rentfree/shared_utils.rs b/sdk-libs/macros/src/rentfree/shared_utils.rs new file mode 100644 index 0000000000..1df57f6c58 --- /dev/null +++ b/sdk-libs/macros/src/rentfree/shared_utils.rs @@ -0,0 +1,82 @@ +//! Shared utilities for rentfree macro implementation. +//! +//! This module provides common utility functions used across multiple files: +//! - Constant identifier detection (SCREAMING_SNAKE_CASE) +//! - Expression identifier extraction + +use syn::{Expr, Ident}; + +/// 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, +/// underscores, or ASCII digits. +/// +/// # Examples +/// ```ignore +/// assert!(is_constant_identifier("MY_CONSTANT")); +/// assert!(is_constant_identifier("SEED_123")); +/// assert!(!is_constant_identifier("myVariable")); +/// assert!(!is_constant_identifier("")); +/// ``` +#[inline] +pub fn is_constant_identifier(ident: &str) -> bool { + !ident.is_empty() && ident.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) +} + +/// Extract the terminal identifier from an expression. +/// +/// This handles various expression patterns: +/// - `Path`: Returns the identifier directly +/// - `Field`: Returns the field name +/// - `MethodCall`: Recursively extracts from receiver +/// - `Reference`: Recursively extracts from referenced expression +/// +/// If `key_method_only` is true, only returns an identifier from MethodCall +/// expressions where the method is `key`. +#[inline] +pub fn extract_terminal_ident(expr: &Expr, key_method_only: bool) -> Option { + match expr { + Expr::Path(path) => path.path.get_ident().cloned(), + Expr::Field(field) => { + if let syn::Member::Named(name) = &field.member { + Some(name.clone()) + } else { + None + } + } + Expr::MethodCall(mc) => { + if key_method_only && mc.method != "key" { + None + } else { + extract_terminal_ident(&mc.receiver, key_method_only) + } + } + Expr::Reference(r) => extract_terminal_ident(&r.expr, key_method_only), + _ => None, + } +} + +/// Check if an expression is a path starting with the given base identifier. +/// +/// Used to check patterns like `ctx.field` where base would be "ctx". +#[inline] +pub fn is_base_path(expr: &Expr, base: &str) -> bool { + matches!(expr, Expr::Path(p) if p.path.segments.first().is_some_and(|s| s.ident == base)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_constant_identifier() { + assert!(is_constant_identifier("MY_CONSTANT")); + assert!(is_constant_identifier("SEED")); + assert!(is_constant_identifier("SEED_123")); + assert!(is_constant_identifier("A")); + assert!(!is_constant_identifier("myVariable")); + assert!(!is_constant_identifier("my_variable")); + assert!(!is_constant_identifier("MyConstant")); + assert!(!is_constant_identifier("")); + } +} diff --git a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs index fb2e810fec..4be7fc4c4b 100644 --- a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs @@ -3,6 +3,7 @@ //! This module extracts PDA seeds from Anchor's attribute syntax and classifies them //! into the categories needed for compression: literals, ctx fields, data fields, etc. +use crate::rentfree::shared_utils::{extract_terminal_ident, is_constant_identifier}; use crate::utils::snake_to_camel_case; use syn::{Expr, Ident, ItemStruct, Type}; @@ -452,11 +453,7 @@ pub fn classify_seed_expr(expr: &Expr) -> syn::Result { // CONSTANT (all uppercase path) Expr::Path(path) => { if let Some(ident) = path.path.get_ident() { - let name = ident.to_string(); - if name - .chars() - .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) - { + if is_constant_identifier(&ident.to_string()) { return Ok(ClassifiedSeed::Constant(path.path.clone())); } // Otherwise it's a variable reference - treat as ctx account @@ -508,7 +505,7 @@ pub fn classify_seed_expr(expr: &Expr) -> syn::Result { let mut ctx_args = Vec::new(); for arg in &call.args { - if let Some(ident) = extract_ctx_ident_from_expr(arg) { + if let Some(ident) = extract_terminal_ident(arg, true) { ctx_args.push(ident); } } @@ -544,7 +541,7 @@ fn classify_method_call(mc: &syn::ExprMethodCall) -> syn::Result // Handle account.key() if mc.method == "key" { - if let Some(ident) = extract_receiver_ident(&mc.receiver) { + if let Some(ident) = extract_terminal_ident(&mc.receiver, false) { // Check if it's params.field or ctx.account if let Expr::Field(field) = &*mc.receiver { if let Expr::Path(path) = &*field.base { @@ -594,46 +591,6 @@ fn extract_params_field(expr: &Expr) -> Option<(Ident, String)> { None } -/// Extract the base identifier from an expression like account.key() -> account -fn extract_receiver_ident(expr: &Expr) -> Option { - match expr { - Expr::Path(path) => path.path.get_ident().cloned(), - Expr::Field(field) => { - if let syn::Member::Named(name) = &field.member { - Some(name.clone()) - } else { - None - } - } - Expr::MethodCall(mc) => extract_receiver_ident(&mc.receiver), - Expr::Reference(r) => extract_receiver_ident(&r.expr), - _ => None, - } -} - -/// Extract ctx account identifier from expression (for function args) -fn extract_ctx_ident_from_expr(expr: &Expr) -> Option { - match expr { - Expr::Reference(r) => extract_ctx_ident_from_expr(&r.expr), - Expr::MethodCall(mc) => { - if mc.method == "key" { - extract_receiver_ident(&mc.receiver) - } else { - None - } - } - Expr::Field(field) => { - if let syn::Member::Named(name) = &field.member { - Some(name.clone()) - } else { - None - } - } - Expr::Path(path) => path.path.get_ident().cloned(), - _ => None, - } -} - /// Get data field names from classified seeds pub fn get_data_fields(seeds: &[ClassifiedSeed]) -> Vec<(Ident, Option)> { let mut fields = Vec::new(); From dbab17cbf832b1f16f2c7e005f08ffbd07751f3e Mon Sep 17 00:00:00 2001 From: ananas Date: Sat, 17 Jan 2026 14:56:27 +0000 Subject: [PATCH 9/9] stash reduce nesting --- sdk-libs/macros/src/lib.rs | 8 +- .../src/rentfree/accounts/light_mint.rs | 7 +- sdk-libs/macros/src/rentfree/accounts/mod.rs | 2 + .../macros/src/rentfree/accounts/parse.rs | 34 +- sdk-libs/macros/src/rentfree/accounts/pda.rs | 12 +- .../src/rentfree/program/crate_context.rs | 26 +- .../macros/src/rentfree/program/decompress.rs | 14 +- .../src/rentfree/program/expr_traversal.rs | 12 +- .../src/rentfree/program/instructions.rs | 42 +- sdk-libs/macros/src/rentfree/program/mod.rs | 6 +- .../src/rentfree/program/seed_codegen.rs | 376 ++--------- .../macros/src/rentfree/program/seed_utils.rs | 5 +- .../macros/src/rentfree/program/visitors.rs | 586 ++++++++++++++++++ sdk-libs/macros/src/rentfree/shared_utils.rs | 5 +- sdk-libs/macros/src/rentfree/traits/mod.rs | 3 +- .../src/rentfree/traits/seed_extraction.rs | 7 +- sdk-libs/macros/src/rentfree/traits/traits.rs | 5 +- 17 files changed, 727 insertions(+), 423 deletions(-) create mode 100644 sdk-libs/macros/src/rentfree/program/visitors.rs diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index aeac65ad48..369ae0dd42 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -284,7 +284,9 @@ pub fn compressible_derive(input: TokenStream) -> TokenStream { #[proc_macro_derive(CompressiblePack)] pub fn compressible_pack(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(rentfree::traits::pack_unpack::derive_compressible_pack(input)) + into_token_stream(rentfree::traits::pack_unpack::derive_compressible_pack( + input, + )) } /// Consolidates all required traits for rent-free state accounts into a single derive. @@ -332,9 +334,7 @@ pub fn compressible_pack(input: TokenStream) -> TokenStream { #[proc_macro_derive(RentFreeAccount, attributes(compress_as))] pub fn rent_free_account(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); - into_token_stream(rentfree::traits::light_compressible::derive_rentfree_account( - input, - )) + into_token_stream(rentfree::traits::light_compressible::derive_rentfree_account(input)) } /// Derives a Rent Sponsor PDA for a program at compile time. diff --git a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs index 89128e30fa..1f45f12677 100644 --- a/sdk-libs/macros/src/rentfree/accounts/light_mint.rs +++ b/sdk-libs/macros/src/rentfree/accounts/light_mint.rs @@ -95,10 +95,9 @@ pub(super) fn parse_light_mint_attr( .map_err(|e| syn::Error::new_spanned(attr, e.to_string()))?; // address_tree_info defaults to params.create_accounts_proof.address_tree_info - let address_tree_info = args - .address_tree_info - .map(Into::into) - .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.address_tree_info)); + let address_tree_info = args.address_tree_info.map(Into::into).unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); return Ok(Some(LightMintField { field_ident: field_ident.clone(), diff --git a/sdk-libs/macros/src/rentfree/accounts/mod.rs b/sdk-libs/macros/src/rentfree/accounts/mod.rs index 5fe9275770..c10cc6a7c4 100644 --- a/sdk-libs/macros/src/rentfree/accounts/mod.rs +++ b/sdk-libs/macros/src/rentfree/accounts/mod.rs @@ -16,3 +16,5 @@ pub fn derive_rentfree(input: DeriveInput) -> Result { let parsed = parse::parse_rentfree_struct(&input)?; pda::generate_rentfree_impl(&parsed) } + +// 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 c0c6d29d32..cc3dfebe4a 100644 --- a/sdk-libs/macros/src/rentfree/accounts/parse.rs +++ b/sdk-libs/macros/src/rentfree/accounts/parse.rs @@ -7,11 +7,10 @@ use syn::{ DeriveInput, Error, Expr, Ident, Token, Type, }; -// Import shared types from seed_extraction module -pub(super) use crate::rentfree::traits::seed_extraction::extract_account_inner_type; - // Import LightMintField and parsing from light_mint module 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; // ============================================================================ // darling support for parsing Expr from attributes @@ -115,9 +114,7 @@ fn parse_instruction_attr(attrs: &[syn::Attribute]) -> Result Result { +pub(super) fn parse_rentfree_struct(input: &DeriveInput) -> Result { let struct_name = input.ident.clone(); let generics = input.generics.clone(); @@ -141,9 +138,10 @@ pub(super) fn parse_rentfree_struct( let mut ctoken_cpi_authority_field = None; for field in fields { - let field_ident = field.ident.clone().ok_or_else(|| { - Error::new_spanned(field, "expected named field with identifier") - })?; + let field_ident = field + .ident + .clone() + .ok_or_else(|| Error::new_spanned(field, "expected named field with identifier"))?; let field_name = field_ident.to_string(); // Track special fields by naming convention. @@ -215,14 +213,13 @@ pub(super) fn parse_rentfree_struct( .map_err(|e| Error::new_spanned(attr, e.to_string()))?; // Use defaults if not specified - let address_tree_info = args - .address_tree_info - .map(Into::into) - .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.address_tree_info)); - let output_tree = args - .output_tree - .map(Into::into) - .unwrap_or_else(|| syn::parse_quote!(params.create_accounts_proof.output_state_tree_index)); + let address_tree_info = + args.address_tree_info.map(Into::into).unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.address_tree_info) + }); + let output_tree = args.output_tree.map(Into::into).unwrap_or_else(|| { + syn::parse_quote!(params.create_accounts_proof.output_state_tree_index) + }); // Validate this is an Account type (or Box) let (is_boxed, inner_type) = @@ -250,8 +247,7 @@ pub(super) fn parse_rentfree_struct( } // Validation: #[rentfree] and #[light_mint] require #[instruction] attribute - if (!rentfree_fields.is_empty() || !light_mint_fields.is_empty()) - && instruction_args.is_none() + if (!rentfree_fields.is_empty() || !light_mint_fields.is_empty()) && instruction_args.is_none() { return Err(Error::new_spanned( input, diff --git a/sdk-libs/macros/src/rentfree/accounts/pda.rs b/sdk-libs/macros/src/rentfree/accounts/pda.rs index 98b27527c1..aed75e89df 100644 --- a/sdk-libs/macros/src/rentfree/accounts/pda.rs +++ b/sdk-libs/macros/src/rentfree/accounts/pda.rs @@ -15,8 +15,10 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use super::light_mint::{generate_mint_action_invocation, MintActionConfig}; -use super::parse::{ParsedRentFreeStruct, RentFreeField}; +use super::{ + light_mint::{generate_mint_action_invocation, MintActionConfig}, + parse::{ParsedRentFreeStruct, RentFreeField}, +}; /// Resolve optional field name to TokenStream, using default if None fn resolve_field_name(field: &Option, default: &str) -> TokenStream { @@ -51,7 +53,11 @@ pub(super) fn generate_rentfree_impl( } // Extract first instruction arg or generate no-op impls - let first_arg = match parsed.instruction_args.as_ref().and_then(|args| args.first()) { + 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. diff --git a/sdk-libs/macros/src/rentfree/program/crate_context.rs b/sdk-libs/macros/src/rentfree/program/crate_context.rs index caf2f27c46..db9191f28d 100644 --- a/sdk-libs/macros/src/rentfree/program/crate_context.rs +++ b/sdk-libs/macros/src/rentfree/program/crate_context.rs @@ -6,8 +6,11 @@ //! //! Based on Anchor's `CrateContext::parse()` pattern from `anchor-syn/src/parser/context.rs`. -use std::collections::BTreeMap; -use std::path::{Path, PathBuf}; +use std::{ + collections::BTreeMap, + path::{Path, PathBuf}, +}; + use syn::{Item, ItemStruct}; /// Context containing all parsed modules in the crate. @@ -37,10 +40,7 @@ impl CrateContext { } else { return Err(syn::Error::new( proc_macro2::Span::call_site(), - format!( - "Could not find lib.rs or main.rs in {:?}", - src_dir - ), + format!("Could not find lib.rs or main.rs in {:?}", src_dir), )); }; @@ -55,9 +55,7 @@ impl CrateContext { /// Iterate over all struct items in all parsed modules. pub fn structs(&self) -> impl Iterator { - self.modules - .values() - .flat_map(|module| module.structs()) + self.modules.values().flat_map(|module| module.structs()) } /// Find structs that have a specific derive attribute (e.g., "RentFree"). @@ -113,10 +111,7 @@ impl ParsedModule { })?; let root_dir = root.parent().unwrap_or(Path::new(".")); - let root_name = root - .file_stem() - .and_then(|s| s.to_str()) - .unwrap_or("root"); + let root_name = root.file_stem().and_then(|s| s.to_str()).unwrap_or("root"); // Create the root module let root_module = ParsedModule { @@ -146,8 +141,7 @@ impl ParsedModule { // External module: mod foo; - need to find the file if let Some(mod_file) = find_module_file(root_dir, root_name, &mod_name) { // Recursively parse the external module - let child_modules = - Self::parse_recursive(&mod_file, &child_path)?; + let child_modules = Self::parse_recursive(&mod_file, &child_path)?; modules.extend(child_modules); } // If file not found, silently skip (might be a cfg'd out module) @@ -196,7 +190,7 @@ fn find_module_file(parent_dir: &Path, parent_name: &str, mod_name: &str) -> Opt } else { // Parent is a regular file like foo.rs, check foo/mod_name.rs let parent_mod_dir = parent_dir.join(parent_name); - let extra_paths = vec![ + let extra_paths = [ parent_mod_dir.join(format!("{}.rs", mod_name)), parent_mod_dir.join(mod_name).join("mod.rs"), ]; diff --git a/sdk-libs/macros/src/rentfree/program/decompress.rs b/sdk-libs/macros/src/rentfree/program/decompress.rs index 16189fe0be..e39768a6c0 100644 --- a/sdk-libs/macros/src/rentfree/program/decompress.rs +++ b/sdk-libs/macros/src/rentfree/program/decompress.rs @@ -4,10 +4,12 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; -use super::expr_traversal::transform_expr_for_ctx_seeds; -use super::parsing::{InstructionVariant, SeedElement, TokenSeedSpec}; -use super::seed_utils::ctx_fields_to_set; -use super::variant_enum::PdaCtxSeedInfo; +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; // ============================================================================= @@ -94,9 +96,7 @@ pub fn generate_decompress_instruction_entrypoint() -> Result { // ============================================================================= #[inline(never)] -pub fn generate_decompress_accounts_struct( - variant: InstructionVariant, -) -> Result { +pub fn generate_decompress_accounts_struct(variant: InstructionVariant) -> Result { // Only Mixed variant is supported - PdaOnly and TokenOnly are not implemented match variant { InstructionVariant::PdaOnly | InstructionVariant::TokenOnly => { diff --git a/sdk-libs/macros/src/rentfree/program/expr_traversal.rs b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs index 54b1d621e3..59866d94b0 100644 --- a/sdk-libs/macros/src/rentfree/program/expr_traversal.rs +++ b/sdk-libs/macros/src/rentfree/program/expr_traversal.rs @@ -4,6 +4,7 @@ //! used in seed derivation code generation. use std::collections::HashSet; + use syn::Expr; use crate::rentfree::shared_utils::is_base_path; @@ -48,8 +49,10 @@ pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet { let mut new_call = method_call.clone(); - new_call.receiver = - Box::new(transform_expr_for_ctx_seeds(&method_call.receiver, ctx_field_names)); + new_call.receiver = Box::new(transform_expr_for_ctx_seeds( + &method_call.receiver, + ctx_field_names, + )); new_call.args = method_call .args .iter() @@ -68,7 +71,10 @@ pub fn transform_expr_for_ctx_seeds(expr: &Expr, ctx_field_names: &HashSet { let mut new_ref = ref_expr.clone(); - new_ref.expr = Box::new(transform_expr_for_ctx_seeds(&ref_expr.expr, ctx_field_names)); + new_ref.expr = Box::new(transform_expr_for_ctx_seeds( + &ref_expr.expr, + ctx_field_names, + )); Expr::Reference(new_ref) } _ => expr.clone(), diff --git a/sdk-libs/macros/src/rentfree/program/instructions.rs b/sdk-libs/macros/src/rentfree/program/instructions.rs index 99a5298ed5..704635cc50 100644 --- a/sdk-libs/macros/src/rentfree/program/instructions.rs +++ b/sdk-libs/macros/src/rentfree/program/instructions.rs @@ -1,6 +1,5 @@ //! Compressible instructions generation - orchestration module. -use crate::utils::to_snake_case; use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Item, ItemMod, Result}; @@ -10,25 +9,24 @@ pub use super::parsing::{ extract_ctx_seed_fields, extract_data_seed_fields, InstructionDataSpec, InstructionVariant, SeedElement, TokenSeedSpec, }; - -use super::parsing::{ - convert_classified_to_seed_elements, convert_classified_to_seed_elements_vec, - extract_context_and_params, macro_error, wrap_function_with_rentfree, +use super::{ + compress::{ + generate_compress_accounts_struct, generate_compress_context_impl, + generate_compress_instruction_entrypoint, generate_error_codes, + generate_process_compress_accounts_idempotent, validate_compressed_account_sizes, + }, + decompress::{ + generate_decompress_accounts_struct, generate_decompress_context_impl, + generate_decompress_instruction_entrypoint, generate_pda_seed_provider_impls, + generate_process_decompress_accounts_idempotent, + }, + parsing::{ + convert_classified_to_seed_elements, convert_classified_to_seed_elements_vec, + extract_context_and_params, macro_error, wrap_function_with_rentfree, + }, + variant_enum::PdaCtxSeedInfo, }; - -use super::compress::{ - generate_compress_accounts_struct, generate_compress_context_impl, - generate_compress_instruction_entrypoint, generate_error_codes, - generate_process_compress_accounts_idempotent, validate_compressed_account_sizes, -}; - -use super::decompress::{ - generate_decompress_accounts_struct, generate_decompress_context_impl, - generate_decompress_instruction_entrypoint, generate_pda_seed_provider_impls, - generate_process_decompress_accounts_idempotent, -}; - -use super::variant_enum::PdaCtxSeedInfo; +use crate::utils::to_snake_case; // ============================================================================= // MAIN CODEGEN @@ -194,10 +192,8 @@ fn codegen( let token_variant_name = format_ident!("TokenAccountVariant"); - let decompress_context_impl = generate_decompress_context_impl( - pda_ctx_seeds.clone(), - token_variant_name, - )?; + let decompress_context_impl = + generate_decompress_context_impl(pda_ctx_seeds.clone(), token_variant_name)?; let decompress_processor_fn = generate_process_decompress_accounts_idempotent()?; let decompress_instruction = generate_decompress_instruction_entrypoint()?; diff --git a/sdk-libs/macros/src/rentfree/program/mod.rs b/sdk-libs/macros/src/rentfree/program/mod.rs index 77dc6cac88..5992ce18be 100644 --- a/sdk-libs/macros/src/rentfree/program/mod.rs +++ b/sdk-libs/macros/src/rentfree/program/mod.rs @@ -5,12 +5,12 @@ //! - Auto-wraps instruction handlers with light_pre_init/light_finalize logic //! - Generates all necessary types, enums, and instruction handlers +mod compress; pub mod crate_context; -pub mod expr_traversal; -mod parsing; mod decompress; -mod compress; +pub mod expr_traversal; pub mod instructions; +mod parsing; pub mod seed_codegen; pub mod seed_utils; pub mod variant_enum; diff --git a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs index d7a0d94cf3..121a2e73f6 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_codegen.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_codegen.rs @@ -1,27 +1,17 @@ //! Seed provider generation for PDA and Light Token accounts. +use std::collections::HashSet; + use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{Ident, Result}; -use super::instructions::{InstructionDataSpec, SeedElement, TokenSeedSpec}; -use super::seed_utils::{generate_seed_derivation_body, seed_element_to_ref_expr, SeedConversionConfig}; -use super::variant_enum::extract_ctx_fields_from_token_spec; -use crate::rentfree::shared_utils::is_constant_identifier; -use crate::rentfree::traits::utils::is_pubkey_type; - -/// Helper to add a Pubkey parameter and its .as_ref() expression. -/// This is the default fallback for ctx.accounts.field and similar patterns. -#[inline] -fn push_pubkey_param( - field_name: &syn::Ident, - parameters: &mut Vec, - expressions: &mut Vec, -) { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name.as_ref() }); -} - +use super::{ + instructions::{InstructionDataSpec, TokenSeedSpec}, + seed_utils::{generate_seed_derivation_body, seed_element_to_ref_expr, SeedConversionConfig}, + variant_enum::extract_ctx_fields_from_token_spec, + visitors::{classify_seed, generate_client_seed_code}, +}; /// Phase 8: Generate TokenSeedProvider impl that uses self.field instead of ctx.accounts.field pub fn generate_ctoken_seed_provider_implementation( @@ -45,7 +35,11 @@ pub fn generate_ctoken_seed_provider_implementation( }; // Build seed refs for get_seeds - use self.field directly for ctx.* seeds - let token_seed_refs: Vec = spec.seeds.iter().map(|s| seed_element_to_ref_expr(s, &config)).collect(); + let token_seed_refs: Vec = spec + .seeds + .iter() + .map(|s| seed_element_to_ref_expr(s, &config)) + .collect(); let get_seeds_arm = quote! { #pattern => { @@ -61,7 +55,10 @@ pub fn generate_ctoken_seed_provider_implementation( // Build authority seeds if let Some(authority_seeds) = &spec.authority { - let auth_seed_refs: Vec = authority_seeds.iter().map(|s| seed_element_to_ref_expr(s, &config)).collect(); + let auth_seed_refs: Vec = authority_seeds + .iter() + .map(|s| seed_element_to_ref_expr(s, &config)) + .collect(); let authority_arm = quote! { #pattern => { @@ -110,7 +107,6 @@ pub fn generate_ctoken_seed_provider_implementation( }) } - #[inline(never)] pub fn generate_client_seed_functions( _account_types: &[Ident], @@ -129,8 +125,7 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let fn_body = - generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); + let fn_body = generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -150,8 +145,7 @@ pub fn generate_client_seed_functions( let (parameters, seed_expressions) = analyze_seed_spec_for_client(spec, instruction_data)?; - let fn_body = - generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); + let fn_body = generate_seed_derivation_body(&seed_expressions, quote! { &crate::ID }); let function = quote! { pub fn #function_name(#(#parameters),*) -> (Vec>, solana_pubkey::Pubkey) { #fn_body @@ -183,12 +177,18 @@ pub fn generate_client_seed_functions( let (fn_params, fn_body) = if auth_parameters.is_empty() { ( quote! { _program_id: &solana_pubkey::Pubkey }, - generate_seed_derivation_body(&auth_seed_expressions, quote! { _program_id }), + generate_seed_derivation_body( + &auth_seed_expressions, + quote! { _program_id }, + ), ) } else { ( quote! { #(#auth_parameters),* }, - generate_seed_derivation_body(&auth_seed_expressions, quote! { &crate::ID }), + generate_seed_derivation_body( + &auth_seed_expressions, + quote! { &crate::ID }, + ), ) }; let authority_function = quote! { @@ -211,6 +211,10 @@ pub fn generate_client_seed_functions( }) } +/// Analyze a seed spec and generate client function parameters and seed expressions. +/// +/// Uses the classification-based approach: first classify each seed, then generate code. +/// This separates "what kind of seed is this?" from "what code to generate?". #[inline(never)] fn analyze_seed_spec_for_client( spec: &TokenSeedSpec, @@ -218,314 +222,20 @@ fn analyze_seed_spec_for_client( ) -> Result<(Vec, Vec)> { let mut parameters = Vec::new(); let mut expressions = Vec::new(); + let mut seen_params = HashSet::new(); for seed in &spec.seeds { - match seed { - SeedElement::Literal(lit) => { - let value = lit.value(); - expressions.push(quote! { #value.as_bytes() }); - } - SeedElement::Expression(expr) => { - match &**expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - // Check for data.field pattern which uses instruction_data types - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - if let Some(data_spec) = instruction_data - .iter() - .find(|d| d.field_name == *field_name) - { - let param_type = &data_spec.field_type; - let param_with_ref = if is_pubkey_type(param_type) { - quote! { #field_name: &#param_type } - } else { - quote! { #field_name: #param_type } - }; - parameters.push(param_with_ref); - expressions.push(quote! { #field_name.as_ref() }); - continue; - } else { - return Err(syn::Error::new_spanned( - field_name, - format!( - "data.{} used in seeds but no type specified", - field_name - ), - )); - } - } - } - } - // Default: ctx.accounts.field, ctx.field, or other field patterns - push_pubkey_param(field_name, &mut parameters, &mut expressions); - } - } - syn::Expr::MethodCall(method_call) => { - if let syn::Expr::Field(field_expr) = &*method_call.receiver { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - if let Some(data_spec) = instruction_data - .iter() - .find(|d| d.field_name == *field_name) - { - let param_type = &data_spec.field_type; - let param_with_ref = if is_pubkey_type(param_type) { - quote! { #field_name: &#param_type } - } else { - quote! { #field_name: #param_type } - }; - parameters.push(param_with_ref); - - let method_name = &method_call.method; - expressions.push( - quote! { #field_name.#method_name().as_ref() }, - ); - } else { - return Err(syn::Error::new_spanned( - field_name, - format!("data.{} used in seeds but no type specified", field_name), - )); - } - } else if segment.ident == "ctx" { - // ctx.field.method() -> add field as Pubkey parameter - parameters.push( - quote! { #field_name: &solana_pubkey::Pubkey }, - ); - let method_name = &method_call.method; - expressions.push( - quote! { #field_name.#method_name().as_ref() }, - ); - } - } - } - } - } else if let syn::Expr::Path(path_expr) = &*method_call.receiver { - if let Some(ident) = path_expr.path.get_ident() { - parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); - expressions.push(quote! { #ident.as_ref() }); - } - } - } - syn::Expr::Lit(lit_expr) => { - // Handle byte string literals: b"seed" -> use directly - if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { - let bytes = byte_str.value(); - expressions.push(quote! { &[#(#bytes),*] }); - } - } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let ident_str = ident.to_string(); - if is_constant_identifier(&ident_str) { - if ident_str == "LIGHT_CPI_SIGNER" { - expressions.push(quote! { crate::#ident.cpi_signer.as_ref() }); - } else { - // Use crate:: prefix and explicit type annotation - expressions.push(quote! { { let __seed: &[u8] = crate::#ident.as_ref(); __seed } }); - } - } else { - parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); - expressions.push(quote! { #ident.as_ref() }); - } - } else { - expressions.push(quote! { (#expr).as_ref() }); - } - } - syn::Expr::Call(call_expr) => { - // Recursively map data.* to parameter names in function call arguments - fn map_client_call_arg( - arg: &syn::Expr, - instruction_data: &[InstructionDataSpec], - parameters: &mut Vec, - ) -> TokenStream { - match arg { - syn::Expr::Reference(ref_expr) => { - let inner = map_client_call_arg( - &ref_expr.expr, - instruction_data, - parameters, - ); - quote! { &#inner } - } - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "data" { - // Add parameter if needed - if let Some(data_spec) = instruction_data - .iter() - .find(|d| d.field_name == *field_name) - { - let param_type = &data_spec.field_type; - let param_with_ref = - if is_pubkey_type(param_type) { - quote! { #field_name: &#param_type } - } else { - quote! { #field_name: #param_type } - }; - if !parameters.iter().any(|p| { - p.to_string() - .contains(&field_name.to_string()) - }) { - parameters.push(param_with_ref); - } - } - return quote! { #field_name }; - } else if segment.ident == "ctx" { - // ctx.field -> add as Pubkey parameter - if !parameters.iter().any(|p| { - p.to_string() - .contains(&field_name.to_string()) - }) { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - } - return quote! { #field_name }; - } - } - } - } - quote! { #field_expr } - } - syn::Expr::MethodCall(method_call) => { - let receiver = map_client_call_arg( - &method_call.receiver, - instruction_data, - parameters, - ); - let method = &method_call.method; - let args: Vec<_> = method_call - .args - .iter() - .map(|a| { - map_client_call_arg(a, instruction_data, parameters) - }) - .collect(); - quote! { (#receiver).#method(#(#args),*) } - } - syn::Expr::Call(nested_call) => { - let func = &nested_call.func; - let args: Vec<_> = nested_call - .args - .iter() - .map(|a| { - map_client_call_arg(a, instruction_data, parameters) - }) - .collect(); - quote! { (#func)(#(#args),*) } - } - _ => quote! { #arg }, - } - } - - let mut mapped_args: Vec = Vec::new(); - for arg in &call_expr.args { - let mapped = - map_client_call_arg(arg, instruction_data, &mut parameters); - mapped_args.push(mapped); - } - let func = &call_expr.func; - expressions.push(quote! { (#func)(#(#mapped_args),*).as_ref() }); - } - syn::Expr::Reference(ref_expr) => { - let (ref_params, ref_exprs) = - analyze_seed_spec_for_client_expr(&ref_expr.expr, instruction_data)?; - parameters.extend(ref_params); - if let Some(first_expr) = ref_exprs.first() { - expressions.push(quote! { (#first_expr).as_ref() }); - } - } - _ => { - expressions.push(quote! { (#expr).as_ref() }); - } - } - } - } - } - - Ok((parameters, expressions)) -} - -#[inline(never)] -fn analyze_seed_spec_for_client_expr( - expr: &syn::Expr, - instruction_data: &[InstructionDataSpec], -) -> Result<(Vec, Vec)> { - let mut parameters = Vec::new(); - let mut expressions = Vec::new(); - - match expr { - syn::Expr::Field(field_expr) => { - if let syn::Member::Named(field_name) = &field_expr.member { - if let syn::Expr::Field(nested_field) = &*field_expr.base { - if let syn::Member::Named(base_name) = &nested_field.member { - if base_name == "accounts" { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name }); - } else if base_name == "data" { - // Use declared instruction_data types to determine parameter type - if let Some(data_spec) = instruction_data - .iter() - .find(|d| d.field_name == *field_name) - { - let param_type = &data_spec.field_type; - let param_with_ref = if is_pubkey_type(param_type) { - quote! { #field_name: &#param_type } - } else { - quote! { #field_name: #param_type } - }; - parameters.push(param_with_ref); - expressions.push(quote! { #field_name }); - } else { - return Err(syn::Error::new_spanned( - field_name, - format!( - "data.{} used in seeds but no type specified", - field_name - ), - )); - } - } - } - } else if let syn::Expr::Path(path) = &*field_expr.base { - if let Some(segment) = path.path.segments.first() { - if segment.ident == "ctx" { - parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); - expressions.push(quote! { #field_name }); - } - } - } - } - } - syn::Expr::MethodCall(method_call) => { - let (recv_params, _) = - analyze_seed_spec_for_client_expr(&method_call.receiver, instruction_data)?; - parameters.extend(recv_params); - } - syn::Expr::Call(call_expr) => { - for arg in &call_expr.args { - let (arg_params, _) = analyze_seed_spec_for_client_expr(arg, instruction_data)?; - parameters.extend(arg_params); - } - } - syn::Expr::Reference(ref_expr) => { - let (ref_params, _) = - analyze_seed_spec_for_client_expr(&ref_expr.expr, instruction_data)?; - parameters.extend(ref_params); - } - syn::Expr::Path(path_expr) => { - if let Some(ident) = path_expr.path.get_ident() { - let name = ident.to_string(); - if !(name == "ctx" || name == "data" || is_constant_identifier(&name)) { - parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); - } - } - } - _ => {} + // Phase 1: Classification + let info = classify_seed(seed)?; + + // Phase 2: Code generation (modifies parameters and expressions in place) + generate_client_seed_code( + &info, + instruction_data, + &mut seen_params, + &mut parameters, + &mut expressions, + )?; } Ok((parameters, expressions)) diff --git a/sdk-libs/macros/src/rentfree/program/seed_utils.rs b/sdk-libs/macros/src/rentfree/program/seed_utils.rs index ff15f3951d..93b875bb6b 100644 --- a/sdk-libs/macros/src/rentfree/program/seed_utils.rs +++ b/sdk-libs/macros/src/rentfree/program/seed_utils.rs @@ -5,9 +5,10 @@ //! - Generating seed derivation code //! - Extracting context fields from seed specifications +use std::collections::HashSet; + use proc_macro2::TokenStream; use quote::quote; -use std::collections::HashSet; use syn::Ident; use super::parsing::SeedElement; @@ -115,8 +116,6 @@ pub fn generate_seed_derivation_body( } } - - /// Build set of ctx field names from identifiers. pub fn ctx_fields_to_set(fields: &[Ident]) -> HashSet { fields.iter().map(|f| f.to_string()).collect() diff --git a/sdk-libs/macros/src/rentfree/program/visitors.rs b/sdk-libs/macros/src/rentfree/program/visitors.rs new file mode 100644 index 0000000000..1b4528d27a --- /dev/null +++ b/sdk-libs/macros/src/rentfree/program/visitors.rs @@ -0,0 +1,586 @@ +//! Visitor-based AST traversal utilities using syn's Visit trait. +//! +//! This module provides: +//! - `FieldExtractor`: A visitor for extracting field names from expressions +//! - `ClientSeedInfo`: Classification of seed elements for client code generation +//! - `classify_seed`: Classify a seed element into a `ClientSeedInfo` +//! - `generate_client_seed_code`: Generate (parameter, expression) from classified seed +//! +//! The implementation stores references during traversal to avoid allocations, +//! only cloning identifiers when producing the final output. + +use std::collections::HashSet; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::{ + visit::{self, Visit}, + Expr, Ident, Member, +}; + +use super::instructions::{InstructionDataSpec, SeedElement}; +use crate::rentfree::{shared_utils::is_constant_identifier, traits::utils::is_pubkey_type}; + +/// Visitor that extracts field names matching ctx.field, ctx.accounts.field, or data.field patterns. +/// +/// Uses syn's Visit trait for efficient read-only traversal. Stores references during +/// traversal to minimize allocations, only cloning when producing final output. +/// +/// # Example +/// ```ignore +/// let fields = FieldExtractor::ctx_fields(&["fee_payer"]) +/// .extract(&some_expr); +/// ``` +pub struct FieldExtractor<'ast, 'cfg> { + /// Extract ctx.field and ctx.accounts.field patterns + extract_ctx: bool, + /// Extract data.field patterns + extract_data: bool, + /// Field names to exclude from results + excluded: &'cfg [&'cfg str], + /// Collected field references (avoids cloning during traversal) + fields: Vec<&'ast Ident>, + /// Track seen field names for deduplication + seen: HashSet, +} + +impl<'ast, 'cfg> FieldExtractor<'ast, 'cfg> { + /// Create an extractor for ctx.field and ctx.accounts.field patterns. + /// + /// Excludes common infrastructure fields like fee_payer, rent_sponsor, etc. + pub fn ctx_fields(excluded: &'cfg [&'cfg str]) -> Self { + Self { + extract_ctx: true, + extract_data: false, + excluded, + fields: Vec::new(), + seen: HashSet::new(), + } + } + + /// Create an extractor for data.field patterns. + pub fn data_fields() -> Self { + Self { + extract_ctx: false, + extract_data: true, + excluded: &[], + fields: Vec::new(), + seen: HashSet::new(), + } + } + + /// Extract field names from the given expression. + /// + /// Visits the expression tree and collects all field names matching the configured patterns. + /// Returns deduplicated field identifiers in order of first occurrence. + /// Cloning is deferred until this final output stage. + pub fn extract(mut self, expr: &'ast Expr) -> Vec { + self.visit_expr(expr); + // Clone only when producing final output + self.fields.into_iter().cloned().collect() + } + + /// Try to add a field reference if not excluded and not already seen. + fn try_add(&mut self, field: &'ast Ident) { + let name = field.to_string(); + if !self.excluded.contains(&name.as_str()) && self.seen.insert(name) { + self.fields.push(field); + } + } + + /// Check if the base expression is `ctx.accounts`. + pub fn is_ctx_accounts(base: &Expr) -> bool { + if let Expr::Field(nested) = base { + if let Member::Named(member) = &nested.member { + return member == "accounts" && Self::is_path_ident(&nested.base, "ctx"); + } + } + false + } + + /// Check if an expression is a path with the given identifier. + fn is_path_ident(expr: &Expr, ident: &str) -> bool { + matches!(expr, Expr::Path(p) if p.path.is_ident(ident)) + } +} + +impl<'ast, 'cfg> Visit<'ast> for FieldExtractor<'ast, 'cfg> { + fn visit_expr_field(&mut self, node: &'ast syn::ExprField) { + if let Member::Named(field_name) = &node.member { + // Check for ctx.accounts.field pattern + if self.extract_ctx && Self::is_ctx_accounts(&node.base) { + self.try_add(field_name); + // Don't recurse further - we found our target + return; + } + + // Check for ctx.field pattern (direct access) + if self.extract_ctx && Self::is_path_ident(&node.base, "ctx") { + self.try_add(field_name); + return; + } + + // Check for data.field pattern + if self.extract_data && Self::is_path_ident(&node.base, "data") { + self.try_add(field_name); + return; + } + } + + // Continue visiting child expressions + visit::visit_expr_field(self, node); + } +} + +// ============================================================================= +// CLIENT SEED CLASSIFICATION +// ============================================================================= + +/// Classified seed for client code generation. +/// +/// Separates "what kind of seed is this?" from "what code to generate?". +/// Each variant captures all info needed for the generation phase. +#[derive(Debug, Clone)] +pub enum ClientSeedInfo { + /// String literal: "seed" -> "seed".as_bytes() + Literal(String), + /// Byte literal: b"seed" -> &[...] + ByteLiteral(Vec), + /// Constant: SEED -> crate::SEED.as_ref() + Constant { name: Ident, is_cpi_signer: bool }, + /// ctx.field or ctx.accounts.field -> Pubkey parameter + CtxField { field: Ident, method: Option }, + /// data.field -> typed parameter from instruction_data + DataField { field: Ident, method: Option }, + /// Function call - stored as original expression for proper AST transformation + FunctionCall(Box), + /// Raw identifier that becomes a Pubkey parameter + Identifier(Ident), + /// Fallback for expressions that don't match other patterns + RawExpr(Box), +} + +/// Classify a SeedElement for client code generation. +pub fn classify_seed(seed: &SeedElement) -> syn::Result { + match seed { + SeedElement::Literal(lit) => Ok(ClientSeedInfo::Literal(lit.value())), + SeedElement::Expression(expr) => classify_seed_expr(expr), + } +} + +/// Classify an expression into a ClientSeedInfo variant. +fn classify_seed_expr(expr: &syn::Expr) -> syn::Result { + match expr { + syn::Expr::Field(field_expr) => classify_field_expr(field_expr), + syn::Expr::MethodCall(method_call) => classify_method_call(method_call), + syn::Expr::Lit(lit_expr) => classify_lit_expr(lit_expr), + syn::Expr::Path(path_expr) => classify_path_expr(path_expr), + syn::Expr::Call(call_expr) => classify_call_expr(call_expr), + syn::Expr::Reference(ref_expr) => classify_seed_expr(&ref_expr.expr), + _ => Ok(ClientSeedInfo::RawExpr(Box::new(expr.clone()))), + } +} + +/// Classify a field expression (e.g., ctx.field, data.field). +fn classify_field_expr(field_expr: &syn::ExprField) -> syn::Result { + if let Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field pattern + if FieldExtractor::is_ctx_accounts(&field_expr.base) { + return Ok(ClientSeedInfo::CtxField { + field: field_name.clone(), + method: None, + }); + } + + // Check for direct base.field patterns + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + return Ok(ClientSeedInfo::DataField { + field: field_name.clone(), + method: None, + }); + } + if segment.ident == "ctx" { + return Ok(ClientSeedInfo::CtxField { + field: field_name.clone(), + method: None, + }); + } + } + } + + // Unrecognized field pattern - preserve as raw expression + // This lets downstream code decide how to handle it + return Ok(ClientSeedInfo::RawExpr(Box::new(syn::Expr::Field( + field_expr.clone(), + )))); + } + + Ok(ClientSeedInfo::RawExpr(Box::new(syn::Expr::Field( + field_expr.clone(), + )))) +} + +/// Classify a method call expression (e.g., data.field.method(), ctx.accounts.field.key()). +fn classify_method_call(method_call: &syn::ExprMethodCall) -> syn::Result { + // Check if receiver is a field expression + if let syn::Expr::Field(field_expr) = &*method_call.receiver { + if let Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field.method() pattern + if FieldExtractor::is_ctx_accounts(&field_expr.base) { + return Ok(ClientSeedInfo::CtxField { + field: field_name.clone(), + method: Some(method_call.method.clone()), + }); + } + + // Check for direct base.field patterns + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + return Ok(ClientSeedInfo::DataField { + field: field_name.clone(), + method: Some(method_call.method.clone()), + }); + } + if segment.ident == "ctx" { + return Ok(ClientSeedInfo::CtxField { + field: field_name.clone(), + method: Some(method_call.method.clone()), + }); + } + } + } + } + } + + // Check if receiver is a path identifier (e.g., ident.as_ref()) + if let syn::Expr::Path(path_expr) = &*method_call.receiver { + if let Some(ident) = path_expr.path.get_ident() { + return Ok(ClientSeedInfo::Identifier(ident.clone())); + } + } + + Ok(ClientSeedInfo::RawExpr(Box::new(syn::Expr::MethodCall( + method_call.clone(), + )))) +} + +/// Classify a literal expression. +fn classify_lit_expr(lit_expr: &syn::ExprLit) -> syn::Result { + if let syn::Lit::ByteStr(byte_str) = &lit_expr.lit { + Ok(ClientSeedInfo::ByteLiteral(byte_str.value())) + } else { + Ok(ClientSeedInfo::RawExpr(Box::new(syn::Expr::Lit( + lit_expr.clone(), + )))) + } +} + +/// Classify a path expression (constant or identifier). +fn classify_path_expr(path_expr: &syn::ExprPath) -> syn::Result { + if let Some(ident) = path_expr.path.get_ident() { + let ident_str = ident.to_string(); + if is_constant_identifier(&ident_str) { + return Ok(ClientSeedInfo::Constant { + name: ident.clone(), + is_cpi_signer: ident_str == "LIGHT_CPI_SIGNER", + }); + } + return Ok(ClientSeedInfo::Identifier(ident.clone())); + } + Ok(ClientSeedInfo::RawExpr(Box::new(syn::Expr::Path( + path_expr.clone(), + )))) +} + +/// Classify a function call expression. +/// We store the original expression because function call arguments need AST transformation, +/// not simple classification - we need to preserve method calls like `.key()` while mapping +/// base identifiers to parameters. +fn classify_call_expr(call_expr: &syn::ExprCall) -> syn::Result { + Ok(ClientSeedInfo::FunctionCall(Box::new(call_expr.clone()))) +} + +// ============================================================================= +// CLIENT CODE GENERATION +// ============================================================================= + +/// Map a function call argument, extracting parameters and transforming ctx/data references. +/// +/// This preserves the expression structure (method calls, references) while replacing +/// `ctx.field`, `ctx.accounts.field`, and `data.field` with just the field name. +/// Returns the transformed expression and collects parameters into the provided vectors. +fn map_call_arg( + arg: &syn::Expr, + instruction_data: &[InstructionDataSpec], + seen_params: &mut HashSet, + parameters: &mut Vec, +) -> syn::Result { + match arg { + syn::Expr::Reference(ref_expr) => { + let inner = map_call_arg(&ref_expr.expr, instruction_data, seen_params, parameters)?; + Ok(quote! { &#inner }) + } + syn::Expr::Field(field_expr) => { + if let syn::Member::Named(field_name) = &field_expr.member { + // Check for ctx.accounts.field + if FieldExtractor::is_ctx_accounts(&field_expr.base) { + if seen_params.insert(field_name.to_string()) { + parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); + } + return Ok(quote! { #field_name }); + } + // Check for ctx.field or data.field + if let syn::Expr::Path(path) = &*field_expr.base { + if let Some(segment) = path.path.segments.first() { + if segment.ident == "data" { + if let Some(data_spec) = instruction_data + .iter() + .find(|d| d.field_name == *field_name) + { + if seen_params.insert(field_name.to_string()) { + let param_type = &data_spec.field_type; + let param_with_ref = if is_pubkey_type(param_type) { + quote! { #field_name: &#param_type } + } else { + quote! { #field_name: #param_type } + }; + parameters.push(param_with_ref); + } + return Ok(quote! { #field_name }); + } + } else if segment.ident == "ctx" { + if seen_params.insert(field_name.to_string()) { + parameters.push(quote! { #field_name: &solana_pubkey::Pubkey }); + } + return Ok(quote! { #field_name }); + } + } + } + } + Ok(quote! { #field_expr }) + } + syn::Expr::MethodCall(method_call) => { + let receiver = map_call_arg( + &method_call.receiver, + instruction_data, + seen_params, + parameters, + )?; + let method = &method_call.method; + let args: Vec = method_call + .args + .iter() + .map(|a| map_call_arg(a, instruction_data, seen_params, parameters)) + .collect::>()?; + Ok(quote! { (#receiver).#method(#(#args),*) }) + } + syn::Expr::Call(nested_call) => { + let func = &nested_call.func; + let args: Vec = nested_call + .args + .iter() + .map(|a| map_call_arg(a, instruction_data, seen_params, parameters)) + .collect::>()?; + Ok(quote! { (#func)(#(#args),*) }) + } + syn::Expr::Path(path_expr) => { + // Check if this is a simple identifier that should become a parameter + if let Some(ident) = path_expr.path.get_ident() { + let name = ident.to_string(); + if name != "ctx" + && name != "data" + && !is_constant_identifier(&name) + && seen_params.insert(name) + { + parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); + } + } + Ok(quote! { #path_expr }) + } + _ => Ok(quote! { #arg }), + } +} + +/// Generate code for a classified seed, adding to the provided parameter and expression lists. +/// +/// This modifies the parameters and expressions vectors directly rather than returning +/// individual values, which allows for proper handling of function call seeds that may +/// contribute multiple parameters. +pub fn generate_client_seed_code( + info: &ClientSeedInfo, + instruction_data: &[InstructionDataSpec], + seen_params: &mut HashSet, + parameters: &mut Vec, + expressions: &mut Vec, +) -> syn::Result<()> { + match info { + ClientSeedInfo::Literal(s) => { + expressions.push(quote! { #s.as_bytes() }); + } + + ClientSeedInfo::ByteLiteral(bytes) => { + expressions.push(quote! { &[#(#bytes),*] }); + } + + ClientSeedInfo::Constant { + name, + is_cpi_signer, + } => { + let expr = if *is_cpi_signer { + quote! { crate::#name.cpi_signer.as_ref() } + } else { + quote! { { let __seed: &[u8] = crate::#name.as_ref(); __seed } } + }; + expressions.push(expr); + } + + ClientSeedInfo::CtxField { field, method } => { + if seen_params.insert(field.to_string()) { + parameters.push(quote! { #field: &solana_pubkey::Pubkey }); + } + let expr = match method { + Some(m) => quote! { #field.#m().as_ref() }, + None => quote! { #field.as_ref() }, + }; + expressions.push(expr); + } + + ClientSeedInfo::DataField { field, method } => { + let data_spec = instruction_data + .iter() + .find(|d| d.field_name == *field) + .ok_or_else(|| { + syn::Error::new( + field.span(), + format!("data.{} used in seeds but no type specified", field), + ) + })?; + + if seen_params.insert(field.to_string()) { + let param_type = &data_spec.field_type; + let param_with_ref = if is_pubkey_type(param_type) { + quote! { #field: &#param_type } + } else { + quote! { #field: #param_type } + }; + parameters.push(param_with_ref); + } + + let expr = match method { + Some(m) => quote! { #field.#m().as_ref() }, + None => quote! { #field.as_ref() }, + }; + expressions.push(expr); + } + + ClientSeedInfo::Identifier(ident) => { + if seen_params.insert(ident.to_string()) { + parameters.push(quote! { #ident: &solana_pubkey::Pubkey }); + } + expressions.push(quote! { #ident.as_ref() }); + } + + ClientSeedInfo::FunctionCall(call_expr) => { + let mut mapped_args: Vec = Vec::new(); + for arg in &call_expr.args { + let mapped = map_call_arg(arg, instruction_data, seen_params, parameters)?; + mapped_args.push(mapped); + } + let func = &call_expr.func; + expressions.push(quote! { (#func)(#(#mapped_args),*).as_ref() }); + } + + ClientSeedInfo::RawExpr(expr) => { + expressions.push(quote! { (#expr).as_ref() }); + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_extract_ctx_accounts_field() { + let expr: Expr = syn::parse_quote!(ctx.accounts.user); + let fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "user"); + } + + #[test] + fn test_extract_ctx_direct_field() { + let expr: Expr = syn::parse_quote!(ctx.program_id); + let fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "program_id"); + } + + #[test] + fn test_extract_data_field() { + let expr: Expr = syn::parse_quote!(data.owner); + let fields = FieldExtractor::data_fields().extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "owner"); + } + + #[test] + fn test_extract_nested_in_method_call() { + let expr: Expr = syn::parse_quote!(ctx.accounts.user.key()); + let fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "user"); + } + + #[test] + fn test_extract_nested_in_reference() { + let expr: Expr = syn::parse_quote!(&ctx.accounts.user.key()); + let fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "user"); + } + + #[test] + fn test_excludes_fields() { + let expr: Expr = syn::parse_quote!(ctx.accounts.fee_payer); + let fields = FieldExtractor::ctx_fields(&["fee_payer"]).extract(&expr); + assert!(fields.is_empty()); + } + + #[test] + fn test_deduplicates_fields() { + let expr: Expr = syn::parse_quote!({ + ctx.accounts.user.key(); + ctx.accounts.user.owner(); + }); + let fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + assert_eq!(fields.len(), 1); + assert_eq!(fields[0].to_string(), "user"); + } + + #[test] + fn test_extract_from_call_args() { + let expr: Expr = syn::parse_quote!(some_fn(&ctx.accounts.user, data.amount)); + let ctx_fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + let data_fields = FieldExtractor::data_fields().extract(&expr); + assert_eq!(ctx_fields.len(), 1); + assert_eq!(ctx_fields[0].to_string(), "user"); + assert_eq!(data_fields.len(), 1); + assert_eq!(data_fields[0].to_string(), "amount"); + } + + #[test] + fn test_separate_extractors_for_ctx_and_data() { + let expr: Expr = syn::parse_quote!((ctx.accounts.user, data.amount)); + let ctx_fields = FieldExtractor::ctx_fields(&[]).extract(&expr); + let data_fields = FieldExtractor::data_fields().extract(&expr); + assert_eq!(ctx_fields.len(), 1); + assert_eq!(ctx_fields[0].to_string(), "user"); + assert_eq!(data_fields.len(), 1); + assert_eq!(data_fields[0].to_string(), "amount"); + } +} diff --git a/sdk-libs/macros/src/rentfree/shared_utils.rs b/sdk-libs/macros/src/rentfree/shared_utils.rs index 1df57f6c58..a9550b5adf 100644 --- a/sdk-libs/macros/src/rentfree/shared_utils.rs +++ b/sdk-libs/macros/src/rentfree/shared_utils.rs @@ -20,7 +20,10 @@ use syn::{Expr, Ident}; /// ``` #[inline] pub fn is_constant_identifier(ident: &str) -> bool { - !ident.is_empty() && ident.chars().all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) + !ident.is_empty() + && ident + .chars() + .all(|c| c.is_uppercase() || c == '_' || c.is_ascii_digit()) } /// Extract the terminal identifier from an expression. diff --git a/sdk-libs/macros/src/rentfree/traits/mod.rs b/sdk-libs/macros/src/rentfree/traits/mod.rs index 643cf6e251..1909b7fa04 100644 --- a/sdk-libs/macros/src/rentfree/traits/mod.rs +++ b/sdk-libs/macros/src/rentfree/traits/mod.rs @@ -8,9 +8,10 @@ //! - `traits` - HasCompressionInfo, Compressible, CompressAs traits //! - `utils` - Shared utility functions -pub mod seed_extraction; pub mod decompress_context; pub mod light_compressible; pub mod pack_unpack; +pub mod seed_extraction; +#[allow(clippy::module_inception)] pub mod traits; pub mod utils; diff --git a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs index 4be7fc4c4b..f24be467bf 100644 --- a/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs +++ b/sdk-libs/macros/src/rentfree/traits/seed_extraction.rs @@ -3,10 +3,13 @@ //! This module extracts PDA seeds from Anchor's attribute syntax and classifies them //! into the categories needed for compression: literals, ctx fields, data fields, etc. -use crate::rentfree::shared_utils::{extract_terminal_ident, is_constant_identifier}; -use crate::utils::snake_to_camel_case; use syn::{Expr, Ident, ItemStruct, Type}; +use crate::{ + rentfree::shared_utils::{extract_terminal_ident, is_constant_identifier}, + utils::snake_to_camel_case, +}; + /// Classified seed element from Anchor's seeds array #[derive(Clone, Debug)] pub enum ClassifiedSeed { diff --git a/sdk-libs/macros/src/rentfree/traits/traits.rs b/sdk-libs/macros/src/rentfree/traits/traits.rs index 27b3dfa272..f7b5413dad 100644 --- a/sdk-libs/macros/src/rentfree/traits/traits.rs +++ b/sdk-libs/macros/src/rentfree/traits/traits.rs @@ -31,7 +31,10 @@ impl FromMeta for CompressAsFields { let name = nv.path.get_ident().cloned().ok_or_else(|| { darling::Error::custom("expected field identifier").with_span(&nv.path) })?; - Ok(CompressAsField { name, value: nv.value.clone() }) + Ok(CompressAsField { + name, + value: nv.value.clone(), + }) } other => Err(darling::Error::custom("expected field = expr").with_span(other)), })