From 2ef4c4a03742b2ed85dbdfb7387886911030e6e0 Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 23 Jan 2026 08:41:36 +0000 Subject: [PATCH 1/2] feat: LightMint --- .../src/state/mint/anchor_wrapper.rs | 434 ++++++++++++++++++ .../src/state/mint/compressed_mint.rs | 4 + .../token-interface/src/state/mint/mod.rs | 5 + .../src/state/mint/zero_copy.rs | 5 +- sdk-libs/macros/src/lib.rs | 5 +- .../macros/src/light_pdas/accounts/builder.rs | 248 ++++++++++ .../macros/src/light_pdas/accounts/derive.rs | 16 +- .../macros/src/light_pdas/accounts/parse.rs | 72 +++ .../src/light_pdas/program/instructions.rs | 22 +- .../src/instruction_accounts.rs | 66 +++ .../csdk-anchor-full-derived-test/src/lib.rs | 32 +- .../tests/mint/light_mint_test.rs | 136 ++++++ .../tests/mint/mod.rs | 1 + 13 files changed, 1036 insertions(+), 10 deletions(-) create mode 100644 program-libs/token-interface/src/state/mint/anchor_wrapper.rs create mode 100644 sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs diff --git a/program-libs/token-interface/src/state/mint/anchor_wrapper.rs b/program-libs/token-interface/src/state/mint/anchor_wrapper.rs new file mode 100644 index 0000000000..307fbe5840 --- /dev/null +++ b/program-libs/token-interface/src/state/mint/anchor_wrapper.rs @@ -0,0 +1,434 @@ +//! Anchor wrapper for Light Protocol mint accounts. +//! +//! Provides `LightMint<'info>` - a type-safe wrapper for mint accounts that are +//! initialized via CPI to the Light Token Program. This wrapper allows Anchor programs +//! to access mint data after CPI initialization completes. +//! +//! # Usage +//! +//! Use `LightMint<'info>` to wrap an account and access mint data via zero-copy: +//! +//! ```ignore +//! use light_token_interface::state::mint::LightMint; +//! +//! #[derive(Accounts, LightAccounts)] +//! pub struct CreateMint<'info> { +//! #[account(mut)] +//! #[light_account(init, mint::signer = mint_signer, ...)] +//! pub cmint: UncheckedAccount<'info>, +//! } +//! +//! pub fn handler(ctx: Context) -> Result<()> { +//! // After CPI completes, wrap and access mint data via zero-copy: +//! let light_mint = LightMint::new(ctx.accounts.cmint.to_account_info()); +//! let mint_data = light_mint.load()?; +//! msg!("Decimals: {}", mint_data.decimals); +//! msg!("Initialized: {}", mint_data.is_initialized()); +//! Ok(()) +//! } +//! ``` +//! +//! # Zero-Copy Pattern +//! +//! `load()` returns a `ZMint<'info>` zero-copy view that reads directly from account +//! data without allocation. `load_mut()` returns a `ZMintMut<'info>` that can write +//! directly to account data. Since zero-copy writes directly, `AccountsExit::exit()` +//! is a no-op. + +use std::ops::Deref; + +use anchor_lang::prelude::*; + +use super::{Mint, ZMint, ZMintMut, IS_INITIALIZED_OFFSET}; +use crate::{TokenError, LIGHT_TOKEN_PROGRAM_ID}; + +/// A wrapper around `AccountInfo` for Light Protocol mint accounts. +/// +/// This struct provides type-safe zero-copy access to mint data after CPI initialization. +/// Each call to `load()` or `load_mut()` creates a fresh zero-copy view into the account +/// data without allocation. +/// +/// # Anchor Integration +/// +/// `LightMint` implements all required Anchor traits, allowing it to be used +/// in `#[derive(Accounts)]` structs. During account deserialization +/// (`try_accounts`), no validation is performed - this allows the account to +/// be uninitialized before CPI. Validation happens when `load()` is called. +/// +/// # Zero-Copy Behavior +/// +/// Unlike borsh deserialization which allocates memory, zero-copy views read +/// and write directly from/to account data. This means: +/// - No memory allocation overhead +/// - Writes via `load_mut()` are immediately reflected in account data +/// - `AccountsExit::exit()` is a no-op since there's nothing to serialize +#[derive(Debug, Clone)] +pub struct LightMint<'info> { + info: AccountInfo<'info>, +} + +impl<'info> LightMint<'info> { + /// Creates a new `LightMint` wrapper from an `AccountInfo`. + /// + /// This does not perform any validation - the account may be uninitialized. + /// Validation occurs when `load()` is called. + /// + /// # Example + /// + /// ```ignore + /// // In an Anchor instruction handler, after CPI: + /// let light_mint = LightMint::new(ctx.accounts.cmint.to_account_info()); + /// let mint_data = light_mint.load()?; + /// assert!(mint_data.is_initialized()); + /// ``` + pub fn new(info: AccountInfo<'info>) -> Self { + Self { info } + } + + /// Loads and validates the mint data, returning an immutable zero-copy view. + /// + /// This method: + /// 1. Validates the account is owned by the Light Token Program + /// 2. Creates a zero-copy view into the account data + /// 3. Validates the mint is initialized and account type is correct + /// + /// Each call creates a fresh view - there is no caching. + /// + /// # Errors + /// + /// Returns `TokenError::InvalidMintOwner` if the account owner is not the Light Token Program. + /// Returns `TokenError::MintNotInitialized` if the mint is not initialized. + /// Returns `TokenError::InvalidAccountType` if the account type is not a mint. + /// Returns `TokenError::MintBorrowFailed` if the account data cannot be borrowed. + /// Returns `TokenError::MintDeserializationFailed` if zero-copy parsing fails. + pub fn load(&self) -> std::result::Result, TokenError> { + // Validate owner + if self.info.owner != &LIGHT_TOKEN_PROGRAM_ID.into() { + return Err(TokenError::InvalidMintOwner); + } + + let data = self + .info + .try_borrow_data() + .map_err(|_| TokenError::MintBorrowFailed)?; + + // Extend lifetime - safe because account data lives for transaction duration. + // This matches the pattern used in Token::from_account_info_checked. + let data_slice: &'info [u8] = + unsafe { core::slice::from_raw_parts(data.as_ptr(), data.len()) }; + + let (mint, _) = Mint::zero_copy_at_checked(data_slice)?; + Ok(mint) + } + + /// Loads and validates the mint data, returning a mutable zero-copy view. + /// + /// This method behaves like `load()` but returns a mutable view, + /// allowing modifications to the mint data. Since zero-copy writes directly + /// to account data, changes are immediately persisted. + /// + /// # Errors + /// + /// Same as `load()`. + pub fn load_mut(&self) -> std::result::Result, TokenError> { + // Validate owner + if self.info.owner != &LIGHT_TOKEN_PROGRAM_ID.into() { + return Err(TokenError::InvalidMintOwner); + } + + let mut data = self + .info + .try_borrow_mut_data() + .map_err(|_| TokenError::MintBorrowFailed)?; + + // Extend lifetime - safe because account data lives for transaction duration. + let data_slice: &'info mut [u8] = + unsafe { core::slice::from_raw_parts_mut(data.as_mut_ptr(), data.len()) }; + + let (mint, _) = Mint::zero_copy_at_mut_checked(data_slice)?; + Ok(mint) + } + + /// Returns a clone of the underlying `AccountInfo`. + /// + /// This is required for macro-generated code that calls `.to_account_info()` + /// on account fields in `#[derive(LightAccounts)]` structs. + pub fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } + + /// Returns true if the mint account appears to be initialized. + /// + /// This performs a quick check without fully parsing the account. + /// It checks: + /// 1. Account is owned by the Light Token Program + /// 2. Account has sufficient data length + /// 3. The is_initialized byte is non-zero + pub fn is_initialized(&self) -> bool { + // Check owner + if self.info.owner != &LIGHT_TOKEN_PROGRAM_ID.into() { + return false; + } + + let data = match self.info.try_borrow_data() { + Ok(d) => d, + Err(_) => return false, + }; + + if data.len() <= IS_INITIALIZED_OFFSET { + return false; + } + + data[IS_INITIALIZED_OFFSET] != 0 + } +} + +// === Deref, AsRef, and ToAccountInfo implementations === + +impl<'info> Deref for LightMint<'info> { + type Target = AccountInfo<'info>; + + fn deref(&self) -> &Self::Target { + &self.info + } +} + +impl<'info> AsRef> for LightMint<'info> { + fn as_ref(&self) -> &AccountInfo<'info> { + &self.info + } +} + + +// === Anchor trait implementations === + +impl<'info, B> Accounts<'info, B> for LightMint<'info> { + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &'info [AccountInfo<'info>], + _ix_data: &[u8], + _bumps: &mut B, + _reallocs: &mut std::collections::BTreeSet, + ) -> Result { + if accounts.is_empty() { + return Err(ErrorCode::AccountNotEnoughKeys.into()); + } + let account = accounts[0].clone(); + *accounts = &accounts[1..]; + Ok(LightMint::new(account)) + } +} + +impl<'info> AccountsExit<'info> for LightMint<'info> { + fn exit(&self, _program_id: &Pubkey) -> Result<()> { + // No-op: zero-copy writes directly to account data + Ok(()) + } +} + +impl<'info> ToAccountMetas for LightMint<'info> { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let is_signer = is_signer.unwrap_or(self.info.is_signer); + if self.info.is_writable { + vec![AccountMeta::new(*self.info.key, is_signer)] + } else { + vec![AccountMeta::new_readonly(*self.info.key, is_signer)] + } + } +} + +impl<'info> ToAccountInfos<'info> for LightMint<'info> { + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'info> anchor_lang::Key for LightMint<'info> { + fn key(&self) -> Pubkey { + *self.info.key + } +} + +#[cfg(test)] +mod tests { + use std::{cell::RefCell as StdRefCell, rc::Rc}; + + use solana_pubkey::Pubkey as SolanaPubkey; + + use super::*; + + /// Helper to create a mock AccountInfo for testing + fn create_mock_account_info<'a>( + key: &'a SolanaPubkey, + owner: &'a SolanaPubkey, + lamports: &'a mut u64, + data: &'a mut [u8], + is_writable: bool, + is_signer: bool, + ) -> AccountInfo<'a> { + AccountInfo { + key, + lamports: Rc::new(StdRefCell::new(lamports)), + data: Rc::new(StdRefCell::new(data)), + owner, + rent_epoch: 0, + is_signer, + is_writable, + executable: false, + } + } + + #[test] + fn test_light_mint_new() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + assert_eq!(light_mint.key(), key); + } + + #[test] + fn test_deref_provides_account_info_access() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + + // Deref should provide access to AccountInfo fields + assert!(light_mint.is_writable); + assert!(!light_mint.is_signer); + } + + #[test] + fn test_load_fails_for_wrong_owner() { + let key = SolanaPubkey::new_unique(); + let wrong_owner = SolanaPubkey::new_unique(); // Not Light Token Program + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = + create_mock_account_info(&key, &wrong_owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + + let result = light_mint.load(); + assert!(matches!(result, Err(TokenError::InvalidMintOwner))); + } + + #[test] + fn test_load_fails_for_uninitialized() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + // Create data with is_initialized = 0 (uninitialized) + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + + let result = light_mint.load(); + // Will fail during validation + assert!(result.is_err()); + } + + #[test] + fn test_to_account_metas_writable() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + + let metas = light_mint.to_account_metas(None); + assert_eq!(metas.len(), 1); + assert_eq!(metas[0].pubkey, key); + assert!(metas[0].is_writable); + assert!(!metas[0].is_signer); + } + + #[test] + fn test_to_account_metas_readonly() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, false, false); + + let light_mint = LightMint::new(info); + + let metas = light_mint.to_account_metas(None); + assert_eq!(metas.len(), 1); + assert!(!metas[0].is_writable); + } + + #[test] + fn test_key_trait() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + assert_eq!(light_mint.key(), key); + } + + #[test] + fn test_is_initialized_false_for_wrong_owner() { + let key = SolanaPubkey::new_unique(); + let wrong_owner = SolanaPubkey::new_unique(); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + // Set is_initialized byte to 1 + data[IS_INITIALIZED_OFFSET] = 1; + + let info = + create_mock_account_info(&key, &wrong_owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + assert!(!light_mint.is_initialized()); + } + + #[test] + fn test_is_initialized_false_for_zero_byte() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + // is_initialized byte is 0 + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + assert!(!light_mint.is_initialized()); + } + + #[test] + fn test_is_initialized_true() { + let key = SolanaPubkey::new_unique(); + let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); + let mut lamports = 1_000_000u64; + let mut data = vec![0u8; 256]; + // Set is_initialized byte to 1 + data[IS_INITIALIZED_OFFSET] = 1; + + let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); + + let light_mint = LightMint::new(info); + assert!(light_mint.is_initialized()); + } +} diff --git a/program-libs/token-interface/src/state/mint/compressed_mint.rs b/program-libs/token-interface/src/state/mint/compressed_mint.rs index f1668c810f..5f6ba7ae8b 100644 --- a/program-libs/token-interface/src/state/mint/compressed_mint.rs +++ b/program-libs/token-interface/src/state/mint/compressed_mint.rs @@ -15,6 +15,10 @@ use crate::{ /// AccountType::Mint discriminator value pub const ACCOUNT_TYPE_MINT: u8 = 1; +/// Byte offset of `is_initialized` field in Mint account data. +/// Layout: 4 (COption prefix) + 32 (pubkey) + 8 (supply) + 1 (decimals) = 45 +pub const IS_INITIALIZED_OFFSET: usize = 45; + #[repr(C)] #[derive(Debug, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)] pub struct Mint { diff --git a/program-libs/token-interface/src/state/mint/mod.rs b/program-libs/token-interface/src/state/mint/mod.rs index 63484b75f5..5181a48c70 100644 --- a/program-libs/token-interface/src/state/mint/mod.rs +++ b/program-libs/token-interface/src/state/mint/mod.rs @@ -3,6 +3,11 @@ mod compressed_mint; mod top_up; mod zero_copy; +#[cfg(feature = "anchor")] +mod anchor_wrapper; + +#[cfg(feature = "anchor")] +pub use anchor_wrapper::*; pub use compressed_mint::*; pub use top_up::*; pub use zero_copy::*; diff --git a/program-libs/token-interface/src/state/mint/zero_copy.rs b/program-libs/token-interface/src/state/mint/zero_copy.rs index f62ae35d1e..462f4cc78b 100644 --- a/program-libs/token-interface/src/state/mint/zero_copy.rs +++ b/program-libs/token-interface/src/state/mint/zero_copy.rs @@ -10,7 +10,7 @@ use light_zero_copy::{ }; use spl_pod::solana_msg::msg; -use super::compressed_mint::{MintMetadata, ACCOUNT_TYPE_MINT}; +use super::compressed_mint::{MintMetadata, ACCOUNT_TYPE_MINT, IS_INITIALIZED_OFFSET}; use crate::{ instructions::mint_action::MintInstructionData, state::{ @@ -107,8 +107,7 @@ impl<'a> ZeroCopyNew<'a> for Mint { bytes: &'a mut [u8], config: Self::ZeroCopyConfig, ) -> Result<(Self::Output, &'a mut [u8]), light_zero_copy::errors::ZeroCopyError> { - // Check that the account is not already initialized (is_initialized byte at offset 45) - const IS_INITIALIZED_OFFSET: usize = 45; // 4 + 32 + 8 + 1 = 45 + // Check that the account is not already initialized if bytes.len() > IS_INITIALIZED_OFFSET && bytes[IS_INITIALIZED_OFFSET] != 0 { return Err(light_zero_copy::errors::ZeroCopyError::MemoryNotZeroed); } diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index 427b8dc614..a7bb84f2e6 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -447,7 +447,10 @@ pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { /// - `ID`: Program ID (from declare_id!) /// /// The struct should have fields named `fee_payer` (or `payer`) and `compression_config`. -#[proc_macro_derive(LightAccounts, attributes(light_account, instruction))] +/// When using `LightMint<'info>` directly in a struct, use ONLY `#[derive(LightAccounts)]` +/// (not `#[derive(Accounts, LightAccounts)]`), as Anchor's derive doesn't know about LightMint. +/// The LightAccounts macro will generate all necessary Anchor trait implementations. +#[proc_macro_derive(LightAccounts, attributes(light_account, instruction, account))] pub fn light_accounts_derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); into_token_stream(light_pdas::accounts::derive_light_accounts(input)) diff --git a/sdk-libs/macros/src/light_pdas/accounts/builder.rs b/sdk-libs/macros/src/light_pdas/accounts/builder.rs index 548962587f..e8b9332ec2 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/builder.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/builder.rs @@ -13,6 +13,7 @@ use super::{ pda::generate_pda_compress_blocks, token::TokenAccountsBuilder, }; +use crate::utils::to_snake_case; /// Builder for RentFree derive macro code generation. /// @@ -531,4 +532,251 @@ impl LightAccountsBuilder { } }) } + + /// Query: any field with `LightMint<'info>` type? + pub fn has_light_mint_type_fields(&self) -> bool { + self.parsed.has_light_mint_type_fields + } + + /// Generate Anchor trait implementations for structs with LightMint fields. + /// + /// When a struct contains `LightMint<'info>` fields, Anchor's `#[derive(Accounts)]` + /// fails because `LightMint` is not in Anchor's hardcoded primitive type whitelist. + /// This method generates the necessary trait implementations manually. + /// + /// Generated traits: + /// - `Accounts<'info, B>` - Account deserialization + /// - `AccountsExit<'info>` - Account serialization on exit + /// - `ToAccountInfos<'info>` - Convert to account info list + /// - `ToAccountMetas` - Convert to account meta list + /// - `Bumps` - Anchor Bumps trait for Context compatibility + pub fn generate_anchor_accounts_impl(&self) -> Result { + let struct_name = &self.parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); + let fields = &self.parsed.all_fields; + + // Generate field assignments for try_accounts + let field_assignments: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + let field_ty = &f.ty; + quote! { + let #field_ident: #field_ty = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + )?; + } + }) + .collect(); + + let field_names: Vec<&syn::Ident> = fields.iter().map(|f| &f.ident).collect(); + + // Generate exit calls - only for mutable fields + // Non-mutable fields like Program<'info, System> don't implement AccountsExit + let exit_calls: Vec = fields + .iter() + .filter(|f| f.is_mut) + .map(|f| { + let field_ident = &f.ident; + quote! { + anchor_lang::AccountsExit::exit(&self.#field_ident, program_id)?; + } + }) + .collect(); + + // Generate to_account_infos calls + let account_info_calls: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#field_ident)); + } + }) + .collect(); + + // Generate to_account_metas calls + let account_meta_calls: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + account_metas.extend(anchor_lang::ToAccountMetas::to_account_metas(&self.#field_ident, None)); + } + }) + .collect(); + + let field_count = fields.len(); + + // Generate the Bumps struct for Anchor compatibility + let bumps_struct_name = syn::Ident::new( + &format!("{}Bumps", struct_name), + struct_name.span(), + ); + + // Generate client accounts module name (snake_case of struct name) + let struct_name_str = struct_name.to_string(); + let client_module_name = syn::Ident::new( + &format!( + "__client_accounts_{}", + to_snake_case(&struct_name_str) + ), + struct_name.span(), + ); + + // Generate fields for the client accounts struct + // Each field maps to a Pubkey (accounts are represented by their keys in client code) + let client_struct_fields: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + pub #field_ident: anchor_lang::prelude::Pubkey, + } + }) + .collect(); + + // Generate ToAccountMetas for client struct + let client_to_account_metas: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + if f.is_mut { + if f.is_signer { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, true)); + } + } else { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, false)); + } + } + } else if f.is_signer { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, true)); + } + } else { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, false)); + } + } + }) + .collect(); + + Ok(quote! { + /// Auto-generated client accounts module for Anchor compatibility. + /// This module is required by Anchor's #[program] macro. + pub mod #client_module_name { + use super::*; + + /// Client-side representation of the accounts struct. + /// Used for building instructions in client code. + #[derive(Clone)] + pub struct #struct_name { + #(#client_struct_fields)* + } + + impl anchor_lang::ToAccountMetas for #struct_name { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + let mut account_metas = Vec::with_capacity(#field_count); + #(#client_to_account_metas)* + account_metas + } + } + } + + /// Auto-generated Bumps struct for Anchor compatibility. + #[derive(Default, Debug, Clone)] + pub struct #bumps_struct_name { + // Empty - bumps are handled separately for LightMint fields + } + + impl #bumps_struct_name { + /// Get a bump by name (returns None for LightMint-based structs). + pub fn get(&self, _name: &str) -> Option { + None + } + } + + /// Anchor Bumps trait implementation for Context compatibility. + #[automatically_derived] + impl #impl_generics anchor_lang::Bumps for #struct_name #ty_generics #where_clause { + type Bumps = #bumps_struct_name; + } + + /// IDL generation method required by Anchor's #[program] macro. + /// This generates a minimal IDL representation for LightMint-based structs. + impl #impl_generics #struct_name #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_accounts( + _accounts: &mut std::collections::BTreeMap, + _types: &mut std::collections::BTreeMap, + ) -> Vec { + // Generate minimal account info for each field + vec![ + #( + anchor_lang::idl::types::IdlInstructionAccountItem::Single( + anchor_lang::idl::types::IdlInstructionAccount { + name: stringify!(#field_names).into(), + docs: vec![], + writable: false, // Could be enhanced to check is_mut + signer: false, // Could be enhanced to check is_signer + optional: false, + address: None, + pda: None, + relations: vec![], + } + ) + ),* + ] + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::Accounts<'info, #bumps_struct_name> for #struct_name #ty_generics #where_clause { + fn try_accounts( + __program_id: &anchor_lang::prelude::Pubkey, + __accounts: &mut &'info [anchor_lang::prelude::AccountInfo<'info>], + __ix_data: &[u8], + __bumps: &mut #bumps_struct_name, + __reallocs: &mut std::collections::BTreeSet, + ) -> anchor_lang::Result { + #(#field_assignments)* + + Ok(Self { + #(#field_names),* + }) + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::AccountsExit<'info> for #struct_name #ty_generics #where_clause { + fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> { + #(#exit_calls)* + Ok(()) + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::ToAccountInfos<'info> for #struct_name #ty_generics #where_clause { + fn to_account_infos(&self) -> Vec> { + let mut account_infos = Vec::with_capacity(#field_count); + #(#account_info_calls)* + account_infos + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::ToAccountMetas for #struct_name #ty_generics #where_clause { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + let mut account_metas = Vec::with_capacity(#field_count); + #(#account_meta_calls)* + account_metas + } + } + }) + } } diff --git a/sdk-libs/macros/src/light_pdas/accounts/derive.rs b/sdk-libs/macros/src/light_pdas/accounts/derive.rs index ad5bbbe5f9..f0953d97ad 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/derive.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/derive.rs @@ -34,9 +34,22 @@ pub(super) fn derive_light_accounts(input: &DeriveInput) -> Result type fields. + // When present, we must generate Anchor trait implementations ourselves + // because Anchor's #[derive(Accounts)] doesn't know about LightMint. + let anchor_impls = if builder.has_light_mint_type_fields() { + builder.generate_anchor_accounts_impl()? + } else { + quote! {} + }; + // No instruction args = no-op impls (backwards compatibility) if !builder.has_instruction_args() { - return builder.generate_noop_impls(); + let noop_impls = builder.generate_noop_impls()?; + return Ok(quote! { + #anchor_impls + #noop_impls + }); } // Generate pre_init body for ALL account types (PDAs, mints, token accounts, ATAs) @@ -51,6 +64,7 @@ pub(super) fn derive_light_accounts(input: &DeriveInput) -> Result, + /// All fields in the struct (for generating Accounts trait when LightMint fields present). + pub all_fields: Vec, + /// True if any field has `LightMint<'info>` type. + /// When true, LightAccounts must generate the Accounts trait implementation + /// because Anchor's derive doesn't know about LightMint. + pub has_light_mint_type_fields: bool, +} + +/// A parsed field from the struct (for Accounts trait generation). +#[derive(Clone)] +pub(super) struct ParsedField { + pub ident: Ident, + pub ty: Type, + /// True if the field has a `#[account(mut)]` attribute + pub is_mut: bool, + /// True if the field is a Signer type + pub is_signer: bool, +} + +/// Check if a type is `LightMint<'info>`. +fn is_light_mint_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + return segment.ident == "LightMint"; + } + } + false +} + +/// Check if a type is `Signer<'info>`. +fn is_signer_type(ty: &Type) -> bool { + if let Type::Path(type_path) = ty { + if let Some(segment) = type_path.path.segments.last() { + return segment.ident == "Signer"; + } + } + false +} + +/// Check if field has `#[account(mut)]` attribute. +fn has_mut_attribute(attrs: &[syn::Attribute]) -> bool { + for attr in attrs { + if attr.path().is_ident("account") { + // Parse the attribute content to look for `mut` + let tokens = match &attr.meta { + syn::Meta::List(list) => list.tokens.clone(), + _ => continue, + }; + let token_str = tokens.to_string(); + if token_str.contains("mut") { + return true; + } + } + } + false } /// A field marked with #[light_account(init)] @@ -286,6 +341,8 @@ pub(super) fn parse_light_accounts_struct( let mut token_account_fields = Vec::new(); let mut ata_fields = Vec::new(); let mut infra_fields = InfraFields::default(); + let mut all_fields = Vec::new(); + let mut has_light_mint_type_fields = false; for field in fields { let field_ident = field @@ -300,6 +357,19 @@ pub(super) fn parse_light_accounts_struct( infra_fields.set(field_type, field_ident.clone())?; } + // Check if field type is LightMint<'info> + if is_light_mint_type(&field.ty) { + has_light_mint_type_fields = true; + } + + // Track all fields for Accounts trait generation + all_fields.push(ParsedField { + ident: field_ident.clone(), + ty: field.ty.clone(), + is_mut: has_mut_attribute(&field.attrs), + is_signer: is_signer_type(&field.ty), + }); + // Check for #[light_account(...)] - the unified syntax if let Some(light_account_field) = parse_light_account_attr(field, &field_ident, &direct_proof_arg)? @@ -337,5 +407,7 @@ pub(super) fn parse_light_accounts_struct( instruction_args, infra_fields, direct_proof_arg, + all_fields, + has_light_mint_type_fields, }) } diff --git a/sdk-libs/macros/src/light_pdas/program/instructions.rs b/sdk-libs/macros/src/light_pdas/program/instructions.rs index 79f2682c7e..1402889f6e 100644 --- a/sdk-libs/macros/src/light_pdas/program/instructions.rs +++ b/sdk-libs/macros/src/light_pdas/program/instructions.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Item, ItemMod, Result, Type}; +use syn::{Item, ItemMod, ItemStruct, Result, Type}; // Re-export types from parsing for external use pub use super::parsing::{ @@ -591,14 +591,30 @@ pub fn light_program_impl(_args: TokenStream, mut module: ItemMod) -> Result fields + // (Anchor's #[derive(Accounts)] doesn't recognize LightMint, so LightAccounts generates all Anchor traits) let mut pda_specs: Vec = Vec::new(); let mut token_specs: Vec = Vec::new(); let mut rentfree_struct_names = std::collections::HashSet::new(); let mut has_any_mint_fields = false; let mut has_any_ata_fields = false; - for item_struct in crate_ctx.structs_with_derive("Accounts") { + // Collect structs with either derive + let accounts_structs = crate_ctx.structs_with_derive("Accounts"); + let light_accounts_structs = crate_ctx.structs_with_derive("LightAccounts"); + + // Combine and deduplicate (some structs may have both derives) + let mut all_structs: Vec<&ItemStruct> = accounts_structs; + for s in light_accounts_structs { + if !all_structs.iter().any(|existing| existing.ident == s.ident) { + all_structs.push(s); + } + } + + for item_struct in all_structs { // Parse #[instruction(...)] attribute to get instruction arg names let instruction_args = parse_instruction_arg_names(&item_struct.attrs)?; diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs index 02ef7bfe52..43b662d2d0 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs @@ -1,6 +1,7 @@ use anchor_lang::prelude::*; use light_compressible::CreateAccountsProof; use light_sdk_macros::LightAccounts; +use light_token_interface::state::mint::LightMint; use crate::state::*; @@ -359,3 +360,68 @@ pub struct CreateMintWithMetadata<'info> { pub system_program: Program<'info, System>, } + +// ============================================================================= +// LightMint Wrapper Test +// ============================================================================= + +pub const LIGHT_MINT_TEST_SIGNER_SEED: &[u8] = b"light_mint_test_signer"; + +#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] +pub struct CreateMintWithLightMintParams { + pub create_accounts_proof: CreateAccountsProof, + pub mint_signer_bump: u8, +} + +/// Test instruction to verify LightMint wrapper works correctly. +/// Uses `LightMint<'info>` directly in the Accounts struct to demonstrate +/// type-safe access to mint data after CPI initialization. +/// +/// Note: Uses `#[derive(LightAccounts)]` alone (not with `#[derive(Accounts)]`) +/// because Anchor's derive doesn't know about `LightMint` type. +/// The `LightAccounts` macro generates the necessary Anchor trait implementations. +#[derive(LightAccounts)] +#[instruction(params: CreateMintWithLightMintParams)] +pub struct CreateMintWithLightMint<'info> { + #[account(mut)] + pub fee_payer: Signer<'info>, + + pub authority: Signer<'info>, + + /// CHECK: PDA derived from authority for mint signer + #[account( + seeds = [LIGHT_MINT_TEST_SIGNER_SEED, authority.key().as_ref()], + bump, + )] + pub mint_signer: UncheckedAccount<'info>, + + /// Mint account with zero-copy access after CPI initialization. + /// LightMint provides type-safe access to mint data without manual wrapping. + #[account(mut)] + #[light_account(init, + mint::signer = mint_signer, + mint::authority = fee_payer, + mint::decimals = 6, + mint::seeds = &[LIGHT_MINT_TEST_SIGNER_SEED, self.authority.to_account_info().key.as_ref()], + mint::bump = params.mint_signer_bump + )] + pub cmint: LightMint<'info>, + + /// CHECK: Compression config + pub compression_config: AccountInfo<'info>, + + /// CHECK: CToken config + pub light_token_compressible_config: AccountInfo<'info>, + + /// CHECK: CToken rent sponsor + #[account(mut)] + pub rent_sponsor: AccountInfo<'info>, + + /// CHECK: CToken program + pub light_token_program: AccountInfo<'info>, + + /// CHECK: CToken CPI authority + pub light_token_cpi_authority: AccountInfo<'info>, + + pub system_program: Program<'info, System>, +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index 826aefb20c..8e08e74ab7 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -265,8 +265,9 @@ pub mod csdk_anchor_full_derived_test { D9U64Params, }, instruction_accounts::{ - CreateMintWithMetadata, CreateMintWithMetadataParams, CreatePdasAndMintAuto, - CreateThreeMints, CreateThreeMintsParams, CreateTwoMints, CreateTwoMintsParams, + CreateMintWithLightMint, CreateMintWithLightMintParams, CreateMintWithMetadata, + CreateMintWithMetadataParams, CreatePdasAndMintAuto, CreateThreeMints, + CreateThreeMintsParams, CreateTwoMints, CreateTwoMintsParams, }, instructions::d10_token_accounts::{ D10SingleAta, D10SingleAtaParams, D10SingleVault, D10SingleVaultParams, @@ -411,6 +412,33 @@ pub mod csdk_anchor_full_derived_test { Ok(()) } + /// Test instruction demonstrating type-safe mint access after CPI initialization. + /// Uses LightMint directly in the Accounts struct for zero-copy access. + pub fn create_mint_with_light_mint<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMintWithLightMint<'info>>, + _params: CreateMintWithLightMintParams, + ) -> Result<()> { + // Direct zero-copy access via LightMint field + // No wrapping needed - cmint is already LightMint<'info> + let mint_data = ctx.accounts.cmint.load().map_err(|e| { + solana_msg::msg!("Failed to load mint: {:?}", e); + anchor_lang::error::ErrorCode::AccountDidNotDeserialize + })?; + + // Verify the mint was initialized correctly using zero-copy accessors + solana_msg::msg!("Mint zero-copy load succeeded!"); + solana_msg::msg!(" decimals: {}", mint_data.decimals); + solana_msg::msg!(" is_initialized: {}", mint_data.is_initialized()); + solana_msg::msg!(" supply: {}", u64::from(mint_data.supply)); + + // Verify expected values from the #[light_account(init)] attributes + assert!(mint_data.is_initialized(), "Mint should be initialized"); + assert_eq!(mint_data.decimals, 6, "Decimals should be 6"); + assert_eq!(u64::from(mint_data.supply), 0, "Initial supply should be 0"); + + Ok(()) + } + /// AMM initialize instruction with all light account markers. /// Tests: 2x #[light_account(init)], 2x #[light_account(token)], 1x #[light_account(init)], /// CreateTokenAccountCpi.rent_free(), CreateTokenAtaCpi.rent_free(), MintToCpi diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs new file mode 100644 index 0000000000..da8c00ddfc --- /dev/null +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs @@ -0,0 +1,136 @@ +//! Integration test for LightMint wrapper functionality. +//! +//! Tests that mint data can be accessed after CPI initialization using +//! type-safe deserialization patterns. + +use anchor_lang::{InstructionData, ToAccountMetas}; +use csdk_anchor_full_derived_test::instruction_accounts::{ + CreateMintWithLightMintParams, LIGHT_MINT_TEST_SIGNER_SEED, +}; +use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; +use light_program_test::{ + program_test::{setup_mock_program_data, LightProgramTest}, + ProgramTestConfig, Rpc, +}; +use light_sdk_types::LIGHT_TOKEN_PROGRAM_ID; +use light_token::instruction::{COMPRESSIBLE_CONFIG_V1, RENT_SPONSOR}; +use solana_instruction::Instruction; +use solana_keypair::Keypair; +use solana_pubkey::Pubkey; +use solana_signer::Signer; + +/// Test creating a mint and verifying the handler can access mint data after CPI. +/// This demonstrates the pattern for accessing type-safe mint data after initialization. +#[tokio::test] +async fn test_create_mint_with_light_mint_wrapper() { + use light_token::instruction::find_mint_address as find_cmint_address; + + let program_id = csdk_anchor_full_derived_test::ID; + let mut config = ProgramTestConfig::new_v2( + true, + Some(vec![("csdk_anchor_full_derived_test", program_id)]), + ); + config = config.with_light_protocol_events(); + + let mut rpc = LightProgramTest::new(config).await.unwrap(); + let payer = rpc.get_payer().insecure_clone(); + + let program_data_pda = setup_mock_program_data(&mut rpc, &payer, &program_id); + + // Initialize rent-free config for the test program + let (init_config_ix, config_pda) = light_client::interface::InitializeRentFreeConfig::new( + &program_id, + &payer.pubkey(), + &program_data_pda, + RENT_SPONSOR, + payer.pubkey(), + ) + .build(); + + rpc.create_and_send_transaction(&[init_config_ix], &payer.pubkey(), &[&payer]) + .await + .expect("Initialize config should succeed"); + + let authority = Keypair::new(); + + // Derive PDA for mint signer + let (mint_signer_pda, mint_signer_bump) = Pubkey::find_program_address( + &[LIGHT_MINT_TEST_SIGNER_SEED, authority.pubkey().as_ref()], + &program_id, + ); + + // Derive mint PDA + let (cmint_pda, _) = find_cmint_address(&mint_signer_pda); + + // Get proof for the mint + let proof_result = get_create_accounts_proof( + &rpc, + &program_id, + vec![CreateAccountsProofInput::mint(mint_signer_pda)], + ) + .await + .unwrap(); + + // Build the instruction + let accounts = csdk_anchor_full_derived_test::accounts::CreateMintWithLightMint { + fee_payer: payer.pubkey(), + authority: authority.pubkey(), + mint_signer: mint_signer_pda, + cmint: cmint_pda, + compression_config: config_pda, + light_token_compressible_config: COMPRESSIBLE_CONFIG_V1, + rent_sponsor: RENT_SPONSOR, + light_token_program: LIGHT_TOKEN_PROGRAM_ID.into(), + light_token_cpi_authority: light_token_types::CPI_AUTHORITY_PDA.into(), + system_program: solana_sdk::system_program::ID, + }; + + let instruction_data = csdk_anchor_full_derived_test::instruction::CreateMintWithLightMint { + _params: CreateMintWithLightMintParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }, + }; + + let instruction = Instruction { + program_id, + accounts: [ + accounts.to_account_metas(None), + proof_result.remaining_accounts, + ] + .concat(), + data: instruction_data.data(), + }; + + // Execute the transaction - this will: + // 1. Create the mint via CPI + // 2. In the handler, access and verify the mint data + rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) + .await + .expect("CreateMintWithLightMint should succeed"); + + // Verify mint exists on-chain with expected data + let cmint_account = rpc + .get_account(cmint_pda) + .await + .unwrap() + .expect("Mint should exist on-chain"); + + // Deserialize and verify + use borsh::BorshDeserialize; + use light_token_interface::state::Mint; + let mint: Mint = + Mint::try_from_slice(&cmint_account.data[..]).expect("Failed to deserialize Mint"); + + // Verify values match what was specified in #[light_account(init)] attributes + assert_eq!(mint.base.decimals, 6, "Mint should have 6 decimals"); + assert!(mint.base.is_initialized, "Mint should be initialized"); + assert_eq!(mint.base.supply, 0, "Initial supply should be 0"); + + // Verify mint authority is the fee_payer (as specified in mint::authority = fee_payer) + assert_eq!( + mint.base.mint_authority, + Some(payer.pubkey().to_bytes().into()), + "Mint authority should be fee_payer" + ); +} diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs index 765f1880d3..69fcb6b4da 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/mod.rs @@ -1 +1,2 @@ +mod light_mint_test; mod metadata_test; From 3a8923ce73fc0d7af376a85375ee8bcba3c1d8cb Mon Sep 17 00:00:00 2001 From: ananas Date: Fri, 23 Jan 2026 09:45:17 +0000 Subject: [PATCH 2/2] works with custom LightAccounts derive --- .../src/state/mint/anchor_wrapper.rs | 164 +++--- .../token-interface/src/state/mint/mod.rs | 2 +- sdk-libs/macros/src/lib.rs | 4 +- .../macros/src/light_pdas/accounts/builder.rs | 492 +++++++++--------- .../macros/src/light_pdas/accounts/derive.rs | 13 +- .../macros/src/light_pdas/accounts/parse.rs | 77 +-- .../src/light_pdas/program/instructions.rs | 4 +- .../src/instruction_accounts.rs | 50 +- .../csdk-anchor-full-derived-test/src/lib.rs | 14 +- .../tests/mint/light_mint_test.rs | 23 +- sdk-tests/single-mint-test/Cargo.toml | 1 + sdk-tests/single-mint-test/src/lib.rs | 9 +- 12 files changed, 457 insertions(+), 396 deletions(-) diff --git a/program-libs/token-interface/src/state/mint/anchor_wrapper.rs b/program-libs/token-interface/src/state/mint/anchor_wrapper.rs index 307fbe5840..215f9bc76b 100644 --- a/program-libs/token-interface/src/state/mint/anchor_wrapper.rs +++ b/program-libs/token-interface/src/state/mint/anchor_wrapper.rs @@ -1,29 +1,33 @@ //! Anchor wrapper for Light Protocol mint accounts. //! -//! Provides `LightMint<'info>` - a type-safe wrapper for mint accounts that are -//! initialized via CPI to the Light Token Program. This wrapper allows Anchor programs -//! to access mint data after CPI initialization completes. +//! Provides `AccountLoader<'info, T>` - a type-safe wrapper for Light Protocol accounts that +//! provides zero-copy access. Named `AccountLoader` for API familiarity with Anchor users. +//! +//! # Key Insight: Why This Works with Anchor +//! +//! Anchor's codegen for ALL field types calls `Accounts::try_accounts()`. Our `AccountLoader` +//! implements this trait with no-op validation, allowing the account to be uninitialized +//! before CPI. Validation happens lazily when `load()` is called. //! //! # Usage //! -//! Use `LightMint<'info>` to wrap an account and access mint data via zero-copy: +//! Use `AccountLoader<'info, Mint>` in Anchor accounts structs: //! //! ```ignore -//! use light_token_interface::state::mint::LightMint; +//! use light_token_interface::state::mint::{AccountLoader, Mint}; //! -//! #[derive(Accounts, LightAccounts)] +//! #[derive(Accounts, LightAccounts)] // Both derives work together! +//! #[instruction(params: CreateMintParams)] //! pub struct CreateMint<'info> { //! #[account(mut)] //! #[light_account(init, mint::signer = mint_signer, ...)] -//! pub cmint: UncheckedAccount<'info>, +//! pub mint: AccountLoader<'info, Mint>, //! } //! //! pub fn handler(ctx: Context) -> Result<()> { -//! // After CPI completes, wrap and access mint data via zero-copy: -//! let light_mint = LightMint::new(ctx.accounts.cmint.to_account_info()); -//! let mint_data = light_mint.load()?; +//! // After CPI completes, access mint data via zero-copy: +//! let mint_data = ctx.accounts.mint.load()?; //! msg!("Decimals: {}", mint_data.decimals); -//! msg!("Initialized: {}", mint_data.is_initialized()); //! Ok(()) //! } //! ``` @@ -35,22 +39,34 @@ //! directly to account data. Since zero-copy writes directly, `AccountsExit::exit()` //! is a no-op. -use std::ops::Deref; +use std::{marker::PhantomData, ops::Deref}; use anchor_lang::prelude::*; use super::{Mint, ZMint, ZMintMut, IS_INITIALIZED_OFFSET}; use crate::{TokenError, LIGHT_TOKEN_PROGRAM_ID}; -/// A wrapper around `AccountInfo` for Light Protocol mint accounts. +/// Marker trait for types that can be loaded via AccountLoader. +/// +/// This trait marks types that have zero-copy serialization support +/// and can be accessed through the AccountLoader pattern. +pub trait LightZeroCopy {} + +impl LightZeroCopy for Mint {} + +/// Zero-copy account loader for Light Protocol accounts. +/// +/// Named `AccountLoader` for API familiarity with Anchor users. +/// Unlike Anchor's AccountLoader, this performs NO validation in `try_accounts` +/// - validation happens lazily when `load()` is called. /// -/// This struct provides type-safe zero-copy access to mint data after CPI initialization. -/// Each call to `load()` or `load_mut()` creates a fresh zero-copy view into the account -/// data without allocation. +/// # Type Parameter +/// +/// - `T`: The account type to load (e.g., `Mint`). Must implement `LightZeroCopy`. /// /// # Anchor Integration /// -/// `LightMint` implements all required Anchor traits, allowing it to be used +/// `AccountLoader` implements all required Anchor traits, allowing it to be used /// in `#[derive(Accounts)]` structs. During account deserialization /// (`try_accounts`), no validation is performed - this allows the account to /// be uninitialized before CPI. Validation happens when `load()` is called. @@ -63,12 +79,13 @@ use crate::{TokenError, LIGHT_TOKEN_PROGRAM_ID}; /// - Writes via `load_mut()` are immediately reflected in account data /// - `AccountsExit::exit()` is a no-op since there's nothing to serialize #[derive(Debug, Clone)] -pub struct LightMint<'info> { +pub struct AccountLoader<'info, T> { info: AccountInfo<'info>, + _phantom: PhantomData, } -impl<'info> LightMint<'info> { - /// Creates a new `LightMint` wrapper from an `AccountInfo`. +impl<'info, T> AccountLoader<'info, T> { + /// Creates a new `AccountLoader` wrapper from an `AccountInfo`. /// /// This does not perform any validation - the account may be uninitialized. /// Validation occurs when `load()` is called. @@ -77,14 +94,36 @@ impl<'info> LightMint<'info> { /// /// ```ignore /// // In an Anchor instruction handler, after CPI: - /// let light_mint = LightMint::new(ctx.accounts.cmint.to_account_info()); - /// let mint_data = light_mint.load()?; + /// let loader = AccountLoader::::new(ctx.accounts.mint.to_account_info()); + /// let mint_data = loader.load()?; /// assert!(mint_data.is_initialized()); /// ``` pub fn new(info: AccountInfo<'info>) -> Self { - Self { info } + Self { + info, + _phantom: PhantomData, + } } + /// Returns a clone of the underlying `AccountInfo`. + /// + /// This is required for macro-generated code that calls `.to_account_info()` + /// on account fields in `#[derive(LightAccounts)]` structs. + pub fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } + + /// Returns a reference to the account's public key. + pub fn key(&self) -> &Pubkey { + self.info.key + } +} + +// ============================================================================= +// Mint-specific methods +// ============================================================================= + +impl<'info> AccountLoader<'info, Mint> { /// Loads and validates the mint data, returning an immutable zero-copy view. /// /// This method: @@ -149,14 +188,6 @@ impl<'info> LightMint<'info> { Ok(mint) } - /// Returns a clone of the underlying `AccountInfo`. - /// - /// This is required for macro-generated code that calls `.to_account_info()` - /// on account fields in `#[derive(LightAccounts)]` structs. - pub fn to_account_info(&self) -> AccountInfo<'info> { - self.info.clone() - } - /// Returns true if the mint account appears to be initialized. /// /// This performs a quick check without fully parsing the account. @@ -183,9 +214,11 @@ impl<'info> LightMint<'info> { } } -// === Deref, AsRef, and ToAccountInfo implementations === +// ============================================================================= +// Deref, AsRef, and ToAccountInfo implementations +// ============================================================================= -impl<'info> Deref for LightMint<'info> { +impl<'info, T> Deref for AccountLoader<'info, T> { type Target = AccountInfo<'info>; fn deref(&self) -> &Self::Target { @@ -193,16 +226,17 @@ impl<'info> Deref for LightMint<'info> { } } -impl<'info> AsRef> for LightMint<'info> { +impl<'info, T> AsRef> for AccountLoader<'info, T> { fn as_ref(&self) -> &AccountInfo<'info> { &self.info } } +// ============================================================================= +// Anchor trait implementations +// ============================================================================= -// === Anchor trait implementations === - -impl<'info, B> Accounts<'info, B> for LightMint<'info> { +impl<'info, T, B> Accounts<'info, B> for AccountLoader<'info, T> { fn try_accounts( _program_id: &Pubkey, accounts: &mut &'info [AccountInfo<'info>], @@ -210,23 +244,25 @@ impl<'info, B> Accounts<'info, B> for LightMint<'info> { _bumps: &mut B, _reallocs: &mut std::collections::BTreeSet, ) -> Result { + // NO validation - just grab AccountInfo + // This allows the account to be uninitialized before CPI if accounts.is_empty() { return Err(ErrorCode::AccountNotEnoughKeys.into()); } let account = accounts[0].clone(); *accounts = &accounts[1..]; - Ok(LightMint::new(account)) + Ok(AccountLoader::new(account)) } } -impl<'info> AccountsExit<'info> for LightMint<'info> { +impl<'info, T> AccountsExit<'info> for AccountLoader<'info, T> { fn exit(&self, _program_id: &Pubkey) -> Result<()> { // No-op: zero-copy writes directly to account data Ok(()) } } -impl<'info> ToAccountMetas for LightMint<'info> { +impl ToAccountMetas for AccountLoader<'_, T> { fn to_account_metas(&self, is_signer: Option) -> Vec { let is_signer = is_signer.unwrap_or(self.info.is_signer); if self.info.is_writable { @@ -237,13 +273,13 @@ impl<'info> ToAccountMetas for LightMint<'info> { } } -impl<'info> ToAccountInfos<'info> for LightMint<'info> { +impl<'info, T> ToAccountInfos<'info> for AccountLoader<'info, T> { fn to_account_infos(&self) -> Vec> { vec![self.info.clone()] } } -impl<'info> anchor_lang::Key for LightMint<'info> { +impl anchor_lang::Key for AccountLoader<'_, T> { fn key(&self) -> Pubkey { *self.info.key } @@ -279,7 +315,7 @@ mod tests { } #[test] - fn test_light_mint_new() { + fn test_account_loader_new() { let key = SolanaPubkey::new_unique(); let owner = SolanaPubkey::new_from_array(LIGHT_TOKEN_PROGRAM_ID); let mut lamports = 1_000_000u64; @@ -287,8 +323,8 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); - assert_eq!(light_mint.key(), key); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); + assert_eq!(*loader.key(), key); } #[test] @@ -300,11 +336,11 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); // Deref should provide access to AccountInfo fields - assert!(light_mint.is_writable); - assert!(!light_mint.is_signer); + assert!(loader.is_writable); + assert!(!loader.is_signer); } #[test] @@ -317,9 +353,9 @@ mod tests { let info = create_mock_account_info(&key, &wrong_owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); - let result = light_mint.load(); + let result = loader.load(); assert!(matches!(result, Err(TokenError::InvalidMintOwner))); } @@ -333,9 +369,9 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); - let result = light_mint.load(); + let result = loader.load(); // Will fail during validation assert!(result.is_err()); } @@ -349,9 +385,9 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); - let metas = light_mint.to_account_metas(None); + let metas = loader.to_account_metas(None); assert_eq!(metas.len(), 1); assert_eq!(metas[0].pubkey, key); assert!(metas[0].is_writable); @@ -367,9 +403,9 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, false, false); - let light_mint = LightMint::new(info); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); - let metas = light_mint.to_account_metas(None); + let metas = loader.to_account_metas(None); assert_eq!(metas.len(), 1); assert!(!metas[0].is_writable); } @@ -383,8 +419,8 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); - assert_eq!(light_mint.key(), key); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); + assert_eq!(anchor_lang::Key::key(&loader), key); } #[test] @@ -399,8 +435,8 @@ mod tests { let info = create_mock_account_info(&key, &wrong_owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); - assert!(!light_mint.is_initialized()); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); + assert!(!loader.is_initialized()); } #[test] @@ -413,8 +449,8 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); - assert!(!light_mint.is_initialized()); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); + assert!(!loader.is_initialized()); } #[test] @@ -428,7 +464,7 @@ mod tests { let info = create_mock_account_info(&key, &owner, &mut lamports, &mut data, true, false); - let light_mint = LightMint::new(info); - assert!(light_mint.is_initialized()); + let loader: AccountLoader<'_, Mint> = AccountLoader::new(info); + assert!(loader.is_initialized()); } } diff --git a/program-libs/token-interface/src/state/mint/mod.rs b/program-libs/token-interface/src/state/mint/mod.rs index 5181a48c70..4b8be8f765 100644 --- a/program-libs/token-interface/src/state/mint/mod.rs +++ b/program-libs/token-interface/src/state/mint/mod.rs @@ -7,7 +7,7 @@ mod zero_copy; mod anchor_wrapper; #[cfg(feature = "anchor")] -pub use anchor_wrapper::*; +pub use anchor_wrapper::{AccountLoader, LightZeroCopy}; pub use compressed_mint::*; pub use top_up::*; pub use zero_copy::*; diff --git a/sdk-libs/macros/src/lib.rs b/sdk-libs/macros/src/lib.rs index a7bb84f2e6..329d36b9e4 100644 --- a/sdk-libs/macros/src/lib.rs +++ b/sdk-libs/macros/src/lib.rs @@ -447,8 +447,8 @@ pub fn derive_light_rent_sponsor(input: TokenStream) -> TokenStream { /// - `ID`: Program ID (from declare_id!) /// /// The struct should have fields named `fee_payer` (or `payer`) and `compression_config`. -/// When using `LightMint<'info>` directly in a struct, use ONLY `#[derive(LightAccounts)]` -/// (not `#[derive(Accounts, LightAccounts)]`), as Anchor's derive doesn't know about LightMint. +/// When using `AccountLoader<'info, Mint>` directly in a struct, use ONLY `#[derive(LightAccounts)]` +/// (not `#[derive(Accounts, LightAccounts)]`), as Anchor's derive doesn't know about AccountLoader. /// The LightAccounts macro will generate all necessary Anchor trait implementations. #[proc_macro_derive(LightAccounts, attributes(light_account, instruction, account))] pub fn light_accounts_derive(input: TokenStream) -> TokenStream { diff --git a/sdk-libs/macros/src/light_pdas/accounts/builder.rs b/sdk-libs/macros/src/light_pdas/accounts/builder.rs index e8b9332ec2..848ce9e653 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/builder.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/builder.rs @@ -13,7 +13,6 @@ use super::{ pda::generate_pda_compress_blocks, token::TokenAccountsBuilder, }; -use crate::utils::to_snake_case; /// Builder for RentFree derive macro code generation. /// @@ -228,6 +227,250 @@ impl LightAccountsBuilder { self.parsed.instruction_args.is_some() } + /// Query: any field with `AccountLoader<'info, Mint>` type? + /// + /// When true, we need to generate Anchor trait implementations ourselves + /// because Anchor's `#[derive(Accounts)]` doesn't know about this type. + pub fn has_light_loader_fields(&self) -> bool { + self.parsed.has_light_loader_fields + } + + /// Generate Anchor trait implementations for structs with AccountLoader<'info, Mint> fields. + /// + /// When a struct contains these fields, Anchor's `#[derive(Accounts)]` fails + /// because the type isn't in Anchor's hardcoded primitive type whitelist. + /// This method generates the necessary trait implementations manually. + /// + /// Generated traits: + /// - `Accounts<'info, B>` - Account deserialization + /// - `AccountsExit<'info>` - Account serialization on exit + /// - `ToAccountInfos<'info>` - Convert to account info list + /// - `ToAccountMetas` - Convert to account meta list + /// - `Bumps` - Anchor Bumps trait for Context compatibility + pub fn generate_anchor_accounts_impl(&self) -> Result { + let struct_name = &self.parsed.struct_name; + let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); + let fields = &self.parsed.all_fields; + + // Generate field assignments for try_accounts + let field_assignments: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + let field_ty = &f.ty; + quote! { + let #field_ident: #field_ty = anchor_lang::Accounts::try_accounts( + __program_id, + __accounts, + __ix_data, + __bumps, + __reallocs, + )?; + } + }) + .collect(); + + let field_names: Vec<&syn::Ident> = fields.iter().map(|f| &f.ident).collect(); + + // Generate exit calls - only for mutable fields + let exit_calls: Vec = fields + .iter() + .filter(|f| f.is_mut) + .map(|f| { + let field_ident = &f.ident; + quote! { + anchor_lang::AccountsExit::exit(&self.#field_ident, program_id)?; + } + }) + .collect(); + + // Generate to_account_infos calls + let account_info_calls: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#field_ident)); + } + }) + .collect(); + + // Generate to_account_metas calls + let account_meta_calls: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + account_metas.extend(anchor_lang::ToAccountMetas::to_account_metas(&self.#field_ident, None)); + } + }) + .collect(); + + let field_count = fields.len(); + + // Generate the Bumps struct for Anchor compatibility + let bumps_struct_name = + syn::Ident::new(&format!("{}Bumps", struct_name), struct_name.span()); + + // Generate client accounts module name (snake_case of struct name) + // This is required because Anchor's #[program] macro references it. + let struct_name_str = struct_name.to_string(); + let client_module_name = syn::Ident::new( + &format!( + "__client_accounts_{}", + crate::utils::to_snake_case(&struct_name_str) + ), + struct_name.span(), + ); + + // Generate fields for the client accounts struct + let client_struct_fields: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + quote! { + pub #field_ident: anchor_lang::prelude::Pubkey, + } + }) + .collect(); + + // Generate ToAccountMetas for client struct + let client_to_account_metas: Vec = fields + .iter() + .map(|f| { + let field_ident = &f.ident; + if f.is_mut { + if f.is_signer { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, true)); + } + } else { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, false)); + } + } + } else if f.is_signer { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, true)); + } + } else { + quote! { + account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, false)); + } + } + }) + .collect(); + + Ok(quote! { + /// Auto-generated client accounts module for Anchor compatibility. + /// Required by Anchor's #[program] macro. + pub mod #client_module_name { + use super::*; + + /// Client-side representation of the accounts struct. + #[derive(Clone)] + pub struct #struct_name { + #(#client_struct_fields)* + } + + impl anchor_lang::ToAccountMetas for #struct_name { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + let mut account_metas = Vec::with_capacity(#field_count); + #(#client_to_account_metas)* + account_metas + } + } + } + + /// Auto-generated Bumps struct for Anchor compatibility. + #[derive(Default, Debug, Clone)] + pub struct #bumps_struct_name { + // Empty - bumps are handled separately for AccountLoader fields + } + + impl #bumps_struct_name { + /// Get a bump by name (returns None for AccountLoader-based structs). + pub fn get(&self, _name: &str) -> Option { + None + } + } + + /// Anchor Bumps trait implementation for Context compatibility. + #[automatically_derived] + impl #impl_generics anchor_lang::Bumps for #struct_name #ty_generics #where_clause { + type Bumps = #bumps_struct_name; + } + + /// IDL generation method required by Anchor's #[program] macro. + impl #impl_generics #struct_name #ty_generics #where_clause { + pub fn __anchor_private_gen_idl_accounts( + _accounts: &mut std::collections::BTreeMap, + _types: &mut std::collections::BTreeMap, + ) -> Vec { + vec![ + #( + anchor_lang::idl::types::IdlInstructionAccountItem::Single( + anchor_lang::idl::types::IdlInstructionAccount { + name: stringify!(#field_names).into(), + docs: vec![], + writable: false, + signer: false, + optional: false, + address: None, + pda: None, + relations: vec![], + } + ) + ),* + ] + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::Accounts<'info, #bumps_struct_name> for #struct_name #ty_generics #where_clause { + fn try_accounts( + __program_id: &anchor_lang::prelude::Pubkey, + __accounts: &mut &'info [anchor_lang::prelude::AccountInfo<'info>], + __ix_data: &[u8], + __bumps: &mut #bumps_struct_name, + __reallocs: &mut std::collections::BTreeSet, + ) -> anchor_lang::Result { + #(#field_assignments)* + + Ok(Self { + #(#field_names),* + }) + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::AccountsExit<'info> for #struct_name #ty_generics #where_clause { + fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> { + #(#exit_calls)* + Ok(()) + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::ToAccountInfos<'info> for #struct_name #ty_generics #where_clause { + fn to_account_infos(&self) -> Vec> { + let mut account_infos = Vec::with_capacity(#field_count); + #(#account_info_calls)* + account_infos + } + } + + #[automatically_derived] + impl #impl_generics anchor_lang::ToAccountMetas for #struct_name #ty_generics #where_clause { + fn to_account_metas(&self, _is_signer: Option) -> Vec { + let mut account_metas = Vec::with_capacity(#field_count); + #(#account_meta_calls)* + account_metas + } + } + }) + } + /// Generate no-op trait impls (for backwards compatibility). pub fn generate_noop_impls(&self) -> Result { let struct_name = &self.parsed.struct_name; @@ -532,251 +775,4 @@ impl LightAccountsBuilder { } }) } - - /// Query: any field with `LightMint<'info>` type? - pub fn has_light_mint_type_fields(&self) -> bool { - self.parsed.has_light_mint_type_fields - } - - /// Generate Anchor trait implementations for structs with LightMint fields. - /// - /// When a struct contains `LightMint<'info>` fields, Anchor's `#[derive(Accounts)]` - /// fails because `LightMint` is not in Anchor's hardcoded primitive type whitelist. - /// This method generates the necessary trait implementations manually. - /// - /// Generated traits: - /// - `Accounts<'info, B>` - Account deserialization - /// - `AccountsExit<'info>` - Account serialization on exit - /// - `ToAccountInfos<'info>` - Convert to account info list - /// - `ToAccountMetas` - Convert to account meta list - /// - `Bumps` - Anchor Bumps trait for Context compatibility - pub fn generate_anchor_accounts_impl(&self) -> Result { - let struct_name = &self.parsed.struct_name; - let (impl_generics, ty_generics, where_clause) = self.parsed.generics.split_for_impl(); - let fields = &self.parsed.all_fields; - - // Generate field assignments for try_accounts - let field_assignments: Vec = fields - .iter() - .map(|f| { - let field_ident = &f.ident; - let field_ty = &f.ty; - quote! { - let #field_ident: #field_ty = anchor_lang::Accounts::try_accounts( - __program_id, - __accounts, - __ix_data, - __bumps, - __reallocs, - )?; - } - }) - .collect(); - - let field_names: Vec<&syn::Ident> = fields.iter().map(|f| &f.ident).collect(); - - // Generate exit calls - only for mutable fields - // Non-mutable fields like Program<'info, System> don't implement AccountsExit - let exit_calls: Vec = fields - .iter() - .filter(|f| f.is_mut) - .map(|f| { - let field_ident = &f.ident; - quote! { - anchor_lang::AccountsExit::exit(&self.#field_ident, program_id)?; - } - }) - .collect(); - - // Generate to_account_infos calls - let account_info_calls: Vec = fields - .iter() - .map(|f| { - let field_ident = &f.ident; - quote! { - account_infos.extend(anchor_lang::ToAccountInfos::to_account_infos(&self.#field_ident)); - } - }) - .collect(); - - // Generate to_account_metas calls - let account_meta_calls: Vec = fields - .iter() - .map(|f| { - let field_ident = &f.ident; - quote! { - account_metas.extend(anchor_lang::ToAccountMetas::to_account_metas(&self.#field_ident, None)); - } - }) - .collect(); - - let field_count = fields.len(); - - // Generate the Bumps struct for Anchor compatibility - let bumps_struct_name = syn::Ident::new( - &format!("{}Bumps", struct_name), - struct_name.span(), - ); - - // Generate client accounts module name (snake_case of struct name) - let struct_name_str = struct_name.to_string(); - let client_module_name = syn::Ident::new( - &format!( - "__client_accounts_{}", - to_snake_case(&struct_name_str) - ), - struct_name.span(), - ); - - // Generate fields for the client accounts struct - // Each field maps to a Pubkey (accounts are represented by their keys in client code) - let client_struct_fields: Vec = fields - .iter() - .map(|f| { - let field_ident = &f.ident; - quote! { - pub #field_ident: anchor_lang::prelude::Pubkey, - } - }) - .collect(); - - // Generate ToAccountMetas for client struct - let client_to_account_metas: Vec = fields - .iter() - .map(|f| { - let field_ident = &f.ident; - if f.is_mut { - if f.is_signer { - quote! { - account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, true)); - } - } else { - quote! { - account_metas.push(anchor_lang::prelude::AccountMeta::new(self.#field_ident, false)); - } - } - } else if f.is_signer { - quote! { - account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, true)); - } - } else { - quote! { - account_metas.push(anchor_lang::prelude::AccountMeta::new_readonly(self.#field_ident, false)); - } - } - }) - .collect(); - - Ok(quote! { - /// Auto-generated client accounts module for Anchor compatibility. - /// This module is required by Anchor's #[program] macro. - pub mod #client_module_name { - use super::*; - - /// Client-side representation of the accounts struct. - /// Used for building instructions in client code. - #[derive(Clone)] - pub struct #struct_name { - #(#client_struct_fields)* - } - - impl anchor_lang::ToAccountMetas for #struct_name { - fn to_account_metas(&self, _is_signer: Option) -> Vec { - let mut account_metas = Vec::with_capacity(#field_count); - #(#client_to_account_metas)* - account_metas - } - } - } - - /// Auto-generated Bumps struct for Anchor compatibility. - #[derive(Default, Debug, Clone)] - pub struct #bumps_struct_name { - // Empty - bumps are handled separately for LightMint fields - } - - impl #bumps_struct_name { - /// Get a bump by name (returns None for LightMint-based structs). - pub fn get(&self, _name: &str) -> Option { - None - } - } - - /// Anchor Bumps trait implementation for Context compatibility. - #[automatically_derived] - impl #impl_generics anchor_lang::Bumps for #struct_name #ty_generics #where_clause { - type Bumps = #bumps_struct_name; - } - - /// IDL generation method required by Anchor's #[program] macro. - /// This generates a minimal IDL representation for LightMint-based structs. - impl #impl_generics #struct_name #ty_generics #where_clause { - pub fn __anchor_private_gen_idl_accounts( - _accounts: &mut std::collections::BTreeMap, - _types: &mut std::collections::BTreeMap, - ) -> Vec { - // Generate minimal account info for each field - vec![ - #( - anchor_lang::idl::types::IdlInstructionAccountItem::Single( - anchor_lang::idl::types::IdlInstructionAccount { - name: stringify!(#field_names).into(), - docs: vec![], - writable: false, // Could be enhanced to check is_mut - signer: false, // Could be enhanced to check is_signer - optional: false, - address: None, - pda: None, - relations: vec![], - } - ) - ),* - ] - } - } - - #[automatically_derived] - impl #impl_generics anchor_lang::Accounts<'info, #bumps_struct_name> for #struct_name #ty_generics #where_clause { - fn try_accounts( - __program_id: &anchor_lang::prelude::Pubkey, - __accounts: &mut &'info [anchor_lang::prelude::AccountInfo<'info>], - __ix_data: &[u8], - __bumps: &mut #bumps_struct_name, - __reallocs: &mut std::collections::BTreeSet, - ) -> anchor_lang::Result { - #(#field_assignments)* - - Ok(Self { - #(#field_names),* - }) - } - } - - #[automatically_derived] - impl #impl_generics anchor_lang::AccountsExit<'info> for #struct_name #ty_generics #where_clause { - fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> { - #(#exit_calls)* - Ok(()) - } - } - - #[automatically_derived] - impl #impl_generics anchor_lang::ToAccountInfos<'info> for #struct_name #ty_generics #where_clause { - fn to_account_infos(&self) -> Vec> { - let mut account_infos = Vec::with_capacity(#field_count); - #(#account_info_calls)* - account_infos - } - } - - #[automatically_derived] - impl #impl_generics anchor_lang::ToAccountMetas for #struct_name #ty_generics #where_clause { - fn to_account_metas(&self, _is_signer: Option) -> Vec { - let mut account_metas = Vec::with_capacity(#field_count); - #(#account_meta_calls)* - account_metas - } - } - }) - } } diff --git a/sdk-libs/macros/src/light_pdas/accounts/derive.rs b/sdk-libs/macros/src/light_pdas/accounts/derive.rs index f0953d97ad..61fc1994c6 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/derive.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/derive.rs @@ -30,14 +30,21 @@ use syn::DeriveInput; use super::builder::LightAccountsBuilder; /// Main orchestration - shows the high-level flow clearly. +/// +/// When structs contain `AccountLoader<'info, Mint>` fields, +/// we must generate Anchor trait implementations ourselves because Anchor's +/// `#[derive(Accounts)]` doesn't know about this type. +/// +/// Users should use ONLY `#[derive(LightAccounts)]` (not both Accounts and LightAccounts) +/// when they have these fields. pub(super) fn derive_light_accounts(input: &DeriveInput) -> Result { let builder = LightAccountsBuilder::parse(input)?; builder.validate()?; - // Check if struct has LightMint<'info> type fields. + // Check if struct has AccountLoader<'info, Mint> type fields. // When present, we must generate Anchor trait implementations ourselves - // because Anchor's #[derive(Accounts)] doesn't know about LightMint. - let anchor_impls = if builder.has_light_mint_type_fields() { + // because Anchor's #[derive(Accounts)] doesn't know about this type. + let anchor_impls = if builder.has_light_loader_fields() { builder.generate_anchor_accounts_impl()? } else { quote! {} diff --git a/sdk-libs/macros/src/light_pdas/accounts/parse.rs b/sdk-libs/macros/src/light_pdas/accounts/parse.rs index ae483ff2a1..73fac018f7 100644 --- a/sdk-libs/macros/src/light_pdas/accounts/parse.rs +++ b/sdk-libs/macros/src/light_pdas/accounts/parse.rs @@ -153,28 +153,6 @@ impl InfraFields { } } -/// Parsed representation of a struct with rentfree and light_mint fields. -pub(super) struct ParsedLightAccountsStruct { - pub struct_name: Ident, - pub generics: syn::Generics, - pub rentfree_fields: Vec, - pub light_mint_fields: Vec, - pub token_account_fields: Vec, - pub ata_fields: Vec, - pub instruction_args: Option>, - /// Infrastructure fields detected by naming convention. - pub infra_fields: InfraFields, - /// If CreateAccountsProof type is passed as a direct instruction arg, stores arg name. - /// Matched by TYPE, not by name - allows any argument name (e.g., `proof`, `create_proof`). - pub direct_proof_arg: Option, - /// All fields in the struct (for generating Accounts trait when LightMint fields present). - pub all_fields: Vec, - /// True if any field has `LightMint<'info>` type. - /// When true, LightAccounts must generate the Accounts trait implementation - /// because Anchor's derive doesn't know about LightMint. - pub has_light_mint_type_fields: bool, -} - /// A parsed field from the struct (for Accounts trait generation). #[derive(Clone)] pub(super) struct ParsedField { @@ -186,11 +164,28 @@ pub(super) struct ParsedField { pub is_signer: bool, } -/// Check if a type is `LightMint<'info>`. -fn is_light_mint_type(ty: &Type) -> bool { +/// Check if a type is `AccountLoader<'info, T>` from light_token_interface. +/// +/// Note: This checks if it's our custom AccountLoader, not Anchor's. The heuristic is: +/// - `AccountLoader<'info, Mint>` is ours (Anchor's requires ZeroCopy which Mint doesn't have) +fn is_light_loader_type(ty: &Type) -> bool { if let Type::Path(type_path) = ty { if let Some(segment) = type_path.path.segments.last() { - return segment.ident == "LightMint"; + // AccountLoader with Mint type parameter is ours + if segment.ident == "AccountLoader" { + // Check if the type parameter is Mint + if let syn::PathArguments::AngleBracketed(args) = &segment.arguments { + for arg in &args.args { + if let syn::GenericArgument::Type(Type::Path(inner_path)) = arg { + if let Some(inner_seg) = inner_path.path.segments.last() { + if inner_seg.ident == "Mint" { + return true; + } + } + } + } + } + } } } false @@ -224,6 +219,28 @@ fn has_mut_attribute(attrs: &[syn::Attribute]) -> bool { false } +/// Parsed representation of a struct with rentfree and light_mint fields. +pub(super) struct ParsedLightAccountsStruct { + pub struct_name: Ident, + pub generics: syn::Generics, + pub rentfree_fields: Vec, + pub light_mint_fields: Vec, + pub token_account_fields: Vec, + pub ata_fields: Vec, + pub instruction_args: Option>, + /// Infrastructure fields detected by naming convention. + pub infra_fields: InfraFields, + /// If CreateAccountsProof type is passed as a direct instruction arg, stores arg name. + /// Matched by TYPE, not by name - allows any argument name (e.g., `proof`, `create_proof`). + pub direct_proof_arg: Option, + /// All fields in the struct (for generating Accounts trait when LightAccountLoader fields present). + pub all_fields: Vec, + /// True if any field has `AccountLoader<'info, Mint>` type. + /// When true, LightAccounts must generate the Accounts trait implementation + /// because Anchor's derive doesn't know about this type. + pub has_light_loader_fields: bool, +} + /// A field marked with #[light_account(init)] pub(super) struct ParsedPdaField { pub ident: Ident, @@ -342,7 +359,7 @@ pub(super) fn parse_light_accounts_struct( let mut ata_fields = Vec::new(); let mut infra_fields = InfraFields::default(); let mut all_fields = Vec::new(); - let mut has_light_mint_type_fields = false; + let mut has_light_loader_fields = false; for field in fields { let field_ident = field @@ -357,9 +374,9 @@ pub(super) fn parse_light_accounts_struct( infra_fields.set(field_type, field_ident.clone())?; } - // Check if field type is LightMint<'info> - if is_light_mint_type(&field.ty) { - has_light_mint_type_fields = true; + // Check if field type is AccountLoader<'info, Mint> + if is_light_loader_type(&field.ty) { + has_light_loader_fields = true; } // Track all fields for Accounts trait generation @@ -408,6 +425,6 @@ pub(super) fn parse_light_accounts_struct( infra_fields, direct_proof_arg, all_fields, - has_light_mint_type_fields, + has_light_loader_fields, }) } diff --git a/sdk-libs/macros/src/light_pdas/program/instructions.rs b/sdk-libs/macros/src/light_pdas/program/instructions.rs index 1402889f6e..214592912f 100644 --- a/sdk-libs/macros/src/light_pdas/program/instructions.rs +++ b/sdk-libs/macros/src/light_pdas/program/instructions.rs @@ -594,8 +594,8 @@ pub fn light_program_impl(_args: TokenStream, mut module: ItemMod) -> Result fields - // (Anchor's #[derive(Accounts)] doesn't recognize LightMint, so LightAccounts generates all Anchor traits) + // - #[derive(LightAccounts)] alone is used when the struct contains AccountLoader<'info, Mint> fields + // (Anchor's #[derive(Accounts)] doesn't recognize AccountLoader, so LightAccounts generates all Anchor traits) let mut pda_specs: Vec = Vec::new(); let mut token_specs: Vec = Vec::new(); let mut rentfree_struct_names = std::collections::HashSet::new(); diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs index 43b662d2d0..656020582c 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/instruction_accounts.rs @@ -1,7 +1,7 @@ use anchor_lang::prelude::*; use light_compressible::CreateAccountsProof; use light_sdk_macros::LightAccounts; -use light_token_interface::state::mint::LightMint; +use light_token_interface::state::mint::{AccountLoader, Mint}; use crate::state::*; @@ -134,7 +134,7 @@ pub struct CreateTwoMintsParams { } /// Test instruction with 2 #[light_account(init)] fields to verify multi-mint support. -#[derive(Accounts, LightAccounts)] +#[derive(LightAccounts)] #[instruction(params: CreateTwoMintsParams)] pub struct CreateTwoMints<'info> { #[account(mut)] @@ -156,7 +156,7 @@ pub struct CreateTwoMints<'info> { )] pub mint_signer_b: UncheckedAccount<'info>, - /// CHECK: Initialized by mint_action - first mint + /// First mint with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer_a, @@ -165,9 +165,9 @@ pub struct CreateTwoMints<'info> { mint::seeds = &[MINT_SIGNER_A_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_a_bump )] - pub cmint_a: UncheckedAccount<'info>, + pub cmint_a: AccountLoader<'info, Mint>, - /// CHECK: Initialized by mint_action - second mint + /// Second mint with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer_b, @@ -176,7 +176,7 @@ pub struct CreateTwoMints<'info> { mint::seeds = &[MINT_SIGNER_B_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_b_bump )] - pub cmint_b: UncheckedAccount<'info>, + pub cmint_b: AccountLoader<'info, Mint>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>, @@ -212,7 +212,7 @@ pub struct CreateThreeMintsParams { } /// Test instruction with 3 #[light_account(init)] fields to verify multi-mint support. -#[derive(Accounts, LightAccounts)] +#[derive(LightAccounts)] #[instruction(params: CreateThreeMintsParams)] pub struct CreateThreeMints<'info> { #[account(mut)] @@ -241,7 +241,7 @@ pub struct CreateThreeMints<'info> { )] pub mint_signer_c: UncheckedAccount<'info>, - /// CHECK: Initialized by light_mint CPI + /// First mint with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer_a, @@ -250,9 +250,9 @@ pub struct CreateThreeMints<'info> { mint::seeds = &[MINT_SIGNER_A_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_a_bump )] - pub cmint_a: UncheckedAccount<'info>, + pub cmint_a: AccountLoader<'info, Mint>, - /// CHECK: Initialized by light_mint CPI + /// Second mint with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer_b, @@ -261,9 +261,9 @@ pub struct CreateThreeMints<'info> { mint::seeds = &[MINT_SIGNER_B_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_b_bump )] - pub cmint_b: UncheckedAccount<'info>, + pub cmint_b: AccountLoader<'info, Mint>, - /// CHECK: Initialized by light_mint CPI + /// Third mint with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer_c, @@ -272,7 +272,7 @@ pub struct CreateThreeMints<'info> { mint::seeds = &[MINT_SIGNER_C_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_c_bump )] - pub cmint_c: UncheckedAccount<'info>, + pub cmint_c: AccountLoader<'info, Mint>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>, @@ -311,7 +311,7 @@ pub struct CreateMintWithMetadataParams { /// Test instruction with #[light_account(init)] with metadata fields. /// Tests the metadata support in the RentFree macro. -#[derive(Accounts, LightAccounts)] +#[derive(LightAccounts)] #[instruction(params: CreateMintWithMetadataParams)] pub struct CreateMintWithMetadata<'info> { #[account(mut)] @@ -326,7 +326,7 @@ pub struct CreateMintWithMetadata<'info> { )] pub mint_signer: UncheckedAccount<'info>, - /// CHECK: Initialized by light_mint CPI with metadata + /// Mint with metadata and zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer, @@ -340,7 +340,7 @@ pub struct CreateMintWithMetadata<'info> { mint::update_authority = authority, mint::additional_metadata = params.additional_metadata.clone() )] - pub cmint: UncheckedAccount<'info>, + pub cmint: AccountLoader<'info, Mint>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>, @@ -362,27 +362,27 @@ pub struct CreateMintWithMetadata<'info> { } // ============================================================================= -// LightMint Wrapper Test +// AccountLoader Wrapper Test // ============================================================================= pub const LIGHT_MINT_TEST_SIGNER_SEED: &[u8] = b"light_mint_test_signer"; #[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)] -pub struct CreateMintWithLightMintParams { +pub struct CreateMintWithAccountLoaderParams { pub create_accounts_proof: CreateAccountsProof, pub mint_signer_bump: u8, } -/// Test instruction to verify LightMint wrapper works correctly. -/// Uses `LightMint<'info>` directly in the Accounts struct to demonstrate +/// Test instruction to verify AccountLoader wrapper works correctly. +/// Uses `AccountLoader<'info, Mint>` directly in the Accounts struct to demonstrate /// type-safe access to mint data after CPI initialization. /// /// Note: Uses `#[derive(LightAccounts)]` alone (not with `#[derive(Accounts)]`) -/// because Anchor's derive doesn't know about `LightMint` type. +/// because Anchor's derive doesn't know about `AccountLoader` type. /// The `LightAccounts` macro generates the necessary Anchor trait implementations. #[derive(LightAccounts)] -#[instruction(params: CreateMintWithLightMintParams)] -pub struct CreateMintWithLightMint<'info> { +#[instruction(params: CreateMintWithAccountLoaderParams)] +pub struct CreateMintWithAccountLoader<'info> { #[account(mut)] pub fee_payer: Signer<'info>, @@ -396,7 +396,7 @@ pub struct CreateMintWithLightMint<'info> { pub mint_signer: UncheckedAccount<'info>, /// Mint account with zero-copy access after CPI initialization. - /// LightMint provides type-safe access to mint data without manual wrapping. + /// AccountLoader provides type-safe access to mint data without manual wrapping. #[account(mut)] #[light_account(init, mint::signer = mint_signer, @@ -405,7 +405,7 @@ pub struct CreateMintWithLightMint<'info> { mint::seeds = &[LIGHT_MINT_TEST_SIGNER_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_bump )] - pub cmint: LightMint<'info>, + pub cmint: AccountLoader<'info, Mint>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>, diff --git a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs index 8e08e74ab7..90c2de697a 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/src/lib.rs @@ -265,7 +265,7 @@ pub mod csdk_anchor_full_derived_test { D9U64Params, }, instruction_accounts::{ - CreateMintWithLightMint, CreateMintWithLightMintParams, CreateMintWithMetadata, + CreateMintWithAccountLoader, CreateMintWithAccountLoaderParams, CreateMintWithMetadata, CreateMintWithMetadataParams, CreatePdasAndMintAuto, CreateThreeMints, CreateThreeMintsParams, CreateTwoMints, CreateTwoMintsParams, }, @@ -413,13 +413,13 @@ pub mod csdk_anchor_full_derived_test { } /// Test instruction demonstrating type-safe mint access after CPI initialization. - /// Uses LightMint directly in the Accounts struct for zero-copy access. - pub fn create_mint_with_light_mint<'info>( - ctx: Context<'_, '_, '_, 'info, CreateMintWithLightMint<'info>>, - _params: CreateMintWithLightMintParams, + /// Uses AccountLoader<'info, Mint> directly in the Accounts struct for zero-copy access. + pub fn create_mint_with_account_loader<'info>( + ctx: Context<'_, '_, '_, 'info, CreateMintWithAccountLoader<'info>>, + _params: CreateMintWithAccountLoaderParams, ) -> Result<()> { - // Direct zero-copy access via LightMint field - // No wrapping needed - cmint is already LightMint<'info> + // Direct zero-copy access via AccountLoader field + // No wrapping needed - cmint is already AccountLoader<'info, Mint> let mint_data = ctx.accounts.cmint.load().map_err(|e| { solana_msg::msg!("Failed to load mint: {:?}", e); anchor_lang::error::ErrorCode::AccountDidNotDeserialize diff --git a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs index da8c00ddfc..eaf1ad8a7b 100644 --- a/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs +++ b/sdk-tests/csdk-anchor-full-derived-test/tests/mint/light_mint_test.rs @@ -1,11 +1,11 @@ -//! Integration test for LightMint wrapper functionality. +//! Integration test for AccountLoader wrapper functionality. //! //! Tests that mint data can be accessed after CPI initialization using //! type-safe deserialization patterns. use anchor_lang::{InstructionData, ToAccountMetas}; use csdk_anchor_full_derived_test::instruction_accounts::{ - CreateMintWithLightMintParams, LIGHT_MINT_TEST_SIGNER_SEED, + CreateMintWithAccountLoaderParams, LIGHT_MINT_TEST_SIGNER_SEED, }; use light_client::interface::{get_create_accounts_proof, CreateAccountsProofInput}; use light_program_test::{ @@ -22,7 +22,7 @@ use solana_signer::Signer; /// Test creating a mint and verifying the handler can access mint data after CPI. /// This demonstrates the pattern for accessing type-safe mint data after initialization. #[tokio::test] -async fn test_create_mint_with_light_mint_wrapper() { +async fn test_create_mint_with_account_loader_wrapper() { use light_token::instruction::find_mint_address as find_cmint_address; let program_id = csdk_anchor_full_derived_test::ID; @@ -72,7 +72,7 @@ async fn test_create_mint_with_light_mint_wrapper() { .unwrap(); // Build the instruction - let accounts = csdk_anchor_full_derived_test::accounts::CreateMintWithLightMint { + let accounts = csdk_anchor_full_derived_test::accounts::CreateMintWithAccountLoader { fee_payer: payer.pubkey(), authority: authority.pubkey(), mint_signer: mint_signer_pda, @@ -85,12 +85,13 @@ async fn test_create_mint_with_light_mint_wrapper() { system_program: solana_sdk::system_program::ID, }; - let instruction_data = csdk_anchor_full_derived_test::instruction::CreateMintWithLightMint { - _params: CreateMintWithLightMintParams { - create_accounts_proof: proof_result.create_accounts_proof, - mint_signer_bump, - }, - }; + let instruction_data = + csdk_anchor_full_derived_test::instruction::CreateMintWithAccountLoader { + _params: CreateMintWithAccountLoaderParams { + create_accounts_proof: proof_result.create_accounts_proof, + mint_signer_bump, + }, + }; let instruction = Instruction { program_id, @@ -107,7 +108,7 @@ async fn test_create_mint_with_light_mint_wrapper() { // 2. In the handler, access and verify the mint data rpc.create_and_send_transaction(&[instruction], &payer.pubkey(), &[&payer, &authority]) .await - .expect("CreateMintWithLightMint should succeed"); + .expect("CreateMintWithAccountLoader should succeed"); // Verify mint exists on-chain with expected data let cmint_account = rpc diff --git a/sdk-tests/single-mint-test/Cargo.toml b/sdk-tests/single-mint-test/Cargo.toml index 618df66e5b..6684882df2 100644 --- a/sdk-tests/single-mint-test/Cargo.toml +++ b/sdk-tests/single-mint-test/Cargo.toml @@ -32,6 +32,7 @@ light-anchor-spl = { workspace = true, features = ["metadata", "idl-build"] } light-token = { workspace = true, features = ["anchor"] } light-token-types = { workspace = true, features = ["anchor"] } light-compressible = { workspace = true, features = ["anchor"] } +light-token-interface = { workspace = true, features = ["anchor"] } solana-program = { workspace = true } solana-pubkey = { workspace = true } solana-msg = { workspace = true } diff --git a/sdk-tests/single-mint-test/src/lib.rs b/sdk-tests/single-mint-test/src/lib.rs index 334688be63..3e2c0635bc 100644 --- a/sdk-tests/single-mint-test/src/lib.rs +++ b/sdk-tests/single-mint-test/src/lib.rs @@ -10,6 +10,7 @@ use light_compressible::CreateAccountsProof; use light_sdk::derive_light_cpi_signer; use light_sdk_macros::{light_program, LightAccounts}; use light_sdk_types::CpiSigner; +use light_token_interface::state::mint::{AccountLoader, Mint}; declare_id!("Mint111111111111111111111111111111111111111"); @@ -25,7 +26,9 @@ pub struct CreateMintParams { } /// Minimal accounts struct for testing single mint creation. -#[derive(Accounts, LightAccounts)] +/// Uses only #[derive(LightAccounts)] because it contains AccountLoader<'info, Mint> field. +/// The LightAccounts macro generates all necessary Anchor trait implementations. +#[derive(LightAccounts)] #[instruction(params: CreateMintParams)] pub struct CreateMint<'info> { #[account(mut)] @@ -40,7 +43,7 @@ pub struct CreateMint<'info> { )] pub mint_signer: UncheckedAccount<'info>, - /// CHECK: Initialized by light_mint CPI + /// Mint account with zero-copy access after CPI initialization. #[account(mut)] #[light_account(init, mint::signer = mint_signer, @@ -49,7 +52,7 @@ pub struct CreateMint<'info> { mint::seeds = &[MINT_SIGNER_SEED, self.authority.to_account_info().key.as_ref()], mint::bump = params.mint_signer_bump )] - pub mint: UncheckedAccount<'info>, + pub mint: AccountLoader<'info, Mint>, /// CHECK: Compression config pub compression_config: AccountInfo<'info>,