Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
470 changes: 470 additions & 0 deletions program-libs/token-interface/src/state/mint/anchor_wrapper.rs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
5 changes: 5 additions & 0 deletions program-libs/token-interface/src/state/mint/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{AccountLoader, LightZeroCopy};
pub use compressed_mint::*;
pub use top_up::*;
pub use zero_copy::*;
5 changes: 2 additions & 3 deletions program-libs/token-interface/src/state/mint/zero_copy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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);
}
Expand Down
5 changes: 4 additions & 1 deletion sdk-libs/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 `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 {
let input = parse_macro_input!(input as DeriveInput);
into_token_stream(light_pdas::accounts::derive_light_accounts(input))
Expand Down
244 changes: 244 additions & 0 deletions sdk-libs/macros/src/light_pdas/accounts/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,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<TokenStream, syn::Error> {
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<TokenStream> = 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<TokenStream> = 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<TokenStream> = 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<TokenStream> = 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<TokenStream> = 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<TokenStream> = 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<bool>) -> Vec<anchor_lang::prelude::AccountMeta> {
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<u8> {
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<String, anchor_lang::idl::types::IdlAccount>,
_types: &mut std::collections::BTreeMap<String, anchor_lang::idl::types::IdlTypeDef>,
) -> Vec<anchor_lang::idl::types::IdlInstructionAccountItem> {
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![],
}
)
),*
]
}
}
Comment on lines +404 to +427
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

IDL metadata loses field mutability and signer information.

The generated IdlInstructionAccount hardcodes writable: false and signer: false for all fields, but you have this metadata available in ParsedField.is_mut and ParsedField.is_signer. Clients relying on IDL to construct transactions may incorrectly mark accounts as read-only.

🔧 Proposed fix to use actual field metadata
-                        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![],
-                            }
-                        )
+                        anchor_lang::idl::types::IdlInstructionAccountItem::Single(
+                            anchor_lang::idl::types::IdlInstructionAccount {
+                                name: stringify!(`#field_names`).into(),
+                                docs: vec![],
+                                writable: `#field_is_mut`,
+                                signer: `#field_is_signer`,
+                                optional: false,
+                                address: None,
+                                pda: None,
+                                relations: vec![],
+                            }
+                        )

You'll need to collect the is_mut and is_signer values alongside field_names:

let field_metadata: Vec<_> = fields.iter().map(|f| {
    let name = &f.ident;
    let is_mut = f.is_mut;
    let is_signer = f.is_signer;
    (name, is_mut, is_signer)
}).collect();
🤖 Prompt for AI Agents
In `@sdk-libs/macros/src/light_pdas/accounts/builder.rs` around lines 404 - 427,
The IDL generator (__anchor_private_gen_idl_accounts) currently hardcodes
writable: false and signer: false for every field; update the code that builds
the account list to collect per-field metadata (e.g., use the ParsedField
values: ParsedField.is_mut and ParsedField.is_signer alongside field_names) and
emit each IdlInstructionAccount with writable set to is_mut and signer set to
is_signer; locate the vector construction that iterates over `#field_names` and
change it to iterate over a collected (name, is_mut, is_signer) tuple so each
IdlInstructionAccount reflects the real mutability and signer flags.


#[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::prelude::Pubkey>,
) -> anchor_lang::Result<Self> {
#(#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<anchor_lang::prelude::AccountInfo<'info>> {
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<bool>) -> Vec<anchor_lang::prelude::AccountMeta> {
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<TokenStream, syn::Error> {
let struct_name = &self.parsed.struct_name;
Expand Down
23 changes: 22 additions & 1 deletion sdk-libs/macros/src/light_pdas/accounts/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,33 @@ 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<TokenStream, syn::Error> {
let builder = LightAccountsBuilder::parse(input)?;
builder.validate()?;

// 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 this type.
let anchor_impls = if builder.has_light_loader_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)
Expand All @@ -51,6 +71,7 @@ pub(super) fn derive_light_accounts(input: &DeriveInput) -> Result<TokenStream,
let finalize_impl = builder.generate_finalize_impl(finalize_body)?;

Ok(quote! {
#anchor_impls
#pre_init_impl
#finalize_impl
})
Expand Down
Loading
Loading