From f8113465664e0f8f1e128ae717c200c74f4786e5 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 11:32:15 -0400 Subject: [PATCH 1/9] Support fixed byte seed parameters Share typed seed parsing across account and standalone seed specs, including bounded fixed byte arrays for PDA seed inputs. --- derive/src/account/seeds.rs | 85 +-------------- derive/src/lib.rs | 1 + derive/src/seed_param.rs | 123 ++++++++++++++++++++++ derive/src/seeds.rs | 87 ++------------- derive/tests/compile_pass/derive_seeds.rs | 15 +++ 5 files changed, 153 insertions(+), 158 deletions(-) create mode 100644 derive/src/seed_param.rs diff --git a/derive/src/account/seeds.rs b/derive/src/account/seeds.rs index 09c1d0df..4f4d57ff 100644 --- a/derive/src/account/seeds.rs +++ b/derive/src/account/seeds.rs @@ -1,79 +1,14 @@ //! Parse `#[seeds(b"prefix", name: Type, ...)]` on account types. use { + crate::seed_param::{parse_seed_type, SeedType}, quote::{format_ident, quote}, syn::{ parse::{Parse, ParseStream}, - Expr, ExprLit, Ident, Lit, LitByteStr, Token, + Expr, ExprLit, Ident, Lit, LitByteStr, Token, Type, }, }; -/// Supported seed parameter types. -enum SeedType { - Address, - U8, - U16, - U32, - U64, -} - -impl SeedType { - /// The field storage type in SeedSet. - /// Address: borrowed reference (zero-copy). - /// Scalars: owned byte array (needs backing storage for to_le_bytes). - fn field_type(&self) -> proc_macro2::TokenStream { - match self { - SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, - SeedType::U8 => quote! { [u8; 1] }, - SeedType::U16 => quote! { [u8; 2] }, - SeedType::U32 => quote! { [u8; 4] }, - SeedType::U64 => quote! { [u8; 8] }, - } - } - - /// The constructor parameter type. Address uses the generated seed lifetime - /// to tie the borrow to the SeedSet. - fn param_type(&self) -> proc_macro2::TokenStream { - match self { - SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, - SeedType::U8 => quote! { u8 }, - SeedType::U16 => quote! { u16 }, - SeedType::U32 => quote! { u32 }, - SeedType::U64 => quote! { u64 }, - } - } - - /// Expression to store the parameter in the SeedSet field. - /// Address: borrow directly (zero-copy). - /// Scalars: convert to le bytes (needs owned storage). - fn to_stored_expr(&self, param: &Ident) -> proc_macro2::TokenStream { - match self { - SeedType::Address => quote! { #param }, - SeedType::U8 => quote! { [#param] }, - _ => quote! { #param.to_le_bytes() }, - } - } - - /// Expression for as_slices() — how to get a `&[u8]` from the field. - /// Address: `.as_ref()` on the `&Address`. - /// Scalars: `&self._field` on the owned `[u8; N]`. - fn slice_expr(&self, field_name: &Ident, prefix: &str) -> proc_macro2::TokenStream { - let prefix_ident: Option = if prefix.is_empty() { - None - } else { - Some(Ident::new(prefix, field_name.span())) - }; - let access = match prefix_ident { - None => quote! { self.#field_name }, - Some(p) => quote! { self.#p.#field_name }, - }; - match self { - SeedType::Address => quote! { #access.as_ref() }, - _ => quote! { &#access }, - } - } -} - /// A single typed seed parameter (e.g. `maker: Address`). pub struct SeedParam { pub name: Ident, @@ -129,20 +64,8 @@ impl Parse for SeedsAttr { } let name: Ident = input.parse()?; let _: Token![:] = input.parse()?; - let ty_ident: Ident = input.parse()?; - let ty = match ty_ident.to_string().as_str() { - "Address" => SeedType::Address, - "u8" => SeedType::U8, - "u16" => SeedType::U16, - "u32" => SeedType::U32, - "u64" => SeedType::U64, - _ => { - return Err(syn::Error::new( - ty_ident.span(), - "unsupported seed type; expected Address, u8, u16, u32, or u64", - )) - } - }; + let ty: Type = input.parse()?; + let ty = parse_seed_type(ty)?; params.push(SeedParam { name, ty }); } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 0b7cbf39..ada32ed0 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -11,6 +11,7 @@ mod event; mod helpers; mod instruction; mod program; +mod seed_param; mod seeds; mod serialize; diff --git a/derive/src/seed_param.rs b/derive/src/seed_param.rs new file mode 100644 index 00000000..476d4fc0 --- /dev/null +++ b/derive/src/seed_param.rs @@ -0,0 +1,123 @@ +use { + quote::quote, + syn::{Error, Expr, Ident, Lit, Result, Type}, +}; + +/// Supported typed PDA seed parameter types. +pub(crate) enum SeedType { + Address, + U8, + U16, + U32, + U64, + Bytes(usize), +} + +impl SeedType { + /// The field storage type in generated SeedSet structs. + pub(crate) fn field_type(&self) -> proc_macro2::TokenStream { + match self { + SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, + SeedType::U8 => quote! { [u8; 1] }, + SeedType::U16 => quote! { [u8; 2] }, + SeedType::U32 => quote! { [u8; 4] }, + SeedType::U64 => quote! { [u8; 8] }, + SeedType::Bytes(len) => quote! { [u8; #len] }, + } + } + + /// The public constructor parameter type for a typed seed. + pub(crate) fn param_type(&self) -> proc_macro2::TokenStream { + match self { + SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, + SeedType::U8 => quote! { u8 }, + SeedType::U16 => quote! { u16 }, + SeedType::U32 => quote! { u32 }, + SeedType::U64 => quote! { u64 }, + SeedType::Bytes(len) => quote! { [u8; #len] }, + } + } + + /// Expression to store the constructor parameter in the SeedSet field. + pub(crate) fn to_stored_expr(&self, param: &Ident) -> proc_macro2::TokenStream { + match self { + SeedType::Address | SeedType::Bytes(_) => quote! { #param }, + SeedType::U8 => quote! { [#param] }, + SeedType::U16 | SeedType::U32 | SeedType::U64 => quote! { #param.to_le_bytes() }, + } + } + + /// Expression for turning a generated SeedSet field into a seed slice. + pub(crate) fn slice_expr(&self, field_name: &Ident, prefix: &str) -> proc_macro2::TokenStream { + let prefix_ident = (!prefix.is_empty()).then(|| Ident::new(prefix, field_name.span())); + let access = match prefix_ident { + None => quote! { self.#field_name }, + Some(p) => quote! { self.#p.#field_name }, + }; + match self { + SeedType::Address => quote! { #access.as_ref() }, + SeedType::U8 | SeedType::U16 | SeedType::U32 | SeedType::U64 | SeedType::Bytes(_) => { + quote! { &#access } + } + } + } +} + +pub(crate) fn parse_seed_type(ty: Type) -> Result { + if let Type::Path(type_path) = &ty { + if let Some(ident) = type_path.path.get_ident() { + return match ident.to_string().as_str() { + "Address" => Ok(SeedType::Address), + "u8" => Ok(SeedType::U8), + "u16" => Ok(SeedType::U16), + "u32" => Ok(SeedType::U32), + "u64" => Ok(SeedType::U64), + _ => Err(Error::new( + ident.span(), + "unsupported seed type; expected Address, u8, u16, u32, u64, or [u8; N] where \ + N <= 32", + )), + }; + } + } + + if let Type::Array(array) = &ty { + let Type::Path(elem_path) = array.elem.as_ref() else { + return Err(Error::new_spanned( + &array.elem, + "unsupported seed array element; expected u8", + )); + }; + if !elem_path.path.is_ident("u8") { + return Err(Error::new_spanned( + &array.elem, + "unsupported seed array element; expected u8", + )); + } + let Expr::Lit(expr_lit) = &array.len else { + return Err(Error::new_spanned( + &array.len, + "seed byte array length must be an integer literal", + )); + }; + let Lit::Int(lit_int) = &expr_lit.lit else { + return Err(Error::new_spanned( + &array.len, + "seed byte array length must be an integer literal", + )); + }; + let len = lit_int.base10_parse::()?; + if len > 32 { + return Err(Error::new_spanned( + &array.len, + "seed byte array length exceeds MAX_SEED_LEN of 32", + )); + } + return Ok(SeedType::Bytes(len)); + } + + Err(Error::new_spanned( + ty, + "unsupported seed type; expected Address, u8, u16, u32, u64, or [u8; N] where N <= 32", + )) +} diff --git a/derive/src/seeds.rs b/derive/src/seeds.rs index ff5a6f27..e990a0b2 100644 --- a/derive/src/seeds.rs +++ b/derive/src/seeds.rs @@ -11,13 +11,14 @@ //! with `AddressVerify` impls. use { + crate::seed_param::{parse_seed_type, SeedType}, proc_macro2::{Span, TokenStream}, quote::{format_ident, quote}, syn::{ parse::{Parse, ParseStream}, parse2, spanned::Spanned, - Data, DeriveInput, Error, Ident, LitByteStr, Result, Token, + Data, DeriveInput, Error, Ident, LitByteStr, Result, Token, Type, }, }; @@ -27,68 +28,6 @@ struct SeedParam { ty: SeedType, } -/// Supported seed parameter types. -enum SeedType { - Address, - U8, - U16, - U32, - U64, -} - -impl SeedType { - /// The field storage type in SeedSet. - /// Address: borrowed reference (zero-copy). - /// Scalars: owned byte array (needs backing storage for to_le_bytes). - fn field_type(&self) -> TokenStream { - match self { - SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, - SeedType::U8 => quote! { [u8; 1] }, - SeedType::U16 => quote! { [u8; 2] }, - SeedType::U32 => quote! { [u8; 4] }, - SeedType::U64 => quote! { [u8; 8] }, - } - } - - /// The constructor parameter type. Address uses the generated seed lifetime - /// to tie the borrow to the SeedSet. - fn param_type(&self) -> TokenStream { - match self { - SeedType::Address => quote! { &'__quasar_seed quasar_lang::prelude::Address }, - SeedType::U8 => quote! { u8 }, - SeedType::U16 => quote! { u16 }, - SeedType::U32 => quote! { u32 }, - SeedType::U64 => quote! { u64 }, - } - } - - /// Expression to store the parameter in the SeedSet field. - fn to_stored_expr(&self, param: &Ident) -> TokenStream { - match self { - SeedType::Address => quote! { #param }, - SeedType::U8 => quote! { [#param] }, - _ => quote! { #param.to_le_bytes() }, - } - } - - /// Expression for as_slices() — how to get a `&[u8]` from the field. - fn slice_expr(&self, field_name: &Ident, prefix: &str) -> TokenStream { - let prefix_ident: Option = if prefix.is_empty() { - None - } else { - Some(Ident::new(prefix, field_name.span())) - }; - let access = match prefix_ident { - None => quote! { self.#field_name }, - Some(p) => quote! { self.#p.#field_name }, - }; - match self { - SeedType::Address => quote! { #access.as_ref() }, - _ => quote! { &#access }, - } - } -} - /// Parsed `#[seeds(...)]` attribute content. struct SeedsAttr { prefix: LitByteStr, @@ -98,6 +37,12 @@ struct SeedsAttr { impl Parse for SeedsAttr { fn parse(input: ParseStream) -> Result { let prefix: LitByteStr = input.parse()?; + if prefix.value().len() > 32 { + return Err(Error::new_spanned( + &prefix, + "seed prefix exceeds MAX_SEED_LEN of 32", + )); + } let mut params = Vec::new(); while input.peek(Token![,]) { @@ -108,20 +53,8 @@ impl Parse for SeedsAttr { } let name: Ident = input.parse()?; let _: Token![:] = input.parse()?; - let ty_ident: Ident = input.parse()?; - let ty = match ty_ident.to_string().as_str() { - "Address" => SeedType::Address, - "u8" => SeedType::U8, - "u16" => SeedType::U16, - "u32" => SeedType::U32, - "u64" => SeedType::U64, - _ => { - return Err(Error::new( - ty_ident.span(), - "unsupported seed type; expected Address, u8, u16, u32, or u64", - )) - } - }; + let ty: Type = input.parse()?; + let ty = parse_seed_type(ty)?; params.push(SeedParam { name, ty }); } diff --git a/derive/tests/compile_pass/derive_seeds.rs b/derive/tests/compile_pass/derive_seeds.rs index a5b15946..8d7b936f 100644 --- a/derive/tests/compile_pass/derive_seeds.rs +++ b/derive/tests/compile_pass/derive_seeds.rs @@ -23,6 +23,12 @@ pub struct VaultPda; #[seeds(b"indexed", authority: Address, index: u64)] pub struct IndexedPda; +// -- With arbitrary fixed-size byte seeds ------------------------------------ + +#[derive(Seeds)] +#[seeds(b"registry", hash: [u8; 32], namespace: [u8; 16])] +pub struct RegistryPda; + fn main() { // Verify TestPda::seeds() exists and returns the SeedSet. let set = TestPda::seeds(); @@ -61,4 +67,13 @@ fn main() { _assert_verify::(); _assert_verify::(); _assert_verify::(); + + let hash = [1u8; 32]; + let namespace = [2u8; 16]; + let set = RegistryPda::seeds(hash, namespace); + let slices = set.as_slices(); + assert_eq!(slices.len(), 3); + assert_eq!(slices[0], b"registry"); + assert_eq!(slices[1], &[1u8; 32]); + assert_eq!(slices[2], &[2u8; 16]); } From 6513262f54a94d8b721d29548cc05f5233d66ff6 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 11:32:26 -0400 Subject: [PATCH 2/9] Generate account-owned PDA signer helpers Move PDA signer construction onto the account group that owns the PDA, while keeping contexts as validated accounts plus bumps. --- derive/src/account/seeds.rs | 12 ++ derive/src/accounts/emit/output.rs | 42 ++++-- derive/src/accounts/emit/parse.rs | 34 ++++- derive/src/accounts/mod.rs | 136 ++++++++++++++++-- derive/src/accounts/plan.rs | 46 ++++-- derive/src/helpers.rs | 6 + derive/src/seeds.rs | 12 ++ derive/tests/compile_pass/account_signers.rs | 40 ++++++ examples/escrow/src/instructions/refund.rs | 15 +- examples/escrow/src/instructions/take.rs | 15 +- .../src/instructions/execute_transfer.rs | 9 +- lang/src/context.rs | 4 +- lang/src/cpi/dyn_cpi.rs | 16 ++- lang/src/cpi/mod.rs | 24 +++- lang/src/prelude.rs | 3 +- lang/src/traits.rs | 24 ++++ metadata/src/init.rs | 10 +- 17 files changed, 360 insertions(+), 88 deletions(-) create mode 100644 derive/tests/compile_pass/account_signers.rs diff --git a/derive/src/account/seeds.rs b/derive/src/account/seeds.rs index 4f4d57ff..20317505 100644 --- a/derive/src/account/seeds.rs +++ b/derive/src/account/seeds.rs @@ -228,6 +228,18 @@ pub fn generate_seeds_impl( } } + impl<'__quasar_seed> quasar_lang::cpi::CpiSignerSeeds for #seed_set_bump<'__quasar_seed> { + #[inline(always)] + fn with_signers(&self, f: F) -> R + where + F: FnOnce(&[quasar_lang::cpi::Signer<'_, '_>]) -> R, + { + let seeds = [#(#signer_seed_exprs_bump),*]; + let signer = quasar_lang::cpi::Signer::from(&seeds); + f(core::slice::from_ref(&signer)) + } + } + // AddressVerify: auto-find bump (full derivation, safe for init). impl<'__quasar_seed> quasar_lang::address::AddressVerify for #seed_set<'__quasar_seed> { #[inline(always)] diff --git a/derive/src/accounts/emit/output.rs b/derive/src/accounts/emit/output.rs index 69ad406c..4786282d 100644 --- a/derive/src/accounts/emit/output.rs +++ b/derive/src/accounts/emit/output.rs @@ -18,9 +18,9 @@ pub(crate) struct AccountsOutput<'a> { pub parse_body: proc_macro2::TokenStream, pub direct_parse_body: proc_macro2::TokenStream, pub bumps_struct: proc_macro2::TokenStream, + pub account_seeds_impl: proc_macro2::TokenStream, pub epilogue_method: proc_macro2::TokenStream, pub has_epilogue_expr: proc_macro2::TokenStream, - pub seeds_methods: proc_macro2::TokenStream, pub client_macro: proc_macro2::TokenStream, pub ix_arg_extraction: proc_macro2::TokenStream, } @@ -41,9 +41,9 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T parse_body, direct_parse_body, bumps_struct, + account_seeds_impl, epilogue_method, has_epilogue_expr, - seeds_methods, client_macro, ix_arg_extraction, } = output; @@ -52,16 +52,6 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T quasar_lang::traits::check_account_count(accounts.len(), Self::COUNT)?; }; - let seeds_impl = if seeds_methods.is_empty() { - quote! {} - } else { - quote! { - impl #impl_generics #name #ty_generics #where_clause { - #seeds_methods - } - } - }; - let has_epilogue_const = quote! { const HAS_EPILOGUE: bool = #has_epilogue_expr; }; @@ -136,11 +126,10 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T quote! { #bumps_struct + #account_seeds_impl #parse_accounts_impl - #seeds_impl - impl #impl_generics AccountCount for #name #ty_generics #where_clause { const COUNT: usize = #count_expr; const NEEDS_EVENT_CPI: bool = #needs_event_cpi_expr; @@ -173,6 +162,31 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T } } + unsafe impl #impl_generics quasar_lang::traits::ParseAccountsRaw for #name #ty_generics #where_clause { + #[inline(always)] + unsafe fn parse_accounts_raw( + input: *mut u8, + base: *mut quasar_lang::__internal::AccountView, + offset: usize, + __program_id: &quasar_lang::prelude::Address, + ) -> Result<*mut u8, ProgramError> { + let mut __inner_buf = core::mem::MaybeUninit::< + [quasar_lang::__internal::AccountView; #count_expr] + >::uninit(); + let input = Self::parse_accounts(input, &mut __inner_buf, __program_id)?; + let __inner = core::mem::ManuallyDrop::new(__inner_buf.assume_init()); + let mut __j = 0usize; + while __j < #count_expr { + core::ptr::write( + base.add(offset + __j), + core::ptr::read(__inner.as_ptr().add(__j)), + ); + __j += 1; + } + Ok(input) + } + } + #client_macro } } diff --git a/derive/src/accounts/emit/parse.rs b/derive/src/accounts/emit/parse.rs index 4c9167dc..a384e610 100644 --- a/derive/src/accounts/emit/parse.rs +++ b/derive/src/accounts/emit/parse.rs @@ -700,11 +700,16 @@ fn emit_bump_init( ) -> proc_macro2::TokenStream { let inits: Vec = semantics .iter() - .filter(|sem| sem.address.is_some()) + .filter(|sem| sem.address.is_some() || matches!(sem.core.kind, FieldKind::Composite)) .map(|sem| { let name = &sem.core.ident; - let var = format_ident!("__bumps_{}", name); - quote! { #name: #var } + if matches!(sem.core.kind, FieldKind::Composite) { + let var = format_ident!("__composite_bumps_{}", name); + quote! { #name: #var } + } else { + let var = format_ident!("__bumps_{}", name); + quote! { #name: #var } + } }) .collect(); @@ -722,10 +727,15 @@ pub(crate) fn emit_bump_struct_def( let bumps_name = &cx.bumps_name; let fields: Vec = semantics .iter() - .filter(|sem| sem.address.is_some()) + .filter(|sem| sem.address.is_some() || matches!(sem.core.kind, FieldKind::Composite)) .map(|sem| { let name = &sem.core.ident; - quote! { pub #name: u8 } + if matches!(sem.core.kind, FieldKind::Composite) { + let ty = composite_assoc_ty(&sem.core.effective_ty); + quote! { pub #name: <#ty as quasar_lang::traits::AccountBumps>::Bumps } + } else { + quote! { pub #name: u8 } + } }) .collect(); @@ -736,6 +746,20 @@ pub(crate) fn emit_bump_struct_def( } } +fn composite_assoc_ty(ty: &syn::Type) -> proc_macro2::TokenStream { + if let syn::Type::Path(type_path) = ty { + if type_path + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "AccountsArray") + { + return quote! { #ty }; + } + } + strip_generics(ty) +} + /// Returns true for account types with owner + discriminator validation. fn is_validated_account_type(ty: &syn::Type) -> bool { use crate::helpers::extract_generic_inner_type; diff --git a/derive/src/accounts/mod.rs b/derive/src/accounts/mod.rs index 770f9859..15d0375c 100644 --- a/derive/src/accounts/mod.rs +++ b/derive/src/accounts/mod.rs @@ -134,24 +134,32 @@ pub(crate) fn derive_accounts(input: TokenStream) -> TokenStream { direct_parse_body, } = accounts_plan; + // Instruction arg extraction + let ix_arg_extraction = if let Some(ref ix_args) = instruction_args { + generate_instruction_arg_extraction(ix_args) + } else { + quote! {} + }; + let bumps_struct = emit::emit_bump_struct_def(&semantics, &emit_cx); + let account_seeds_impl = emit_account_seeds_impl( + name, + &bumps_name, + &semantics, + &impl_generics_ts, + &ty_generics_ts, + &where_clause_ts, + &ix_arg_extraction, + instruction_args.is_some(), + ); let epilogue_method = match emit::emit_epilogue(&semantics, &typed_plan) { Ok(ts) => ts, Err(e) => return e.to_compile_error().into(), }; let has_epilogue_expr = emit::emit_has_epilogue(&typed_plan, &semantics); - let seeds_methods = quote::quote! {}; - let client_macro = crate::client_macro::generate_accounts_macro(name, &semantics); - // Instruction arg extraction - let ix_arg_extraction = if let Some(ref ix_args) = instruction_args { - generate_instruction_arg_extraction(ix_args) - } else { - quote! {} - }; - // IDL accounts meta fragment (feature-gated behind `idl-build`) let idl_accounts_meta = emit_idl_accounts_meta(name, &semantics, &instruction_args); @@ -170,9 +178,9 @@ pub(crate) fn derive_accounts(input: TokenStream) -> TokenStream { parse_body, direct_parse_body, bumps_struct, + account_seeds_impl, epilogue_method, has_epilogue_expr, - seeds_methods, client_macro, ix_arg_extraction, }); @@ -420,7 +428,7 @@ fn emit_needs_event_cpi_expr(semantics: &[resolve::FieldSemantics]) -> proc_macr .iter() .map(|sem| match sem.core.kind { resolve::FieldKind::Composite => { - let inner_ty = strip_generics(&sem.core.effective_ty); + let inner_ty = composite_event_ty(&sem.core.effective_ty); quote! { <#inner_ty as AccountCount>::NEEDS_EVENT_CPI } } resolve::FieldKind::Single if is_event_cpi_field(sem) => { @@ -433,6 +441,112 @@ fn emit_needs_event_cpi_expr(semantics: &[resolve::FieldSemantics]) -> proc_macr quote! { false #(|| #terms)* } } +fn emit_account_seeds_impl( + name: &syn::Ident, + bumps_name: &syn::Ident, + semantics: &[resolve::FieldSemantics], + impl_generics: &proc_macro2::TokenStream, + ty_generics: &proc_macro2::TokenStream, + where_clause: &proc_macro2::TokenStream, + ix_arg_extraction: &proc_macro2::TokenStream, + has_instruction_args: bool, +) -> proc_macro2::TokenStream { + let field_refs: Vec = semantics + .iter() + .map(|sem| { + let field_name = &sem.core.ident; + quote! { let #field_name = &self.#field_name; } + }) + .collect(); + + let signer_methods: Vec = semantics + .iter() + .filter_map(|sem| { + let field_name = &sem.core.ident; + if !matches!(sem.core.kind, resolve::FieldKind::Single) { + return None; + } + let _count = sem.address.as_ref().and_then(seed_expr_count)?; + let addr_expr = sem.address.as_ref()?; + let method_name = format_ident!("{}_signer", field_name); + if has_instruction_args { + Some(quote! { + #[inline(always)] + #[allow(unused_variables)] + pub fn #method_name<'__quasar_seed>( + &'__quasar_seed self, + bumps: &'__quasar_seed #bumps_name, + data: &'__quasar_seed [u8], + ) -> Result< + impl quasar_lang::cpi::CpiSignerSeeds + '__quasar_seed, + quasar_lang::prelude::ProgramError, + > { + let __ix_data = data; + #ix_arg_extraction + #(#field_refs)* + Ok(#addr_expr.with_bump(bumps.#field_name)) + } + }) + } else { + Some(quote! { + #[inline(always)] + #[allow(unused_variables)] + pub fn #method_name<'__quasar_seed>( + &'__quasar_seed self, + bumps: &'__quasar_seed #bumps_name, + ) -> impl quasar_lang::cpi::CpiSignerSeeds + '__quasar_seed { + #(#field_refs)* + #addr_expr.with_bump(bumps.#field_name) + } + }) + } + }) + .collect(); + + quote! { + impl #impl_generics #name #ty_generics #where_clause { + #(#signer_methods)* + } + + impl #impl_generics quasar_lang::traits::AccountBumps for #name #ty_generics #where_clause { + type Bumps = #bumps_name; + } + } +} + +fn seed_expr_count(expr: &Expr) -> Option { + match expr { + Expr::Call(call) if is_seeds_path(&call.func) => Some(call.args.len() + 2), + Expr::Paren(paren) => seed_expr_count(&paren.expr), + Expr::Group(group) => seed_expr_count(&group.expr), + _ => None, + } +} + +fn is_seeds_path(expr: &Expr) -> bool { + let Expr::Path(path) = expr else { + return false; + }; + path.path + .segments + .last() + .is_some_and(|segment| segment.ident == "seeds") +} + +fn composite_event_ty(ty: &Type) -> proc_macro2::TokenStream { + if let Type::Path(type_path) = ty { + if type_path + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "AccountsArray") + { + return quote! { #ty }; + } + } + strip_generics(ty) +} + fn is_event_cpi_field(sem: &resolve::FieldSemantics) -> bool { if sem.core.ident == "event_authority" { return true; diff --git a/derive/src/accounts/plan.rs b/derive/src/accounts/plan.rs index fa40646b..1017ad37 100644 --- a/derive/src/accounts/plan.rs +++ b/derive/src/accounts/plan.rs @@ -108,7 +108,7 @@ fn build_parse_fields(semantics: &[resolve::FieldSemantics]) -> Vec { - let inner_ty = strip_generics(&sem.core.effective_ty); + let inner_ty = composite_parse_ty(&sem.core.effective_ty); fields.push(ParseFieldPlan { field_name: sem.core.ident.clone(), offset_expr: offset_expr.clone(), @@ -132,6 +132,24 @@ fn build_parse_fields(semantics: &[resolve::FieldSemantics]) -> Vec proc_macro2::TokenStream { + if is_accounts_array_type(ty) { + return quote! { #ty }; + } + strip_generics(ty) +} + +fn is_accounts_array_type(ty: &syn::Type) -> bool { + if let syn::Type::Path(type_path) = ty { + return type_path + .path + .segments + .last() + .is_some_and(|segment| segment.ident == "AccountsArray"); + } + false +} + fn emit_parse_account_steps(fields: &[ParseFieldPlan]) -> Vec { fields.iter().map(emit_parse_field_step).collect() } @@ -142,16 +160,14 @@ fn emit_parse_field_step(field: &ParseFieldPlan) -> proc_macro2::TokenStream { let cur_offset = &field.offset_expr; quote! { { - let mut __inner_buf = core::mem::MaybeUninit::< - [quasar_lang::__internal::AccountView; <#inner_ty as AccountCount>::COUNT] - >::uninit(); - input = <#inner_ty>::parse_accounts(input, &mut __inner_buf, __program_id)?; - let __inner = unsafe { __inner_buf.assume_init() }; - let mut __j = 0usize; - while __j < <#inner_ty as AccountCount>::COUNT { - unsafe { core::ptr::write(base.add(#cur_offset + __j), *__inner.as_ptr().add(__j)); } - __j += 1; - } + input = unsafe { + <#inner_ty as quasar_lang::traits::ParseAccountsRaw>::parse_accounts_raw( + input, + base, + #cur_offset, + __program_id, + )? + }; } } } @@ -235,7 +251,7 @@ fn emit_count_expr(fields: &[ParseFieldPlan]) -> proc_macro2::TokenStream { let addends: Vec = fields .iter() .map(|field| match &field.kind { - ParseFieldKind::Composite { inner_ty } => { + ParseFieldKind::Composite { inner_ty, .. } => { quote! { <#inner_ty as AccountCount>::COUNT } } ParseFieldKind::Single(_) => quote! { 1usize }, @@ -264,11 +280,13 @@ fn emit_parse_body_from_inner( .any(|field| matches!(field.kind, ParseFieldKind::Composite { .. })) { let mut field_lets: Vec = Vec::new(); - field_lets.push(quote! { let mut __accounts_rest = accounts; }); + field_lets.push(quote! { + let mut __accounts_rest: &mut [quasar_lang::__internal::AccountView] = accounts; + }); for field in fields { match &field.kind { - ParseFieldKind::Composite { inner_ty } => { + ParseFieldKind::Composite { inner_ty, .. } => { let field_name = &field.field_name; let bumps_var = format_ident!("__composite_bumps_{}", field_name); field_lets.push(quote! { diff --git a/derive/src/helpers.rs b/derive/src/helpers.rs index e7299912..82f82d13 100644 --- a/derive/src/helpers.rs +++ b/derive/src/helpers.rs @@ -232,6 +232,12 @@ pub(crate) fn is_composite_type(ty: &Type) -> bool { } if let Type::Path(type_path) = ty { if let Some(last) = type_path.path.segments.last() { + if last.ident == "AccountsArray" { + return true; + } + if last.ident.to_string().ends_with("Accounts") { + return true; + } if let PathArguments::AngleBracketed(args) = &last.arguments { return args .args diff --git a/derive/src/seeds.rs b/derive/src/seeds.rs index e990a0b2..bfde655d 100644 --- a/derive/src/seeds.rs +++ b/derive/src/seeds.rs @@ -217,6 +217,18 @@ fn derive_seeds_inner(input: TokenStream) -> Result { } } + impl<'__quasar_seed> quasar_lang::cpi::CpiSignerSeeds for #seed_set_bump<'__quasar_seed> { + #[inline(always)] + fn with_signers(&self, f: F) -> R + where + F: FnOnce(&[quasar_lang::cpi::Signer<'_, '_>]) -> R, + { + let seeds = [#(#signer_seed_exprs_bump),*]; + let signer = quasar_lang::cpi::Signer::from(&seeds); + f(core::slice::from_ref(&signer)) + } + } + // AddressVerify: auto-find bump (full derivation, safe for init). impl<'__quasar_seed> quasar_lang::address::AddressVerify for #seed_set<'__quasar_seed> { #[inline(always)] diff --git a/derive/tests/compile_pass/account_signers.rs b/derive/tests/compile_pass/account_signers.rs new file mode 100644 index 00000000..0b8a38bf --- /dev/null +++ b/derive/tests/compile_pass/account_signers.rs @@ -0,0 +1,40 @@ +//! Generated context signer seeds. +#![allow(unexpected_cfgs)] +extern crate alloc; + +use quasar_derive::Accounts; +use quasar_lang::{cpi::CpiSignerSeeds, prelude::*}; + +solana_address::declare_id!("11111111111111111111111111111112"); + +#[account(discriminator = 8)] +#[seeds(b"vault", authority: Address)] +pub struct Vault { + pub authority: Address, + pub bump: u8, +} + +#[derive(Accounts)] +pub struct VaultAccounts { + pub authority: Signer, + + #[account(mut, address = Vault::seeds(authority.address()))] + pub vault: Account, +} + +#[derive(Accounts)] +pub struct UsesNestedVault { + pub vault_accounts: VaultAccounts, +} + +fn assert_cpi_signer(_seeds: &S) {} + +fn use_flat(ctx: Ctx) { + assert_cpi_signer(&ctx.accounts.vault_signer(&ctx.bumps)); +} + +fn use_nested(ctx: Ctx) { + assert_cpi_signer(&ctx.accounts.vault_accounts.vault_signer(&ctx.bumps.vault_accounts)); +} + +fn main() {} diff --git a/examples/escrow/src/instructions/refund.rs b/examples/escrow/src/instructions/refund.rs index 32de26cd..4b1021fe 100644 --- a/examples/escrow/src/instructions/refund.rs +++ b/examples/escrow/src/instructions/refund.rs @@ -27,14 +27,8 @@ pub struct Refund { impl Refund { #[inline(always)] - pub fn withdraw_tokens_and_close(&mut self, bumps: &RefundBumps) -> Result<(), ProgramError> { - let bump = [bumps.escrow]; - let seeds = [ - Seed::from(b"escrow" as &[u8]), - Seed::from(self.maker.address().as_ref()), - Seed::from(bump.as_ref()), - ]; - + pub fn withdraw_tokens_and_close(&self, bumps: &RefundBumps) -> Result<(), ProgramError> { + let escrow_signer = self.escrow_signer(bumps); self.token_program .transfer( &self.vault_ta_a, @@ -42,12 +36,11 @@ impl Refund { &self.escrow, self.vault_ta_a.amount(), ) - .invoke_signed(&seeds)?; + .invoke_signed(&escrow_signer)?; self.token_program .close_account(&self.vault_ta_a, &self.maker, &self.escrow) - .invoke_signed(&seeds)?; - Ok(()) + .invoke_signed(&escrow_signer) } #[inline(always)] diff --git a/examples/escrow/src/instructions/take.rs b/examples/escrow/src/instructions/take.rs index c05a8426..4934ac97 100644 --- a/examples/escrow/src/instructions/take.rs +++ b/examples/escrow/src/instructions/take.rs @@ -48,14 +48,8 @@ impl Take { } #[inline(always)] - pub fn withdraw_tokens_and_close(&mut self, bumps: &TakeBumps) -> Result<(), ProgramError> { - let bump = [bumps.escrow]; - let seeds = [ - Seed::from(b"escrow" as &[u8]), - Seed::from(self.maker.address().as_ref()), - Seed::from(bump.as_ref()), - ]; - + pub fn withdraw_tokens_and_close(&self, bumps: &TakeBumps) -> Result<(), ProgramError> { + let escrow_signer = self.escrow_signer(bumps); self.token_program .transfer( &self.vault_ta_a, @@ -63,12 +57,11 @@ impl Take { &self.escrow, self.vault_ta_a.amount(), ) - .invoke_signed(&seeds)?; + .invoke_signed(&escrow_signer)?; self.token_program .close_account(&self.vault_ta_a, &self.taker, &self.escrow) - .invoke_signed(&seeds)?; - Ok(()) + .invoke_signed(&escrow_signer) } #[inline(always)] diff --git a/examples/multisig/src/instructions/execute_transfer.rs b/examples/multisig/src/instructions/execute_transfer.rs index 888254a5..f50f1397 100644 --- a/examples/multisig/src/instructions/execute_transfer.rs +++ b/examples/multisig/src/instructions/execute_transfer.rs @@ -48,15 +48,10 @@ impl ExecuteTransfer { return Err(ProgramError::MissingRequiredSignature); } - let bump = [bumps.vault]; - let seeds = [ - Seed::from(b"vault" as &[u8]), - Seed::from(self.config.address().as_ref()), - Seed::from(bump.as_ref()), - ]; + let vault_signer = self.vault_signer(bumps); self.system_program .transfer(&self.vault, &self.recipient, amount) - .invoke_signed(&seeds)?; + .invoke_signed(&vault_signer)?; Ok(()) } } diff --git a/lang/src/context.rs b/lang/src/context.rs index f7713818..caadadc5 100644 --- a/lang/src/context.rs +++ b/lang/src/context.rs @@ -55,7 +55,7 @@ pub struct Ctx<'input, T: ParseAccounts<'input> + ParseAccountsUnchecked<'input> pub accounts: T, /// PDA bump seeds discovered during validation. - pub bumps: T::Bumps, + pub bumps: >::Bumps, /// 32-byte program ID (raw bytes, not [`Address`]). pub program_id: &'input [u8; 32], @@ -102,7 +102,7 @@ pub struct CtxWithRemaining< pub accounts: T, /// PDA bump seeds discovered during validation. - pub bumps: T::Bumps, + pub bumps: >::Bumps, /// 32-byte program ID (raw bytes). pub program_id: &'input [u8; 32], diff --git a/lang/src/cpi/dyn_cpi.rs b/lang/src/cpi/dyn_cpi.rs index 5962a6ce..1e0e549d 100644 --- a/lang/src/cpi/dyn_cpi.rs +++ b/lang/src/cpi/dyn_cpi.rs @@ -8,7 +8,7 @@ use { super::{ cpi_account_from_view, get_cpi_return, invoke_raw, result_from_raw, CpiReturn, - InstructionAccount, Seed, Signer, + CpiSignerSeeds, InstructionAccount, Signer, }, crate::utils::hint::unlikely, core::mem::MaybeUninit, @@ -210,8 +210,11 @@ impl<'a, const MAX_ACCTS: usize, const MAX_DATA: usize> CpiDynamic<'a, MAX_ACCTS /// Invoke the CPI with a single PDA signer (seeds for one address). #[inline(always)] #[must_use = "CPI result must be handled with `?` or matched"] - pub fn invoke_signed(&self, seeds: &[Seed]) -> ProgramResult { - self.invoke_inner(&[Signer::from(seeds)]) + pub fn invoke_signed(&self, seeds: &S) -> ProgramResult + where + S: CpiSignerSeeds + ?Sized, + { + seeds.with_signers(|signers| self.invoke_inner(signers)) } /// Invoke the CPI with multiple PDA signers. @@ -231,8 +234,11 @@ impl<'a, const MAX_ACCTS: usize, const MAX_DATA: usize> CpiDynamic<'a, MAX_ACCTS /// Invoke the CPI with one PDA signer and read back raw return data. #[inline(always)] #[must_use = "CPI result must be handled with `?` or matched"] - pub fn invoke_signed_with_return(&self, seeds: &[Seed]) -> Result { - self.invoke_with_return_inner(&[Signer::from(seeds)]) + pub fn invoke_signed_with_return(&self, seeds: &S) -> Result + where + S: CpiSignerSeeds + ?Sized, + { + seeds.with_signers(|signers| self.invoke_with_return_inner(signers)) } /// Invoke the CPI with multiple PDA signers and read back raw return data. diff --git a/lang/src/cpi/mod.rs b/lang/src/cpi/mod.rs index f6097843..bfa5f0a0 100644 --- a/lang/src/cpi/mod.rs +++ b/lang/src/cpi/mod.rs @@ -27,6 +27,16 @@ pub use { }, }; +/// Signer seed source for a single PDA signer. +/// +/// Generated account-owned signer helpers return values that implement this +/// trait, so CPI callers do not need to materialize or expose raw seed slices. +pub trait CpiSignerSeeds { + fn with_signers(&self, f: F) -> R + where + F: FnOnce(&[Signer<'_, '_>]) -> R; +} + #[cfg(any(target_os = "solana", target_arch = "bpf"))] #[repr(C)] struct CInstruction<'a> { @@ -352,8 +362,11 @@ impl<'a, const ACCTS: usize, const DATA: usize> CpiCall<'a, ACCTS, DATA> { /// Invoke the CPI with a single PDA signer (seeds for one address). #[inline(always)] #[must_use = "CPI result must be handled with `?` or matched"] - pub fn invoke_signed(&self, seeds: &[Seed]) -> ProgramResult { - self.invoke_inner(&[Signer::from(seeds)]) + pub fn invoke_signed(&self, seeds: &S) -> ProgramResult + where + S: CpiSignerSeeds + ?Sized, + { + seeds.with_signers(|signers| self.invoke_inner(signers)) } /// Invoke the CPI with multiple PDA signers. @@ -373,8 +386,11 @@ impl<'a, const ACCTS: usize, const DATA: usize> CpiCall<'a, ACCTS, DATA> { /// Invoke the CPI with one PDA signer and read back raw return data. #[inline(always)] #[must_use = "CPI result must be handled with `?` or matched"] - pub fn invoke_signed_with_return(&self, seeds: &[Seed]) -> Result { - self.invoke_with_return_inner(&[Signer::from(seeds)]) + pub fn invoke_signed_with_return(&self, seeds: &S) -> Result + where + S: CpiSignerSeeds + ?Sized, + { + seeds.with_signers(|signers| self.invoke_with_return_inner(signers)) } /// Invoke the CPI with multiple PDA signers and read back raw return data. diff --git a/lang/src/prelude.rs b/lang/src/prelude.rs index 25cd22d7..cb43abe1 100644 --- a/lang/src/prelude.rs +++ b/lang/src/prelude.rs @@ -14,7 +14,7 @@ pub use { context::{Context, Ctx, CtxWithRemaining}, cpi::{ system::{SystemProgram, SYSTEM_PROGRAM_ID}, - CpiDynamic, CpiReturn, Seed, + CpiDynamic, CpiReturn, CpiSignerSeeds, }, dispatch, emit, error::QuasarError, @@ -24,6 +24,7 @@ pub use { PodBool, PodI128, PodI16, PodI32, PodI64, PodString, PodU128, PodU16, PodU32, PodU64, PodVec, }, + remaining::{Remaining, RemainingAccount, RemainingAccounts}, require, require_eq, require_keys_eq, return_data::set_return_data, sysvars::{clock::Clock, rent::Rent}, diff --git a/lang/src/traits.rs b/lang/src/traits.rs index f202115b..c5953f5b 100644 --- a/lang/src/traits.rs +++ b/lang/src/traits.rs @@ -145,6 +145,12 @@ pub trait ParseAccounts<'input>: Sized { } } +/// Internal bump bundle carrier for macro-generated account structs. +#[doc(hidden)] +pub trait AccountBumps { + type Bumps: Copy; +} + /// Internal exact-length parsing fast path used by dispatch and nested /// composite account parsing. /// @@ -174,6 +180,24 @@ pub unsafe trait ParseAccountsUnchecked<'input>: ParseAccounts<'input> { } } +/// Internal raw SVM-buffer parser used by generated account composition code. +/// +/// Implemented by `#[derive(Accounts)]` and fixed-size account group wrappers. +/// This preserves header/duplicate parsing for nested account groups before the +/// typed `ParseAccountsUnchecked` pass loads account wrappers. +#[doc(hidden)] +pub unsafe trait ParseAccountsRaw: AccountCount { + /// # Safety + /// + /// `base.add(offset..offset + Self::COUNT)` must be writable. + unsafe fn parse_accounts_raw( + input: *mut u8, + base: *mut AccountView, + offset: usize, + program_id: &Address, + ) -> Result<*mut u8, ProgramError>; +} + /// Convert a typed account wrapper to its underlying [`AccountView`]. /// /// All account types (`Account`, `Signer`, `UncheckedAccount`, etc.) diff --git a/metadata/src/init.rs b/metadata/src/init.rs index 839bbd3c..2f982860 100644 --- a/metadata/src/init.rs +++ b/metadata/src/init.rs @@ -1,4 +1,8 @@ -use {super::instructions::MetadataCpi, crate::codec::BorshCpiEncode, quasar_lang::prelude::*}; +use { + super::instructions::MetadataCpi, + crate::codec::BorshCpiEncode, + quasar_lang::{cpi::Seed, prelude::*}, +}; /// Extension trait for metadata account initialization. /// @@ -92,7 +96,7 @@ pub trait InitMetadata: AsAccountView + Sized { is_mutable, true, )? - .invoke_signed(seeds) + .invoke_with_signers(&[quasar_lang::cpi::Signer::from(seeds)]) } } @@ -175,6 +179,6 @@ pub trait InitMasterEdition: AsAccountView + Sized { rent, max_supply, ) - .invoke_signed(seeds) + .invoke_with_signers(&[quasar_lang::cpi::Signer::from(seeds)]) } } From d48b068fee247ebfa620e295de4dbbbc4292a229 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 11:32:40 -0400 Subject: [PATCH 3/9] Add fixed-size account arrays Introduce AccountsArray for bounded repeated account groups that preserve typed parsing, bumps, epilogues, and nested account behavior. --- derive/tests/compile_pass/account_arrays.rs | 26 +++ lang/src/accounts/array.rs | 183 ++++++++++++++++++++ lang/src/accounts/mod.rs | 2 + 3 files changed, 211 insertions(+) create mode 100644 derive/tests/compile_pass/account_arrays.rs create mode 100644 lang/src/accounts/array.rs diff --git a/derive/tests/compile_pass/account_arrays.rs b/derive/tests/compile_pass/account_arrays.rs new file mode 100644 index 00000000..392603cf --- /dev/null +++ b/derive/tests/compile_pass/account_arrays.rs @@ -0,0 +1,26 @@ +//! Bounded reusable account groups. +#![allow(unexpected_cfgs)] +extern crate alloc; + +use quasar_derive::Accounts; +use quasar_lang::prelude::*; + +solana_address::declare_id!("11111111111111111111111111111112"); + +#[derive(Accounts)] +pub struct SignerPair { + pub first: Signer, + pub second: Signer, +} + +#[derive(Accounts)] +pub struct UsesAccountArray { + pub payer: Signer, + pub pairs: AccountsArray, +} + +fn main() { + fn _assert_count() {} + _assert_count::>(); + _assert_count::(); +} diff --git a/lang/src/accounts/array.rs b/lang/src/accounts/array.rs new file mode 100644 index 00000000..0aa22a69 --- /dev/null +++ b/lang/src/accounts/array.rs @@ -0,0 +1,183 @@ +use { + crate::{ + prelude::*, + traits::{check_account_count, AccountBumps, ParseAccountsRaw, ParseAccountsUnchecked}, + }, + core::mem::MaybeUninit, +}; + +/// Fixed-size repeated account group. +/// +/// `AccountsArray` always consumes exactly `N * T::COUNT` accounts, where +/// `T` is another `#[derive(Accounts)]` struct. +pub struct AccountsArray { + items: [T; N], +} + +impl AccountsArray { + #[inline(always)] + pub fn as_slice(&self) -> &[T] { + &self.items + } + + #[inline(always)] + pub fn iter(&self) -> core::slice::Iter<'_, T> { + self.items.iter() + } + + #[inline(always)] + pub const fn len(&self) -> usize { + N + } + + #[inline(always)] + pub const fn is_empty(&self) -> bool { + N == 0 + } +} + +impl core::ops::Deref for AccountsArray { + type Target = [T; N]; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.items + } +} + +impl AsRef<[T]> for AccountsArray { + #[inline(always)] + fn as_ref(&self) -> &[T] { + self.as_slice() + } +} + +impl AccountCount for AccountsArray +where + T: AccountCount, +{ + const COUNT: usize = T::COUNT * N; + const NEEDS_EVENT_CPI: bool = N > 0 && T::NEEDS_EVENT_CPI; +} + +unsafe impl ParseAccountsRaw for AccountsArray +where + T: ParseAccountsRaw, +{ + #[inline(always)] + unsafe fn parse_accounts_raw( + mut input: *mut u8, + base: *mut AccountView, + offset: usize, + program_id: &Address, + ) -> Result<*mut u8, ProgramError> { + let mut i = 0usize; + while i < N { + input = T::parse_accounts_raw(input, base, offset + i * T::COUNT, program_id)?; + i += 1; + } + Ok(input) + } +} + +impl<'input, T, const N: usize> ParseAccounts<'input> for AccountsArray +where + T: ParseAccounts<'input> + ParseAccountsUnchecked<'input> + AccountCount, +{ + type Bumps = [T::Bumps; N]; + + #[inline(always)] + fn parse( + accounts: &'input mut [AccountView], + program_id: &Address, + ) -> Result<(Self, Self::Bumps), ProgramError> { + check_account_count(accounts.len(), Self::COUNT)?; + unsafe { Self::parse_unchecked(accounts, program_id) } + } + + #[inline(always)] + fn parse_with_instruction_data( + accounts: &'input mut [AccountView], + data: &[u8], + program_id: &Address, + ) -> Result<(Self, Self::Bumps), ProgramError> { + check_account_count(accounts.len(), Self::COUNT)?; + unsafe { Self::parse_with_instruction_data_unchecked(accounts, data, program_id) } + } + + const HAS_EPILOGUE: bool = T::HAS_EPILOGUE; + + #[inline(always)] + fn epilogue(&mut self) -> Result<(), ProgramError> { + let mut i = 0usize; + while i < N { + self.items[i].epilogue()?; + i += 1; + } + Ok(()) + } +} + +unsafe impl<'input, T, const N: usize> ParseAccountsUnchecked<'input> for AccountsArray +where + T: ParseAccounts<'input> + ParseAccountsUnchecked<'input> + AccountCount, +{ + #[inline(always)] + unsafe fn parse_unchecked( + accounts: &'input mut [AccountView], + program_id: &Address, + ) -> Result<(Self, Self::Bumps), ProgramError> { + Self::parse_with_instruction_data_unchecked(accounts, &[], program_id) + } + + #[inline(always)] + unsafe fn parse_with_instruction_data_unchecked( + accounts: &'input mut [AccountView], + data: &[u8], + program_id: &Address, + ) -> Result<(Self, Self::Bumps), ProgramError> { + let mut items = MaybeUninit::<[T; N]>::uninit(); + let mut bumps = MaybeUninit::<[T::Bumps; N]>::uninit(); + let items_ptr = items.as_mut_ptr() as *mut T; + let bumps_ptr = bumps.as_mut_ptr() as *mut T::Bumps; + + let mut rest = accounts; + let mut i = 0usize; + while i < N { + let (chunk, next) = rest.split_at_mut(T::COUNT); + rest = next; + let (item, item_bumps) = + match T::parse_with_instruction_data_unchecked(chunk, data, program_id) { + Ok(parsed) => parsed, + Err(err) => { + let mut j = 0usize; + while j < i { + unsafe { + core::ptr::drop_in_place(items_ptr.add(j)); + core::ptr::drop_in_place(bumps_ptr.add(j)); + } + j += 1; + } + return Err(err); + } + }; + core::ptr::write(items_ptr.add(i), item); + core::ptr::write(bumps_ptr.add(i), item_bumps); + i += 1; + } + + Ok(( + Self { + items: items.assume_init(), + }, + bumps.assume_init(), + )) + } +} + +impl AccountBumps for AccountsArray +where + T: AccountBumps, +{ + type Bumps = [T::Bumps; N]; +} diff --git a/lang/src/accounts/mod.rs b/lang/src/accounts/mod.rs index f92e7f12..7cb521e5 100644 --- a/lang/src/accounts/mod.rs +++ b/lang/src/accounts/mod.rs @@ -25,3 +25,5 @@ pub mod migration; pub use migration::*; pub mod uninit; pub use uninit::*; +pub mod array; +pub use array::*; From f90260796d6002dde21d553fcf8f6634274ccf1a Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 11:32:54 -0400 Subject: [PATCH 4/9] Add bounded typed remaining accounts Let handlers parse remaining-account tails into capped typed slices with duplicate and declared-account rejection before use. --- lang/src/remaining.rs | 122 ++++++++++++++++++++- lang/tests/compile_pass/remaining_typed.rs | 17 +++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 lang/tests/compile_pass/remaining_typed.rs diff --git a/lang/src/remaining.rs b/lang/src/remaining.rs index 7a6e7775..b9d5dbce 100644 --- a/lang/src/remaining.rs +++ b/lang/src/remaining.rs @@ -1,5 +1,5 @@ use { - crate::error::QuasarError, + crate::{account_load::AccountLoad, error::QuasarError}, solana_account_view::{AccountView, Ref, RefMut, RuntimeAccount, NOT_BORROWED}, solana_address::Address, solana_program_error::ProgramError, @@ -283,6 +283,14 @@ impl<'a> RemainingAccounts<'a> { cache: core::mem::MaybeUninit::uninit(), } } + + #[inline(always)] + pub fn parse(&self) -> Result, ProgramError> + where + T: AccountLoad, + { + Remaining::parse(Self::new(self.ptr, self.boundary, self.declared)) + } } /// Walk-based dup resolution for one-off `get()` access. @@ -343,6 +351,118 @@ fn resolve_dup_walk( /// `Err(QuasarError::RemainingAccountsOverflow)` after 64 accounts. pub type RemainingIter<'a> = RemainingIterImpl<'a, false>; +/// Bounded typed view over a remaining-account tail. +/// +/// `Remaining` accepts any number of remaining accounts up to `N` and +/// validates each one as `T`. Use raw [`RemainingAccounts`] when the account +/// tail is intentionally uncapped or forwarded without local validation. +pub struct Remaining { + items: [core::mem::MaybeUninit; N], + len: usize, +} + +impl Remaining +where + T: AccountLoad, +{ + pub fn parse(accounts: RemainingAccounts<'_>) -> Result { + let mut out = Self { + // SAFETY: An uninitialized `[MaybeUninit; N]` is valid. + items: unsafe { + core::mem::MaybeUninit::<[core::mem::MaybeUninit; N]>::uninit().assume_init() + }, + len: 0, + }; + let mut seen = unsafe { + core::mem::MaybeUninit::<[core::mem::MaybeUninit
; N]>::uninit().assume_init() + }; + let mut seen_len = 0usize; + + for account in accounts.iter() { + let account = account?; + if out.len >= N { + return Err(QuasarError::RemainingAccountsOverflow.into()); + } + + let view = unsafe { account.as_account_view_unchecked() }; + if accounts + .declared + .iter() + .any(|declared| crate::keys_eq(declared.address(), view.address())) + { + return Err(QuasarError::RemainingAccountDuplicate.into()); + } + let mut i = 0usize; + while i < seen_len { + let seen_address = unsafe { seen[i].assume_init_ref() }; + if crate::keys_eq(seen_address, view.address()) { + return Err(QuasarError::RemainingAccountDuplicate.into()); + } + i += 1; + } + + if T::IS_SIGNER && !view.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if T::IS_EXECUTABLE && !view.executable() { + return Err(ProgramError::InvalidAccountData); + } + + let item = T::load_checked(view)?; + seen[seen_len].write(*view.address()); + seen_len += 1; + out.items[out.len].write(item); + out.len += 1; + } + + Ok(out) + } +} + +impl Remaining { + #[inline(always)] + pub fn as_slice(&self) -> &[T] { + unsafe { core::slice::from_raw_parts(self.items.as_ptr() as *const T, self.len) } + } + + #[inline(always)] + pub fn iter(&self) -> core::slice::Iter<'_, T> { + self.as_slice().iter() + } + + #[inline(always)] + pub const fn len(&self) -> usize { + self.len + } + + #[inline(always)] + pub const fn capacity(&self) -> usize { + N + } + + #[inline(always)] + pub const fn is_empty(&self) -> bool { + self.len == 0 + } +} + +impl Drop for Remaining { + fn drop(&mut self) { + let mut i = 0usize; + while i < self.len { + unsafe { self.items[i].assume_init_drop() }; + i += 1; + } + } +} + +impl AsRef<[T]> for Remaining { + #[inline(always)] + fn as_ref(&self) -> &[T] { + self.as_slice() + } +} + #[doc(hidden)] pub struct RemainingIterImpl<'a, const _SPECIALIZE: bool = false> { /// Current position in the SVM input buffer. diff --git a/lang/tests/compile_pass/remaining_typed.rs b/lang/tests/compile_pass/remaining_typed.rs new file mode 100644 index 00000000..a35990d3 --- /dev/null +++ b/lang/tests/compile_pass/remaining_typed.rs @@ -0,0 +1,17 @@ +#![allow(unexpected_cfgs)] +use quasar_lang::prelude::*; + +fn parse_signers(accounts: RemainingAccounts<'_>) -> Result, ProgramError> { + accounts.parse() +} + +fn main() { + fn _assert_remaining() { + let _ = core::mem::size_of::>(); + } + + _assert_remaining::(); + _assert_remaining::(); + + let _ = parse_signers; +} From d13342db63e7df67c4192cd9bf01b9c21acd7438 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 13:38:35 -0400 Subject: [PATCH 5/9] Lazily resolve rent for lifecycle ops Defer rent sysvar fetching until init or realloc actually needs the value, while keeping direct borrowed-rent codegen for explicit rent accounts. --- derive/src/account/fixed.rs | 6 +- derive/src/accounts/emit/parse.rs | 15 +-- derive/src/accounts/emit/typed_emit.rs | 4 +- .../compile_pass/ata_init_without_rent.rs | 34 ++++++ lang/src/account_init.rs | 8 +- lang/src/accounts/account.rs | 4 +- lang/src/accounts/interface_account.rs | 4 +- lang/src/lib.rs | 2 +- lang/src/ops/init.rs | 14 ++- lang/src/ops/mod.rs | 105 +++++++++++++++--- lang/src/ops/realloc.rs | 14 +-- .../compile_fail/realloc_bad_signer.stderr | 14 ++- .../compile_fail/realloc_bad_unchecked.stderr | 14 ++- metadata/src/account_init.rs | 8 +- spl/src/lib.rs | 12 +- 15 files changed, 193 insertions(+), 65 deletions(-) create mode 100644 derive/tests/compile_pass/ata_init_without_rent.rs diff --git a/derive/src/account/fixed.rs b/derive/src/account/fixed.rs index b87a81ba..1440d303 100644 --- a/derive/src/account/fixed.rs +++ b/derive/src/account/fixed.rs @@ -169,8 +169,8 @@ pub(super) fn generate_account( type InitParams<'a> = (); #[inline(always)] - fn init<'a>( - ctx: quasar_lang::account_init::InitCtx<'a>, + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, _params: &(), ) -> Result<(), quasar_lang::prelude::ProgramError> { quasar_lang::account_init::init_account( @@ -179,7 +179,7 @@ pub(super) fn generate_account( ctx.space, ctx.program_id, ctx.signers, - ctx.rent, + ctx.rent.get()?, ::DISCRIMINATOR, ) } diff --git a/derive/src/accounts/emit/parse.rs b/derive/src/accounts/emit/parse.rs index a384e610..70e209fb 100644 --- a/derive/src/accounts/emit/parse.rs +++ b/derive/src/accounts/emit/parse.rs @@ -3,9 +3,8 @@ //! Generated parse body shape: //! //! ```text -//! // Rent (only when init/realloc/migration needs it) -//! let __rent: Rent = Sysvar::get()?; -//! let __rent_ctx = OpCtxWithRent::new(&program_id, &__rent); +//! // Rent source (only when init/realloc/migration may need it) +//! let __rent_ctx = OpCtx::new(&program_id, &__rent); //! //! // Phase 1: load non-init fields //! let field_a = ::load(field_a)?; @@ -114,7 +113,7 @@ fn emit_rent_context( #field.borrow_unchecked() ) }; - let __rent_ctx = quasar_lang::ops::OpCtxWithRent::new( + let __rent_ctx = quasar_lang::ops::OpCtx::new( unsafe { &*(__program_id as *const quasar_lang::prelude::Address) }, __rent, ); @@ -122,11 +121,9 @@ fn emit_rent_context( } RentPlan::FetchOnce => { quote! { - let __rent: quasar_lang::sysvars::rent::Rent = - ::get()?; - let __rent_ctx = quasar_lang::ops::OpCtxWithRent::new( + let __rent_ctx = quasar_lang::ops::OpCtx::new( unsafe { &*(__program_id as *const quasar_lang::prelude::Address) }, - &__rent, + quasar_lang::ops::RentResolver::fetch_once(), ); } } @@ -256,7 +253,7 @@ fn emit_post_load_typed( space: (#realloc_expr) as usize, payer: #payer_ident.to_account_view(), }; - __realloc_op.apply::<#ty>(&mut #ident, &__rent_ctx)?; + __realloc_op.apply::<#ty, _>(&mut #ident, &__rent_ctx)?; } }, true, diff --git a/derive/src/accounts/emit/typed_emit.rs b/derive/src/accounts/emit/typed_emit.rs index 0df8b80a..02fbc055 100644 --- a/derive/src/accounts/emit/typed_emit.rs +++ b/derive/src/accounts/emit/typed_emit.rs @@ -121,7 +121,7 @@ pub(crate) fn emit_behavior_init( params: __init_params, idempotent: #idempotent, }; - __init_op.apply::<#field_ty>(#field_ident, &__rent_ctx)?; + __init_op.apply::<#field_ty, _>(#field_ident, &__rent_ctx)?; #did_init_assignment }; @@ -182,7 +182,7 @@ pub(crate) fn emit_program_init( params: __init_params, idempotent: #idempotent, }; - __init_op.apply::<#field_ty>(#field_ident, &__rent_ctx)?; + __init_op.apply::<#field_ty, _>(#field_ident, &__rent_ctx)?; }; let body = if has_address { diff --git a/derive/tests/compile_pass/ata_init_without_rent.rs b/derive/tests/compile_pass/ata_init_without_rent.rs new file mode 100644 index 00000000..81685a33 --- /dev/null +++ b/derive/tests/compile_pass/ata_init_without_rent.rs @@ -0,0 +1,34 @@ +//! ATA init does not require Quasar to materialize Rent. +#![allow(unexpected_cfgs)] + +extern crate alloc; + +use { + quasar_derive::Accounts, + quasar_lang::prelude::*, + quasar_spl::{accounts::associated_token, TokenProgram, *}, +}; + +solana_address::declare_id!("11111111111111111111111111111112"); + +#[derive(Accounts)] +pub struct InitAtaWithoutRent { + #[account(mut)] + pub payer: Signer, + pub mint: Account, + #[account(mut, + init(idempotent), + associated_token( + authority = payer, mint = mint, + token_program = token_program, + system_program = system_program, + ata_program = ata_program, + ), + )] + pub ata_vault: Account, + pub token_program: Program, + pub system_program: Program, + pub ata_program: Program, +} + +fn main() {} diff --git a/lang/src/account_init.rs b/lang/src/account_init.rs index 8118f36e..5770c0bc 100644 --- a/lang/src/account_init.rs +++ b/lang/src/account_init.rs @@ -1,6 +1,7 @@ use { crate::{ cpi::{system, Signer}, + ops::RentAccess, sysvars::rent::Rent, }, solana_account_view::AccountView, @@ -9,13 +10,13 @@ use { }; /// Context for account initialization CPI. -pub struct InitCtx<'a> { +pub struct InitCtx<'a, R: RentAccess> { pub payer: &'a AccountView, pub target: &'a mut AccountView, pub program_id: &'a Address, pub space: u64, pub signers: &'a [Signer<'a, 'a>], - pub rent: &'a Rent, + pub rent: &'a R, } /// Initialization behavior for account types. @@ -37,7 +38,8 @@ pub trait AccountInit { /// runtime error if no behavior fills the params. const DEFAULT_INIT_PARAMS_VALID: bool = true; - fn init<'a>(ctx: InitCtx<'a>, params: &Self::InitParams<'a>) -> ProgramResult; + fn init<'a, R: RentAccess>(ctx: InitCtx<'a, R>, params: &Self::InitParams<'a>) + -> ProgramResult; } /// Create account via system program + write discriminator. diff --git a/lang/src/accounts/account.rs b/lang/src/accounts/account.rs index 0317bf38..5babe8ec 100644 --- a/lang/src/accounts/account.rs +++ b/lang/src/accounts/account.rs @@ -393,8 +393,8 @@ impl crate::account_init::AccountInit for A const DEFAULT_INIT_PARAMS_VALID: bool = T::DEFAULT_INIT_PARAMS_VALID; #[inline(always)] - fn init<'a>( - ctx: crate::account_init::InitCtx<'a>, + fn init<'a, R: crate::ops::RentAccess>( + ctx: crate::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> solana_program_error::ProgramResult { T::init(ctx, params) diff --git a/lang/src/accounts/interface_account.rs b/lang/src/accounts/interface_account.rs index d8027ba8..52303b6e 100644 --- a/lang/src/accounts/interface_account.rs +++ b/lang/src/accounts/interface_account.rs @@ -95,8 +95,8 @@ impl crate::account_init::AccountInit for I const DEFAULT_INIT_PARAMS_VALID: bool = T::DEFAULT_INIT_PARAMS_VALID; #[inline(always)] - fn init<'a>( - ctx: crate::account_init::InitCtx<'a>, + fn init<'a, R: crate::ops::RentAccess>( + ctx: crate::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> solana_program_error::ProgramResult { T::init(ctx, params) diff --git a/lang/src/lib.rs b/lang/src/lib.rs index 66e92ec1..b26b7a5c 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -361,7 +361,7 @@ pub mod event; pub mod instruction_arg; /// Low-level `sol_log_data` syscall wrapper. pub mod log; -/// Op-dispatch: `OpCtxWithRent`, `SupportsRealloc`, structural ops (init, +/// Op-dispatch: `OpCtx`, `SupportsRealloc`, structural ops (init, /// realloc, close). pub mod ops; /// Program Derived Address creation and lookup. diff --git a/lang/src/ops/init.rs b/lang/src/ops/init.rs index 5ecbfe3c..aff162ca 100644 --- a/lang/src/ops/init.rs +++ b/lang/src/ops/init.rs @@ -5,7 +5,7 @@ //! `idempotent = true`, already-initialized accounts are silently accepted. use { - super::OpCtxWithRent, + super::{OpCtx, RentAccess}, crate::{ account_init::{AccountInit, InitCtx}, account_load::AccountLoad, @@ -31,11 +31,15 @@ pub struct Op<'a, Params = ()> { impl<'a, P> Op<'a, P> { /// Execute the init operation on a raw account slot. #[inline(always)] - pub fn apply = P>>( + pub fn apply( &self, slot: &mut AccountView, - ctx: &'a OpCtxWithRent<'a>, - ) -> Result<(), ProgramError> { + ctx: &'a OpCtx<'a, R>, + ) -> Result<(), ProgramError> + where + F: AccountLoad + AccountInit = P>, + R: RentAccess, + { if crate::is_system_program(slot.owner()) { // SAFETY: lifetime unification — all refs are live for the inlined call. let target = unsafe { &mut *(slot as *mut AccountView) }; @@ -47,7 +51,7 @@ impl<'a, P> Op<'a, P> { program_id, space: self.space, signers: self.signers, - rent: ctx.rent, + rent: &ctx.rent, }, &self.params, )?; diff --git a/lang/src/ops/mod.rs b/lang/src/ops/mod.rs index 35a0536d..01de9b56 100644 --- a/lang/src/ops/mod.rs +++ b/lang/src/ops/mod.rs @@ -4,29 +4,89 @@ //! contribution, and exit actions. Structural ops (init, realloc, PDA //! verification) use their own inherent methods. //! -//! `OpCtxWithRent` carries instruction-scoped state (program_id + &Rent). -//! The derive emits this when any field uses init, realloc, or migration. +//! `OpCtx` carries instruction-scoped state. Rent is resolved lazily so +//! idempotent/no-op init paths do not pay for sysvar access. pub mod close; pub mod init; pub mod realloc; -/// Context with rent: program_id + pre-fetched Rent. +use core::{ + cell::{Cell, UnsafeCell}, + mem::MaybeUninit, +}; + +#[doc(hidden)] +pub trait RentAccess { + fn get(&self) -> Result<&crate::sysvars::rent::Rent, solana_program_error::ProgramError>; +} + +impl RentAccess for crate::sysvars::rent::Rent { + #[inline(always)] + fn get(&self) -> Result<&crate::sysvars::rent::Rent, solana_program_error::ProgramError> { + Ok(self) + } +} + +impl RentAccess for &crate::sysvars::rent::Rent { + #[inline(always)] + fn get(&self) -> Result<&crate::sysvars::rent::Rent, solana_program_error::ProgramError> { + Ok(*self) + } +} + +/// Lazily resolves Rent for lifecycle operations. /// -/// The derive emits this when any field uses init, realloc, or migration. -/// Rent is populated exactly once at instruction entry — either deserialized -/// from a `Sysvar` field or fetched via `Rent::get()` syscall. -pub struct OpCtxWithRent<'a> { +/// Used only when no `Sysvar` account is present. The syscall is +/// deferred until the first operation that actually needs a rent value. +#[doc(hidden)] +pub struct RentResolver { + fetched: Cell, + cached: UnsafeCell>, +} + +impl RentResolver { + #[inline(always)] + pub fn fetch_once() -> Self { + Self { + fetched: Cell::new(false), + cached: UnsafeCell::new(MaybeUninit::uninit()), + } + } +} + +impl RentAccess for RentResolver { + #[inline(always)] + fn get(&self) -> Result<&crate::sysvars::rent::Rent, solana_program_error::ProgramError> { + if !self.fetched.get() { + let rent = ::get()?; + unsafe { (*self.cached.get()).write(rent) }; + self.fetched.set(true); + } + + Ok(unsafe { &*(*self.cached.get()).as_ptr() }) + } +} + +impl Drop for RentResolver { + #[inline(always)] + fn drop(&mut self) { + if self.fetched.get() { + unsafe { (*self.cached.get()).assume_init_drop() }; + } + } +} + +/// Lifecycle operation context. +#[doc(hidden)] +pub struct OpCtx<'a, R> { pub program_id: &'a solana_address::Address, - pub rent: &'a crate::sysvars::rent::Rent, + pub rent: R, } -impl<'a> OpCtxWithRent<'a> { +impl<'a, R> OpCtx<'a, R> { #[inline(always)] - pub fn new( - program_id: &'a solana_address::Address, - rent: &'a crate::sysvars::rent::Rent, - ) -> Self { + pub fn new(program_id: &'a solana_address::Address, rent: R) -> Self { Self { program_id, rent } } } @@ -36,3 +96,22 @@ impl<'a> OpCtxWithRent<'a> { /// The `realloc::Op` requires `F: SupportsRealloc` to ensure only /// realloc-capable accounts are used with `realloc(...)`. pub trait SupportsRealloc {} + +#[cfg(test)] +mod tests { + use super::{RentAccess, RentResolver}; + + #[test] + fn borrowed_rent_access_returns_same_rent() { + let rent: crate::sysvars::rent::Rent = unsafe { core::mem::zeroed() }; + let borrowed = &rent; + let resolved = borrowed.get().unwrap(); + assert!(core::ptr::eq(resolved, &rent)); + } + + #[test] + fn rent_resolver_starts_unfetched() { + let resolver = RentResolver::fetch_once(); + assert!(!resolver.fetched.get()); + } +} diff --git a/lang/src/ops/realloc.rs b/lang/src/ops/realloc.rs index 6e7bf982..020824f6 100644 --- a/lang/src/ops/realloc.rs +++ b/lang/src/ops/realloc.rs @@ -4,7 +4,7 @@ //! discriminator. Rejects shrinking below the account type's minimum Space. use { - super::{OpCtxWithRent, SupportsRealloc}, + super::{OpCtx, RentAccess, SupportsRealloc}, crate::account_load::AccountLoad, solana_account_view::AccountView, solana_program_error::ProgramError, @@ -19,16 +19,16 @@ pub struct Op<'a> { impl<'a> Op<'a> { /// Apply realloc to a field. #[inline(always)] - pub fn apply( - &self, - field: &mut F, - ctx: &OpCtxWithRent<'_>, - ) -> Result<(), ProgramError> { + pub fn apply(&self, field: &mut F, ctx: &OpCtx<'_, R>) -> Result<(), ProgramError> + where + F: AccountLoad + SupportsRealloc + crate::traits::Space, + R: RentAccess, + { let min_space = ::SPACE; if self.space < min_space { return Err(ProgramError::AccountDataTooSmall); } let view = unsafe { ::to_account_view_mut(field) }; - crate::accounts::realloc_account(view, self.space, self.payer, Some(ctx.rent)) + crate::accounts::realloc_account(view, self.space, self.payer, Some(ctx.rent.get()?)) } } diff --git a/lang/tests/compile_fail/realloc_bad_signer.stderr b/lang/tests/compile_fail/realloc_bad_signer.stderr index 87e968f9..63736d93 100644 --- a/lang/tests/compile_fail/realloc_bad_signer.stderr +++ b/lang/tests/compile_fail/realloc_bad_signer.stderr @@ -14,8 +14,11 @@ error[E0277]: the trait bound `quasar_lang::accounts::Signer: SupportsRealloc` i note: required by a bound in `quasar_lang::ops::realloc::Op::<'a>::apply` --> src/ops/realloc.rs | - | pub fn apply( - | ^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` + | pub fn apply(&self, field: &mut F, ctx: &OpCtx<'_, R>) -> Result<(), ProgramError> + | ----- required by a bound in this associated function + | where + | F: AccountLoad + SupportsRealloc + crate::traits::Space, + | ^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` error[E0277]: the trait bound `quasar_lang::accounts::Signer: quasar_lang::prelude::Space` is not satisfied --> tests/compile_fail/realloc_bad_signer.rs:9:18 @@ -33,5 +36,8 @@ error[E0277]: the trait bound `quasar_lang::accounts::Signer: quasar_lang::prelu note: required by a bound in `quasar_lang::ops::realloc::Op::<'a>::apply` --> src/ops/realloc.rs | - | pub fn apply( - | ^^^^^^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` + | pub fn apply(&self, field: &mut F, ctx: &OpCtx<'_, R>) -> Result<(), ProgramError> + | ----- required by a bound in this associated function + | where + | F: AccountLoad + SupportsRealloc + crate::traits::Space, + | ^^^^^^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` diff --git a/lang/tests/compile_fail/realloc_bad_unchecked.stderr b/lang/tests/compile_fail/realloc_bad_unchecked.stderr index fdb83c45..f62557e4 100644 --- a/lang/tests/compile_fail/realloc_bad_unchecked.stderr +++ b/lang/tests/compile_fail/realloc_bad_unchecked.stderr @@ -14,8 +14,11 @@ error[E0277]: the trait bound `quasar_lang::accounts::UncheckedAccount: Supports note: required by a bound in `quasar_lang::ops::realloc::Op::<'a>::apply` --> src/ops/realloc.rs | - | pub fn apply( - | ^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` + | pub fn apply(&self, field: &mut F, ctx: &OpCtx<'_, R>) -> Result<(), ProgramError> + | ----- required by a bound in this associated function + | where + | F: AccountLoad + SupportsRealloc + crate::traits::Space, + | ^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` error[E0277]: the trait bound `quasar_lang::accounts::UncheckedAccount: quasar_lang::prelude::Space` is not satisfied --> tests/compile_fail/realloc_bad_unchecked.rs:9:18 @@ -33,5 +36,8 @@ error[E0277]: the trait bound `quasar_lang::accounts::UncheckedAccount: quasar_l note: required by a bound in `quasar_lang::ops::realloc::Op::<'a>::apply` --> src/ops/realloc.rs | - | pub fn apply( - | ^^^^^^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` + | pub fn apply(&self, field: &mut F, ctx: &OpCtx<'_, R>) -> Result<(), ProgramError> + | ----- required by a bound in this associated function + | where + | F: AccountLoad + SupportsRealloc + crate::traits::Space, + | ^^^^^^^^^^^^^^^^^^^^ required by this bound in `Op::<'a>::apply` diff --git a/metadata/src/account_init.rs b/metadata/src/account_init.rs index 769394c7..7c9459a4 100644 --- a/metadata/src/account_init.rs +++ b/metadata/src/account_init.rs @@ -43,8 +43,8 @@ impl quasar_lang::account_init::AccountInit for MetadataAccount { const DEFAULT_INIT_PARAMS_VALID: bool = false; #[inline(always)] - fn init<'a>( - ctx: quasar_lang::account_init::InitCtx<'a>, + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> quasar_lang::__solana_program_error::ProgramResult { match params { @@ -114,8 +114,8 @@ impl quasar_lang::account_init::AccountInit for MasterEditionAccount { const DEFAULT_INIT_PARAMS_VALID: bool = false; #[inline(always)] - fn init<'a>( - ctx: quasar_lang::account_init::InitCtx<'a>, + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> quasar_lang::__solana_program_error::ProgramResult { match params { diff --git a/spl/src/lib.rs b/spl/src/lib.rs index 18b6d898..cd6a6ecc 100644 --- a/spl/src/lib.rs +++ b/spl/src/lib.rs @@ -90,8 +90,8 @@ macro_rules! impl_token_account_init { const DEFAULT_INIT_PARAMS_VALID: bool = false; #[inline(always)] - fn init<'a>( - ctx: quasar_lang::account_init::InitCtx<'a>, + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> Result<(), ProgramError> { match params { @@ -107,7 +107,7 @@ macro_rules! impl_token_account_init { mint, authority, ctx.signers, - ctx.rent, + ctx.rent.get()?, ), crate::token::TokenInitKind::AssociatedToken { mint, @@ -146,8 +146,8 @@ macro_rules! impl_mint_account_init { const DEFAULT_INIT_PARAMS_VALID: bool = false; #[inline(always)] - fn init<'a>( - ctx: quasar_lang::account_init::InitCtx<'a>, + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, params: &Self::InitParams<'a>, ) -> Result<(), ProgramError> { match params { @@ -165,7 +165,7 @@ macro_rules! impl_mint_account_init { authority, *freeze_authority, ctx.signers, - ctx.rent, + ctx.rent.get()?, ), } } From ba9239ec185340404e3be71349002aa367587f5e Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 14:06:18 -0400 Subject: [PATCH 6/9] Remove custom account macro mode --- derive/src/account/fixed.rs | 213 +++++++----------- derive/src/account/mod.rs | 49 ---- derive/src/helpers.rs | 24 +- .../compile_fail/account_custom_removed.rs | 8 + .../account_custom_removed.stderr | 5 + lang/tests/compile_pass/account_custom.rs | 71 ------ 6 files changed, 101 insertions(+), 269 deletions(-) create mode 100644 lang/tests/compile_fail/account_custom_removed.rs create mode 100644 lang/tests/compile_fail/account_custom_removed.stderr delete mode 100644 lang/tests/compile_pass/account_custom.rs diff --git a/derive/src/account/fixed.rs b/derive/src/account/fixed.rs index 1440d303..34e62eed 100644 --- a/derive/src/account/fixed.rs +++ b/derive/src/account/fixed.rs @@ -17,7 +17,6 @@ pub(super) fn generate_account( field_infos: &[PodFieldInfo<'_>], input: &DeriveInput, gen_set_inner: bool, - custom: bool, ) -> TokenStream { let vis = &input.vis; let attrs = &input.attrs; @@ -30,105 +29,64 @@ pub(super) fn generate_account( let zc_definition = super::layout::emit_zc_definition(name, has_dynamic, &zc); let account_wrapper = super::layout::emit_account_wrapper(attrs, vis, name, disc_len, &zc.zc_path); - // Custom accounts skip Owner, Discriminator, and generated AccountLoad checks — - // the user's check() method replaces all framework validation. Instead we - // generate a direct AccountLoad impl that delegates to Self::check(). - let (discriminator_impl, owner_impl, space_impl, account_check_impl, custom_account_load) = - if custom { - let space = super::traits::emit_space_impl( + let (discriminator_impl, owner_impl, space_impl, account_check_impl) = if has_dynamic { + // Dynamic/compact accounts: inline validation into AccountLoad::check. + let disc = super::traits::emit_discriminator_impl(name, disc_bytes, &bump_offset_impl); + let owner = super::traits::emit_owner_impl(name); + let space = + super::traits::emit_space_impl(name, field_infos, has_dynamic, disc_len, &zc.zc_mod); + let account_load = + super::traits::emit_dynamic_account_load(super::traits::AccountLoadSpec { name, - field_infos, has_dynamic, disc_len, - &zc.zc_mod, - ); - let account_load = quote::quote! { - impl quasar_lang::account_load::AccountLoad for #name { - - #[inline(always)] - fn check(view: &quasar_lang::__internal::AccountView) -> Result<(), quasar_lang::prelude::ProgramError> { - #name::check(view) - } - } - - }; - // Custom accounts do NOT get generated AccountInit/AccountExit — - // the user provides manual trait impls if needed. - ( - quote::quote! {}, - quote::quote! {}, - space, - quote::quote! {}, - account_load, - ) - } else if has_dynamic { - // Dynamic/compact accounts: inline validation into AccountLoad::check. - let disc = super::traits::emit_discriminator_impl(name, disc_bytes, &bump_offset_impl); - let owner = super::traits::emit_owner_impl(name); - let space = super::traits::emit_space_impl( - name, - field_infos, - has_dynamic, - disc_len, - &zc.zc_mod, - ); - let account_load = - super::traits::emit_dynamic_account_load(super::traits::AccountLoadSpec { - name, - has_dynamic, - disc_len, - disc_indices, - disc_bytes, - zc_path: &zc.zc_path, - zc_mod: &zc.zc_mod, - }); - (disc, owner, space, quote::quote! {}, account_load) - } else { - // Fixed accounts: emit AccountLayout + composed checks. - // AccountLoad::check is the single source of truth, composing - // Discriminator + DataLen + ZeroPod. - let disc = super::traits::emit_discriminator_impl(name, disc_bytes, &bump_offset_impl); - let owner = super::traits::emit_owner_impl(name); - let space = super::traits::emit_space_impl( - name, - field_infos, - has_dynamic, - disc_len, - &zc.zc_mod, - ); - let disc_len_lit = disc_len; - let zc_mod_ident = &zc.zc_mod; - let account_load = quote::quote! { - impl quasar_lang::account_layout::AccountLayout for #name { - type Schema = #zc_mod_ident::__Schema; - type Target = <#zc_mod_ident::__Schema as quasar_lang::__zeropod::ZeroPodFixed>::Zc; - const DATA_OFFSET: usize = #disc_len_lit; - } + disc_indices, + disc_bytes, + zc_path: &zc.zc_path, + zc_mod: &zc.zc_mod, + }); + (disc, owner, space, account_load) + } else { + // Fixed accounts: emit AccountLayout + composed checks. + // AccountLoad::check is the single source of truth, composing + // Discriminator + DataLen + ZeroPod. + let disc = super::traits::emit_discriminator_impl(name, disc_bytes, &bump_offset_impl); + let owner = super::traits::emit_owner_impl(name); + let space = + super::traits::emit_space_impl(name, field_infos, has_dynamic, disc_len, &zc.zc_mod); + let disc_len_lit = disc_len; + let zc_mod_ident = &zc.zc_mod; + let account_load = quote::quote! { + impl quasar_lang::account_layout::AccountLayout for #name { + type Schema = #zc_mod_ident::__Schema; + type Target = <#zc_mod_ident::__Schema as quasar_lang::__zeropod::ZeroPodFixed>::Zc; + const DATA_OFFSET: usize = #disc_len_lit; + } - impl quasar_lang::checks::Discriminator for #name {} - impl quasar_lang::checks::DataLen for #name {} - impl quasar_lang::checks::ZeroPod for #name {} + impl quasar_lang::checks::Discriminator for #name {} + impl quasar_lang::checks::DataLen for #name {} + impl quasar_lang::checks::ZeroPod for #name {} - impl quasar_lang::account_load::AccountLoad for #name { - #[inline(always)] - fn check(view: &quasar_lang::__internal::AccountView) -> Result<(), quasar_lang::__solana_program_error::ProgramError> { - <#name as quasar_lang::checks::Discriminator>::check(view)?; - <#name as quasar_lang::checks::ZeroPod>::check(view)?; - Ok(()) - } + impl quasar_lang::account_load::AccountLoad for #name { + #[inline(always)] + fn check(view: &quasar_lang::__internal::AccountView) -> Result<(), quasar_lang::__solana_program_error::ProgramError> { + <#name as quasar_lang::checks::Discriminator>::check(view)?; + <#name as quasar_lang::checks::ZeroPod>::check(view)?; + Ok(()) + } - #[inline(always)] - fn check_checked(view: &quasar_lang::__internal::AccountView) -> Result<(), quasar_lang::__solana_program_error::ProgramError> { - <#name as quasar_lang::checks::Discriminator>::check_checked(view)?; - <#name as quasar_lang::checks::ZeroPod>::check_checked(view)?; - Ok(()) - } + #[inline(always)] + fn check_checked(view: &quasar_lang::__internal::AccountView) -> Result<(), quasar_lang::__solana_program_error::ProgramError> { + <#name as quasar_lang::checks::Discriminator>::check_checked(view)?; + <#name as quasar_lang::checks::ZeroPod>::check_checked(view)?; + Ok(()) } + } - }; - // Composed checks are the single generated validation path. - (disc, owner, space, quote::quote! {}, account_load) }; + // Composed checks are the single generated validation path. + (disc, owner, space, account_load) + }; let dynamic_impl_block = super::dynamic::emit_dynamic_impl_block(name, has_dynamic, disc_len, &zc.zc_mod, &dynamic); let compact_mut = super::dynamic::emit_compact_mut( @@ -158,49 +116,42 @@ pub(super) fn generate_account( gen_set_inner, }); - // Generate AccountInit + AccountExit for non-custom accounts. - // Custom accounts and one_of enums skip these — the user provides - // manual impls if needed. - let lifecycle_impls = if custom { - quote::quote! {} - } else { - quote::quote! { - impl quasar_lang::account_init::AccountInit for #name { - type InitParams<'a> = (); - - #[inline(always)] - fn init<'a, R: quasar_lang::ops::RentAccess>( - ctx: quasar_lang::account_init::InitCtx<'a, R>, - _params: &(), - ) -> Result<(), quasar_lang::prelude::ProgramError> { - quasar_lang::account_init::init_account( - ctx.payer, - ctx.target, - ctx.space, - ctx.program_id, - ctx.signers, - ctx.rent.get()?, - ::DISCRIMINATOR, - ) - } + let lifecycle_impls = quote::quote! { + impl quasar_lang::account_init::AccountInit for #name { + type InitParams<'a> = (); + + #[inline(always)] + fn init<'a, R: quasar_lang::ops::RentAccess>( + ctx: quasar_lang::account_init::InitCtx<'a, R>, + _params: &(), + ) -> Result<(), quasar_lang::prelude::ProgramError> { + quasar_lang::account_init::init_account( + ctx.payer, + ctx.target, + ctx.space, + ctx.program_id, + ctx.signers, + ctx.rent.get()?, + ::DISCRIMINATOR, + ) } + } - impl quasar_lang::ops::close::AccountClose for #name { - #[inline(always)] - fn close( - view: &mut quasar_lang::__internal::AccountView, - dest: &quasar_lang::__internal::AccountView, - ) -> Result<(), quasar_lang::prelude::ProgramError> { - quasar_lang::ops::close::close_account( - view, - dest, - ::DISCRIMINATOR.len(), - ) - } + impl quasar_lang::ops::close::AccountClose for #name { + #[inline(always)] + fn close( + view: &mut quasar_lang::__internal::AccountView, + dest: &quasar_lang::__internal::AccountView, + ) -> Result<(), quasar_lang::prelude::ProgramError> { + quasar_lang::ops::close::close_account( + view, + dest, + ::DISCRIMINATOR.len(), + ) } - - impl quasar_lang::ops::SupportsRealloc for #name {} } + + impl quasar_lang::ops::SupportsRealloc for #name {} }; // IDL fragment emission (feature-gated) @@ -323,8 +274,6 @@ pub(super) fn generate_account( #account_check_impl - #custom_account_load - #lifecycle_impls #dynamic_impl_block diff --git a/derive/src/account/mod.rs b/derive/src/account/mod.rs index e87a24ed..761ffa57 100644 --- a/derive/src/account/mod.rs +++ b/derive/src/account/mod.rs @@ -38,16 +38,6 @@ pub(crate) fn account(attr: TokenStream, item: TokenStream) -> TokenStream { let name = &input.ident; - // --- custom on unit struct: transparent wrapper with user-provided check() --- - if args.custom { - if let Data::Struct(data) = &input.data { - if matches!(data.fields, Fields::Unit) { - return generate_custom_account(name).into(); - } - } - // custom with fields: fall through to normal codegen with disc_len = 0 - } - // --- one_of: polymorphic account on enum --- if args.one_of { match &input.data { @@ -166,48 +156,9 @@ pub(crate) fn account(attr: TokenStream, item: TokenStream) -> TokenStream { &pod_field_infos, &input, gen_set_inner, - args.custom, ); if let Some(seeds_tokens) = &seeds_impl { output.extend(TokenStream::from(seeds_tokens.clone())); } output } - -/// Generate a custom account type: `#[repr(transparent)]` wrapper over -/// `AccountView` with user-provided `check()`. -/// -/// The user must implement: -/// ```ignore -/// impl MyType { -/// pub fn check(view: &AccountView) -> Result<(), ProgramError> { ... } -/// } -/// ``` -/// -/// For full manual control over the wrapper struct and trait impls, users -/// can skip `#[account(custom)]` and implement `#[repr(transparent)]` + -/// `AsAccountView` + `AccountLoad` directly. -fn generate_custom_account(name: &syn::Ident) -> proc_macro2::TokenStream { - quote::quote! { - #[repr(transparent)] - pub struct #name { - view: quasar_lang::__internal::AccountView, - } - - impl quasar_lang::traits::AsAccountView for #name { - #[inline(always)] - fn to_account_view(&self) -> &quasar_lang::__internal::AccountView { - &self.view - } - } - - impl quasar_lang::account_load::AccountLoad for #name { - - #[inline(always)] - fn check(view: &quasar_lang::__internal::AccountView) -> Result<(), solana_program_error::ProgramError> { - #name::check(view) - } - } - - } -} diff --git a/derive/src/helpers.rs b/derive/src/helpers.rs index 82f82d13..aee4e92b 100644 --- a/derive/src/helpers.rs +++ b/derive/src/helpers.rs @@ -33,9 +33,6 @@ pub(crate) struct AccountAttr { pub unsafe_no_disc: bool, pub set_inner: bool, pub fixed_capacity: bool, - /// `custom` — transparent wrapper over AccountView with user-provided - /// check(). - pub custom: bool, /// `one_of` — polymorphic account on enum declarations. pub one_of: bool, /// `implements(TraitPath)` — trait all variants implement; generates Deref. @@ -48,7 +45,6 @@ impl Parse for AccountAttr { let mut unsafe_no_disc = false; let mut set_inner = false; let mut fixed_capacity = false; - let mut custom = false; let mut one_of = false; let mut implements: Option = None; @@ -61,7 +57,11 @@ impl Parse for AccountAttr { } else if ident == "fixed_capacity" { fixed_capacity = true; } else if ident == "custom" { - custom = true; + return Err(syn::Error::new( + ident.span(), + "`#[account(custom)]` has been removed; implement `AsAccountView` and \ + `AccountLoad` directly for fully custom account wrappers", + )); } else if ident == "one_of" { one_of = true; } else if ident == "discriminator" { @@ -74,22 +74,13 @@ impl Parse for AccountAttr { return Err(syn::Error::new( ident.span(), "expected `discriminator`, `unsafe_no_disc`, `set_inner`, `fixed_capacity`, \ - `custom`, `one_of`, or `implements`", + `one_of`, or `implements`", )); } let _ = input.parse::>(); } - // custom accounts don't have discriminators - if custom && (!disc_bytes.is_empty() || unsafe_no_disc || set_inner || fixed_capacity) { - return Err(syn::Error::new( - input.span(), - "`custom` cannot be combined with `discriminator`, `unsafe_no_disc`, `set_inner`, \ - or `fixed_capacity`", - )); - } - - if !custom && !one_of && disc_bytes.is_empty() && !unsafe_no_disc { + if !one_of && disc_bytes.is_empty() && !unsafe_no_disc { return Err(syn::Error::new( input.span(), "expected `discriminator` or `unsafe_no_disc`", @@ -116,7 +107,6 @@ impl Parse for AccountAttr { unsafe_no_disc, set_inner, fixed_capacity, - custom, one_of, implements, }) diff --git a/lang/tests/compile_fail/account_custom_removed.rs b/lang/tests/compile_fail/account_custom_removed.rs new file mode 100644 index 00000000..4b6173d6 --- /dev/null +++ b/lang/tests/compile_fail/account_custom_removed.rs @@ -0,0 +1,8 @@ +#![allow(unexpected_cfgs)] + +use quasar_lang::prelude::*; + +#[account(custom)] +pub struct RemovedCustomAccount; + +fn main() {} diff --git a/lang/tests/compile_fail/account_custom_removed.stderr b/lang/tests/compile_fail/account_custom_removed.stderr new file mode 100644 index 00000000..421d1276 --- /dev/null +++ b/lang/tests/compile_fail/account_custom_removed.stderr @@ -0,0 +1,5 @@ +error: `#[account(custom)]` has been removed; implement `AsAccountView` and `AccountLoad` directly for fully custom account wrappers + --> tests/compile_fail/account_custom_removed.rs:5:11 + | +5 | #[account(custom)] + | ^^^^^^ diff --git a/lang/tests/compile_pass/account_custom.rs b/lang/tests/compile_pass/account_custom.rs deleted file mode 100644 index 421305d9..00000000 --- a/lang/tests/compile_pass/account_custom.rs +++ /dev/null @@ -1,71 +0,0 @@ -#![allow(unexpected_cfgs)] -//! Proves that `#[account(custom)]` works in two modes: -//! -//! 1. **Unit struct** — transparent AccountView wrapper, user provides check(). -//! Use case: accept any account, verify later (ResolvedSigner). -//! -//! 2. **Struct with fields** — full zero-copy typed access, user provides check() -//! instead of framework owner/discriminator checks. -//! Use case: typed data with custom validation logic. -//! -//! For full manual control, users can implement `#[repr(transparent)]` + -//! `AsAccountView` + `AccountLoad` directly without `#[account(custom)]`. - -use quasar_lang::prelude::*; - -solana_address::declare_id!("11111111111111111111111111111112"); - -// ── Mode 1: unit struct (no data, just a wrapper) ── - -#[account(custom)] -pub struct ResolvedSigner; - -impl ResolvedSigner { - pub fn check(_view: &AccountView) -> Result<(), ProgramError> { - Ok(()) - } -} - -// ── Mode 2: struct with fields (typed zero-copy access) ── - -#[account(custom)] -pub struct OraclePrice { - pub price: u64, - pub confidence: u64, - pub slot: u64, -} - -impl OraclePrice { - pub fn check(view: &AccountView) -> Result<(), ProgramError> { - // Custom validation — check data length, skip owner/disc - let data = unsafe { view.borrow_unchecked() }; - if data.len() < core::mem::size_of::() * 3 { - return Err(ProgramError::AccountDataTooSmall); - } - Ok(()) - } -} - -// ── Use both in derive ── - -#[derive(Accounts)] -pub struct ReadOracle { - pub signer: ResolvedSigner, - pub oracle: OraclePrice, // custom type used directly, not Account -} - -#[program] -pub mod test_custom_account { - use super::*; - - #[instruction(discriminator = 0)] - pub fn read_oracle(ctx: Ctx) -> Result<(), ProgramError> { - let _ = ctx.accounts.signer.to_account_view(); - // Typed access to oracle fields via zero-copy deref: - let _price = ctx.accounts.oracle.price; - let _conf = ctx.accounts.oracle.confidence; - Ok(()) - } -} - -fn main() {} From a05102140097f877137493845b4b91c2b996943d Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 14:06:22 -0400 Subject: [PATCH 7/9] Accept rent sources for dynamic account writes --- derive/src/account/dynamic.rs | 26 ++++++------- derive/src/account/methods.rs | 10 ++--- examples/multisig/src/instructions/create.rs | 3 +- lang/src/ops/mod.rs | 7 ++++ .../account_dynamic_set_inner_rent_access.rs | 37 +++++++++++++++++++ 5 files changed, 62 insertions(+), 21 deletions(-) create mode 100644 lang/tests/compile_pass/account_dynamic_set_inner_rent_access.rs diff --git a/derive/src/account/dynamic.rs b/derive/src/account/dynamic.rs index ed1aea81..fc7c50a7 100644 --- a/derive/src/account/dynamic.rs +++ b/derive/src/account/dynamic.rs @@ -178,15 +178,14 @@ pub(super) fn emit_dyn_writer( .collect(); quote! { - pub struct #writer_name<'a> { + pub struct #writer_name<'a, R: quasar_lang::ops::RentAccess> { __view: &'a mut AccountView, __payer: &'a AccountView, - __rent_lpb: u64, - __rent_threshold: u64, + __rent: &'a R, #(#setter_fields,)* } - impl<'a> core::ops::Deref for #writer_name<'a> { + impl<'a, R: quasar_lang::ops::RentAccess> core::ops::Deref for #writer_name<'a, R> { type Target = #zc_path; #[inline(always)] @@ -195,14 +194,14 @@ pub(super) fn emit_dyn_writer( } } - impl<'a> core::ops::DerefMut for #writer_name<'a> { + impl<'a, R: quasar_lang::ops::RentAccess> core::ops::DerefMut for #writer_name<'a, R> { #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { unsafe { &mut *(self.__view.data_mut_ptr().add(#disc_len) as *mut #zc_path) } } } - impl<'a> #writer_name<'a> { + impl<'a, R: quasar_lang::ops::RentAccess> #writer_name<'a, R> { #(#setter_methods)* pub fn commit(&mut self) -> Result<(), ProgramError> { @@ -213,12 +212,13 @@ pub(super) fn emit_dyn_writer( #(#size_terms)*; let __old_total = self.__view.data_len(); if __new_total != __old_total { + let __rent = self.__rent.get()?; quasar_lang::accounts::account::realloc_account_raw( self.__view, __new_total, self.__payer, - self.__rent_lpb, - self.__rent_threshold, + __rent.lamports_per_byte(), + __rent.exemption_threshold_raw(), )?; } @@ -237,12 +237,11 @@ pub(super) fn emit_dyn_writer( impl #name { #[inline(always)] - pub fn compact_writer<'a>( + pub fn compact_writer<'a, R: quasar_lang::ops::RentAccess>( &'a mut self, payer: &'a AccountView, - rent_lpb: u64, - rent_threshold: u64, - ) -> #writer_name<'a> { + rent: &'a R, + ) -> #writer_name<'a, R> { // SAFETY: `self.__view` is the transparent account backing store for this // wrapper. Reborrowing it as `&mut AccountView` is sound here because the // writer exclusively owns `&'a mut self` for its full lifetime and does not @@ -251,8 +250,7 @@ pub(super) fn emit_dyn_writer( #writer_name { __view, __payer: payer, - __rent_lpb: rent_lpb, - __rent_threshold: rent_threshold, + __rent: rent, #(#setter_inits,)* } } diff --git a/derive/src/account/methods.rs b/derive/src/account/methods.rs index dd0c12f4..915c7ed7 100644 --- a/derive/src/account/methods.rs +++ b/derive/src/account/methods.rs @@ -105,12 +105,11 @@ pub(super) fn emit_set_inner_impl(spec: SetInnerSpec<'_>) -> proc_macro2::TokenS impl #name { #[inline(always)] - pub fn set_inner( + pub fn set_inner( &mut self, inner: #inner_name<'_>, payer: &AccountView, - rent_lpb: u64, - rent_threshold: u64, + rent: &R, ) -> Result<(), ProgramError> { #(let #init_field_names = inner.#init_field_names;)* #(#max_checks)* @@ -119,12 +118,13 @@ pub(super) fn emit_set_inner_impl(spec: SetInnerSpec<'_>) -> proc_macro2::TokenS let __view = unsafe { &mut *(self as *mut Self as *mut AccountView) }; if __space != __view.data_len() { + let __rent = rent.get()?; quasar_lang::accounts::account::realloc_account_raw( __view, __space, payer, - rent_lpb, - rent_threshold, + __rent.lamports_per_byte(), + __rent.exemption_threshold_raw(), )?; } diff --git a/examples/multisig/src/instructions/create.rs b/examples/multisig/src/instructions/create.rs index 2fc16984..54e62259 100644 --- a/examples/multisig/src/instructions/create.rs +++ b/examples/multisig/src/instructions/create.rs @@ -55,8 +55,7 @@ impl Create { signers, }, self.creator.to_account_view(), - self.rent.lamports_per_byte(), - self.rent.exemption_threshold_raw(), + &self.rent, ) } } diff --git a/lang/src/ops/mod.rs b/lang/src/ops/mod.rs index 01de9b56..8c765d93 100644 --- a/lang/src/ops/mod.rs +++ b/lang/src/ops/mod.rs @@ -35,6 +35,13 @@ impl RentAccess for &crate::sysvars::rent::Rent { } } +impl RentAccess for crate::accounts::Sysvar { + #[inline(always)] + fn get(&self) -> Result<&crate::sysvars::rent::Rent, solana_program_error::ProgramError> { + Ok(self.get()) + } +} + /// Lazily resolves Rent for lifecycle operations. /// /// Used only when no `Sysvar` account is present. The syscall is diff --git a/lang/tests/compile_pass/account_dynamic_set_inner_rent_access.rs b/lang/tests/compile_pass/account_dynamic_set_inner_rent_access.rs new file mode 100644 index 00000000..24b45514 --- /dev/null +++ b/lang/tests/compile_pass/account_dynamic_set_inner_rent_access.rs @@ -0,0 +1,37 @@ +#![allow(unexpected_cfgs)] + +use quasar_lang::prelude::*; + +solana_address::declare_id!("11111111111111111111111111111112"); + +#[account(discriminator = 1, set_inner)] +pub struct Profile { + pub bump: u8, + pub name: String<32>, + pub scores: Vec, +} + +#[derive(Accounts)] +pub struct InitProfile { + #[account(mut)] + pub payer: Signer, + #[account(mut)] + pub profile: Account, + pub rent: Sysvar, +} + +impl InitProfile { + pub fn handler(&mut self) -> Result<(), ProgramError> { + self.profile.set_inner( + ProfileInner { + bump: 1, + name: "leo", + scores: &[1, 2, 3], + }, + self.payer.to_account_view(), + &self.rent, + ) + } +} + +fn main() {} From 69b42c19799815a911a8bb26a212814d56d05406 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 16:11:17 -0400 Subject: [PATCH 8/9] Refine account groups and typed remaining parsing Make account-group composition explicit and let typed remaining tails parse account wrappers or derived account groups through the same internal contract. --- derive/src/accounts/emit/output.rs | 29 +- derive/src/accounts/mod.rs | 53 ++- derive/src/accounts/resolve/lower.rs | 15 +- derive/src/accounts/syntax/attrs.rs | 4 + derive/src/helpers.rs | 3 - derive/tests/compile_pass/account_signers.rs | 23 +- lang/src/accounts/array.rs | 7 +- lang/src/accounts/signer.rs | 23 ++ lang/src/accounts/system_account.rs | 22 ++ lang/src/accounts/unchecked.rs | 22 ++ lang/src/context.rs | 8 +- lang/src/macros.rs | 23 ++ lang/src/prelude.rs | 4 +- lang/src/remaining.rs | 393 +++++++++++++++++-- lang/src/traits.rs | 5 + lang/tests/compile_pass/remaining_typed.rs | 13 + lang/tests/miri.rs | 95 +++++ 17 files changed, 674 insertions(+), 68 deletions(-) diff --git a/derive/src/accounts/emit/output.rs b/derive/src/accounts/emit/output.rs index 4786282d..ada8a828 100644 --- a/derive/src/accounts/emit/output.rs +++ b/derive/src/accounts/emit/output.rs @@ -18,7 +18,7 @@ pub(crate) struct AccountsOutput<'a> { pub parse_body: proc_macro2::TokenStream, pub direct_parse_body: proc_macro2::TokenStream, pub bumps_struct: proc_macro2::TokenStream, - pub account_seeds_impl: proc_macro2::TokenStream, + pub signer_helpers_impl: proc_macro2::TokenStream, pub epilogue_method: proc_macro2::TokenStream, pub has_epilogue_expr: proc_macro2::TokenStream, pub client_macro: proc_macro2::TokenStream, @@ -41,7 +41,7 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T parse_body, direct_parse_body, bumps_struct, - account_seeds_impl, + signer_helpers_impl, epilogue_method, has_epilogue_expr, client_macro, @@ -126,7 +126,7 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T quote! { #bumps_struct - #account_seeds_impl + #signer_helpers_impl #parse_accounts_impl @@ -187,6 +187,29 @@ pub(crate) fn emit_accounts_output(output: AccountsOutput<'_>) -> proc_macro2::T } } + impl #parse_impl_generics quasar_lang::remaining::RemainingItem<'input> + for #name #ty_generics + #parse_where_clause + { + const COUNT: usize = ::COUNT; + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [quasar_lang::__internal::AccountView], + program_id: Option<&quasar_lang::prelude::Address>, + data: &[u8], + ) -> Result { + let program_id = program_id.ok_or(ProgramError::InvalidInstructionData)?; + let (item, _bumps) = + ::parse_with_instruction_data_unchecked( + accounts, + data, + program_id, + )?; + Ok(item) + } + } + #client_macro } } diff --git a/derive/src/accounts/mod.rs b/derive/src/accounts/mod.rs index 15d0375c..93edd2dc 100644 --- a/derive/src/accounts/mod.rs +++ b/derive/src/accounts/mod.rs @@ -142,16 +142,16 @@ pub(crate) fn derive_accounts(input: TokenStream) -> TokenStream { }; let bumps_struct = emit::emit_bump_struct_def(&semantics, &emit_cx); - let account_seeds_impl = emit_account_seeds_impl( + let signer_helpers_impl = emit_signer_helpers_impl(SignerHelpersCtx { name, - &bumps_name, - &semantics, - &impl_generics_ts, - &ty_generics_ts, - &where_clause_ts, - &ix_arg_extraction, - instruction_args.is_some(), - ); + bumps_name: &bumps_name, + semantics: &semantics, + impl_generics: &impl_generics_ts, + ty_generics: &ty_generics_ts, + where_clause: &where_clause_ts, + ix_arg_extraction: &ix_arg_extraction, + has_instruction_args: instruction_args.is_some(), + }); let epilogue_method = match emit::emit_epilogue(&semantics, &typed_plan) { Ok(ts) => ts, Err(e) => return e.to_compile_error().into(), @@ -178,7 +178,7 @@ pub(crate) fn derive_accounts(input: TokenStream) -> TokenStream { parse_body, direct_parse_body, bumps_struct, - account_seeds_impl, + signer_helpers_impl, epilogue_method, has_epilogue_expr, client_macro, @@ -441,16 +441,29 @@ fn emit_needs_event_cpi_expr(semantics: &[resolve::FieldSemantics]) -> proc_macr quote! { false #(|| #terms)* } } -fn emit_account_seeds_impl( - name: &syn::Ident, - bumps_name: &syn::Ident, - semantics: &[resolve::FieldSemantics], - impl_generics: &proc_macro2::TokenStream, - ty_generics: &proc_macro2::TokenStream, - where_clause: &proc_macro2::TokenStream, - ix_arg_extraction: &proc_macro2::TokenStream, +struct SignerHelpersCtx<'a> { + name: &'a syn::Ident, + bumps_name: &'a syn::Ident, + semantics: &'a [resolve::FieldSemantics], + impl_generics: &'a proc_macro2::TokenStream, + ty_generics: &'a proc_macro2::TokenStream, + where_clause: &'a proc_macro2::TokenStream, + ix_arg_extraction: &'a proc_macro2::TokenStream, has_instruction_args: bool, -) -> proc_macro2::TokenStream { +} + +fn emit_signer_helpers_impl(ctx: SignerHelpersCtx<'_>) -> proc_macro2::TokenStream { + let SignerHelpersCtx { + name, + bumps_name, + semantics, + impl_generics, + ty_generics, + where_clause, + ix_arg_extraction, + has_instruction_args, + } = ctx; + let field_refs: Vec = semantics .iter() .map(|sem| { @@ -511,6 +524,8 @@ fn emit_account_seeds_impl( impl #impl_generics quasar_lang::traits::AccountBumps for #name #ty_generics #where_clause { type Bumps = #bumps_name; } + + impl #impl_generics quasar_lang::traits::AccountGroup for #name #ty_generics #where_clause {} } } diff --git a/derive/src/accounts/resolve/lower.rs b/derive/src/accounts/resolve/lower.rs index 9ebf57d8..e98a7298 100644 --- a/derive/src/accounts/resolve/lower.rs +++ b/derive/src/accounts/resolve/lower.rs @@ -71,7 +71,12 @@ fn lower_core(field: &syn::Field, directives: &[Directive]) -> FieldCore { other => other.clone(), }; - let kind = classify_kind(ty); + let kind = classify_kind( + ty, + directives + .iter() + .any(|d| matches!(d, Directive::Core(CoreDirective::Group))), + ); let inner_ty = extract_inner_ty(&effective_ty); let dynamic = detect_dynamic(&effective_ty, inner_ty.as_ref()); @@ -96,8 +101,8 @@ fn lower_core(field: &syn::Field, directives: &[Directive]) -> FieldCore { } } -fn classify_kind(raw_ty: &Type) -> FieldKind { - if is_composite_type(raw_ty) { +fn classify_kind(raw_ty: &Type, explicit_group: bool) -> FieldKind { + if explicit_group || is_composite_type(raw_ty) { FieldKind::Composite } else { FieldKind::Single @@ -110,7 +115,9 @@ fn lower_directives(sem: &mut FieldSemantics, directives: Vec) -> syn for directive in directives { match directive { Directive::Core(core) => match core { - CoreDirective::Mut | CoreDirective::Dup => { /* handled in lower_core */ } + CoreDirective::Mut | CoreDirective::Dup | CoreDirective::Group => { + /* handled in lower_core */ + } CoreDirective::Init { idempotent } => { sem.init = Some(InitDirective { idempotent }); sem.core.is_mut = true; diff --git a/derive/src/accounts/syntax/attrs.rs b/derive/src/accounts/syntax/attrs.rs index 957fca77..d15f2c63 100644 --- a/derive/src/accounts/syntax/attrs.rs +++ b/derive/src/accounts/syntax/attrs.rs @@ -34,6 +34,7 @@ pub(crate) enum Directive { pub(crate) enum CoreDirective { Mut, Dup, + Group, Init { idempotent: bool }, Payer(Ident), Address(syn::Expr, Option), @@ -180,6 +181,9 @@ impl Parse for ParsedDirective { "dup" => Ok(ParsedDirective { inner: Directive::Core(CoreDirective::Dup), }), + "group" => Ok(ParsedDirective { + inner: Directive::Core(CoreDirective::Group), + }), _ => Err(syn::Error::new_spanned( &path, format!("unknown bare directive `{name}`; did you mean `{name}(...)`?"), diff --git a/derive/src/helpers.rs b/derive/src/helpers.rs index aee4e92b..168b6b4e 100644 --- a/derive/src/helpers.rs +++ b/derive/src/helpers.rs @@ -225,9 +225,6 @@ pub(crate) fn is_composite_type(ty: &Type) -> bool { if last.ident == "AccountsArray" { return true; } - if last.ident.to_string().ends_with("Accounts") { - return true; - } if let PathArguments::AngleBracketed(args) = &last.arguments { return args .args diff --git a/derive/tests/compile_pass/account_signers.rs b/derive/tests/compile_pass/account_signers.rs index 0b8a38bf..f0c83028 100644 --- a/derive/tests/compile_pass/account_signers.rs +++ b/derive/tests/compile_pass/account_signers.rs @@ -15,26 +15,41 @@ pub struct Vault { } #[derive(Accounts)] -pub struct VaultAccounts { +pub struct VaultBundle { pub authority: Signer, #[account(mut, address = Vault::seeds(authority.address()))] pub vault: Account, } +#[derive(Accounts)] +pub struct VaultAccounts { + #[account(group)] + pub vault_bundle: VaultBundle, +} + #[derive(Accounts)] pub struct UsesNestedVault { - pub vault_accounts: VaultAccounts, + #[account(group)] + pub vault_bundle: VaultBundle, } fn assert_cpi_signer(_seeds: &S) {} fn use_flat(ctx: Ctx) { - assert_cpi_signer(&ctx.accounts.vault_signer(&ctx.bumps)); + let vault_signer = ctx + .accounts + .vault_bundle + .vault_signer(&ctx.bumps.vault_bundle); + assert_cpi_signer(&vault_signer); } fn use_nested(ctx: Ctx) { - assert_cpi_signer(&ctx.accounts.vault_accounts.vault_signer(&ctx.bumps.vault_accounts)); + let vault_signer = ctx + .accounts + .vault_bundle + .vault_signer(&ctx.bumps.vault_bundle); + assert_cpi_signer(&vault_signer); } fn main() {} diff --git a/lang/src/accounts/array.rs b/lang/src/accounts/array.rs index 0aa22a69..b4248dcb 100644 --- a/lang/src/accounts/array.rs +++ b/lang/src/accounts/array.rs @@ -1,7 +1,10 @@ use { crate::{ prelude::*, - traits::{check_account_count, AccountBumps, ParseAccountsRaw, ParseAccountsUnchecked}, + traits::{ + check_account_count, AccountBumps, AccountGroup, ParseAccountsRaw, + ParseAccountsUnchecked, + }, }, core::mem::MaybeUninit, }; @@ -181,3 +184,5 @@ where { type Bumps = [T::Bumps; N]; } + +impl AccountGroup for AccountsArray where T: AccountGroup {} diff --git a/lang/src/accounts/signer.rs b/lang/src/accounts/signer.rs index 145713c1..009c9f67 100644 --- a/lang/src/accounts/signer.rs +++ b/lang/src/accounts/signer.rs @@ -16,3 +16,26 @@ impl crate::account_load::AccountLoad for Signer { Ok(()) } } + +impl<'input> crate::remaining::RemainingItem<'input> for Signer { + const COUNT: usize = 1; + const REJECT_DUPLICATES: bool = false; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_account::(accounts) + } +} diff --git a/lang/src/accounts/system_account.rs b/lang/src/accounts/system_account.rs index 0a01edae..630734c5 100644 --- a/lang/src/accounts/system_account.rs +++ b/lang/src/accounts/system_account.rs @@ -18,3 +18,25 @@ impl crate::account_load::AccountLoad for SystemAccount { ::check(view) } } + +impl<'input> crate::remaining::RemainingItem<'input> for SystemAccount { + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_account::(accounts) + } +} diff --git a/lang/src/accounts/unchecked.rs b/lang/src/accounts/unchecked.rs index 37b75dda..b29a9cd6 100644 --- a/lang/src/accounts/unchecked.rs +++ b/lang/src/accounts/unchecked.rs @@ -16,6 +16,28 @@ impl crate::account_load::AccountLoad for UncheckedAccount { } } +impl<'input> crate::remaining::RemainingItem<'input> for UncheckedAccount { + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + crate::remaining::parse_remaining_account::(accounts) + } +} + /// Bounds-checked data writes for unchecked account slots. /// /// This keeps the common "write these bytes at this offset" path safe without diff --git a/lang/src/context.rs b/lang/src/context.rs index caadadc5..68dc1968 100644 --- a/lang/src/context.rs +++ b/lang/src/context.rs @@ -171,6 +171,12 @@ impl<'input, T: ParseAccounts<'input> + ParseAccountsUnchecked<'input> + Account /// runtime borrows, so duplicate entries are safe by default. #[inline(always)] pub fn remaining_accounts(&self) -> RemainingAccounts<'input> { - RemainingAccounts::new(self.remaining_ptr, self.accounts_boundary, self.declared) + RemainingAccounts::new_with_context( + self.remaining_ptr, + self.accounts_boundary, + self.declared, + unsafe { as_address(self.program_id) }, + self.data, + ) } } diff --git a/lang/src/macros.rs b/lang/src/macros.rs index 664b4c20..0932a8ab 100644 --- a/lang/src/macros.rs +++ b/lang/src/macros.rs @@ -89,6 +89,28 @@ macro_rules! define_account { } } + impl<'__quasar_remaining> $crate::remaining::RemainingItem<'__quasar_remaining> for $name { + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: $crate::__internal::AccountView, + _program_id: Option<&$crate::prelude::Address>, + _data: &[u8], + ) -> Result { + $crate::remaining::parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'__quasar_remaining mut [$crate::__internal::AccountView], + _program_id: Option<&$crate::prelude::Address>, + _data: &[u8], + ) -> Result { + $crate::remaining::parse_remaining_account::(accounts) + } + } + }; // Base form: `pub struct Signer => [checks::Signer]` @@ -126,6 +148,7 @@ macro_rules! define_account { &mut *(view as *mut AccountView as *mut Self) } } + }; } diff --git a/lang/src/prelude.rs b/lang/src/prelude.rs index cb43abe1..5ea84e3e 100644 --- a/lang/src/prelude.rs +++ b/lang/src/prelude.rs @@ -29,8 +29,8 @@ pub use { return_data::set_return_data, sysvars::{clock::Clock, rent::Rent}, traits::{ - AccountCount, AsAccountView, CheckOwner, Discriminator, Event, HasSeeds, Id, Owner, - Owners, ParseAccounts, ProgramInterface, Space, StaticView, ZeroCopyDeref, + AccountCount, AccountGroup, AsAccountView, CheckOwner, Discriminator, Event, HasSeeds, + Id, Owner, Owners, ParseAccounts, ProgramInterface, Space, StaticView, ZeroCopyDeref, }, String, Vec, ZcElem, ZcField, ZcValidate, ZeroPodError, }, diff --git a/lang/src/remaining.rs b/lang/src/remaining.rs index b9d5dbce..2a4a3752 100644 --- a/lang/src/remaining.rs +++ b/lang/src/remaining.rs @@ -215,6 +215,10 @@ pub struct RemainingAccounts<'a> { boundary: *const u8, /// Previously parsed declared accounts (for dup resolution). declared: &'a [AccountView], + /// Program ID for typed account-group parsing. + program_id: Option<&'a Address>, + /// Instruction data for typed account-group parsing. + data: &'a [u8], } impl<'a> RemainingAccounts<'a> { @@ -226,6 +230,27 @@ impl<'a> RemainingAccounts<'a> { ptr, boundary, declared, + program_id: None, + data: &[], + } + } + + /// Creates a remaining accounts accessor that can parse typed account + /// groups requiring program ID and instruction data. + #[inline(always)] + pub fn new_with_context( + ptr: *mut u8, + boundary: *const u8, + declared: &'a [AccountView], + program_id: &'a Address, + data: &'a [u8], + ) -> Self { + Self { + ptr, + boundary, + declared, + program_id: Some(program_id), + data, } } /// Returns `true` if there are no remaining accounts. @@ -287,9 +312,194 @@ impl<'a> RemainingAccounts<'a> { #[inline(always)] pub fn parse(&self) -> Result, ProgramError> where - T: AccountLoad, + T: RemainingItem<'a>, { - Remaining::parse(Self::new(self.ptr, self.boundary, self.declared)) + Remaining::parse(Self { + ptr: self.ptr, + boundary: self.boundary, + declared: self.declared, + program_id: self.program_id, + data: self.data, + }) + } +} + +#[doc(hidden)] +pub trait RemainingItem<'input>: Sized { + const COUNT: usize; + const REJECT_DUPLICATES: bool = true; + + /// # Safety + /// + /// `account` must be an initialized account view already checked against + /// declared/remaining duplicates. + unsafe fn parse_remaining_one( + account: AccountView, + program_id: Option<&Address>, + data: &[u8], + ) -> Result { + let mut account = core::mem::MaybeUninit::new(account); + let accounts = core::slice::from_raw_parts_mut(account.as_mut_ptr(), 1); + Self::parse_remaining_chunk(accounts, program_id, data) + } + + /// # Safety + /// + /// `accounts` must contain exactly `Self::COUNT` initialized account + /// views, already checked against declared/remaining duplicates. + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + program_id: Option<&Address>, + data: &[u8], + ) -> Result; +} + +#[doc(hidden)] +#[inline(always)] +pub fn parse_remaining_view(view: &AccountView) -> Result { + if T::IS_SIGNER && !view.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + if T::IS_EXECUTABLE && !view.executable() { + return Err(ProgramError::InvalidAccountData); + } + T::load_checked(view) +} + +#[doc(hidden)] +#[inline(always)] +pub fn parse_remaining_account( + accounts: &[AccountView], +) -> Result { + let view = accounts.first().ok_or(ProgramError::NotEnoughAccountKeys)?; + parse_remaining_view::(view) +} + +impl<'input, T> RemainingItem<'input> for crate::accounts::Account +where + T: crate::traits::AsAccountView + + crate::account_load::AccountLoad + + crate::traits::CheckOwner + + crate::traits::StaticView, +{ + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_account::(accounts) + } +} + +impl<'input, T> RemainingItem<'input> for crate::accounts::InterfaceAccount +where + T: crate::traits::Owners + crate::account_load::AccountLoad, +{ + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_account::(accounts) + } +} + +impl<'input, T> RemainingItem<'input> for crate::accounts::Program +where + T: crate::traits::Id, +{ + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_account::(accounts) + } +} + +impl<'input, T> RemainingItem<'input> for crate::accounts::Interface +where + T: crate::traits::ProgramInterface, +{ + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_account::(accounts) + } +} + +impl<'input, T> RemainingItem<'input> for crate::accounts::Sysvar +where + T: crate::sysvars::Sysvar, +{ + const COUNT: usize = 1; + + #[inline(always)] + unsafe fn parse_remaining_one( + account: AccountView, + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_view::(&account) + } + + #[inline(always)] + unsafe fn parse_remaining_chunk( + accounts: &'input mut [AccountView], + _program_id: Option<&Address>, + _data: &[u8], + ) -> Result { + parse_remaining_account::(accounts) } } @@ -353,19 +563,22 @@ pub type RemainingIter<'a> = RemainingIterImpl<'a, false>; /// Bounded typed view over a remaining-account tail. /// -/// `Remaining` accepts any number of remaining accounts up to `N` and -/// validates each one as `T`. Use raw [`RemainingAccounts`] when the account -/// tail is intentionally uncapped or forwarded without local validation. +/// `Remaining` accepts any number of typed remaining items up to `N`. +/// For single account wrappers, one item consumes one raw remaining account. +/// For `#[derive(Accounts)]` groups, one item consumes the group's fixed +/// account count. Use raw [`RemainingAccounts`] when the tail is intentionally +/// uncapped or forwarded without local validation. pub struct Remaining { items: [core::mem::MaybeUninit; N], len: usize, } -impl Remaining -where - T: AccountLoad, -{ - pub fn parse(accounts: RemainingAccounts<'_>) -> Result { +impl Remaining { + #[inline(always)] + pub fn parse<'input>(accounts: RemainingAccounts<'input>) -> Result + where + T: RemainingItem<'input>, + { let mut out = Self { // SAFETY: An uninitialized `[MaybeUninit; N]` is valid. items: unsafe { @@ -374,43 +587,158 @@ where len: 0, }; let mut seen = unsafe { - core::mem::MaybeUninit::<[core::mem::MaybeUninit
; N]>::uninit().assume_init() + core::mem::MaybeUninit::< + [core::mem::MaybeUninit
; MAX_REMAINING_ACCOUNTS], + >::uninit() + .assume_init() + }; + let mut chunk = unsafe { + core::mem::MaybeUninit::< + [core::mem::MaybeUninit; MAX_REMAINING_ACCOUNTS], + >::uninit() + .assume_init() }; let mut seen_len = 0usize; + let mut chunk_len = 0usize; + let chunk_count = T::COUNT; - for account in accounts.iter() { - let account = account?; + if chunk_count == 0 || chunk_count > MAX_REMAINING_ACCOUNTS { + return Err(ProgramError::InvalidAccountData); + } + if chunk_count == 1 { + return Self::parse_single(accounts); + } + + let mut ptr = accounts.ptr; + while (ptr as *const u8) < accounts.boundary { if out.len >= N { return Err(QuasarError::RemainingAccountsOverflow.into()); } + if seen_len >= MAX_REMAINING_ACCOUNTS { + return Err(QuasarError::RemainingAccountsOverflow.into()); + } - let view = unsafe { account.as_account_view_unchecked() }; - if accounts - .declared - .iter() - .any(|declared| crate::keys_eq(declared.address(), view.address())) - { + let raw = ptr as *mut RuntimeAccount; + // SAFETY: `ptr` is within the SVM buffer (checked against boundary). + let borrow = unsafe { (*raw).borrow_state }; + if borrow != NOT_BORROWED { return Err(QuasarError::RemainingAccountDuplicate.into()); } - let mut i = 0usize; - while i < seen_len { - let seen_address = unsafe { seen[i].assume_init_ref() }; - if crate::keys_eq(seen_address, view.address()) { + + // SAFETY: Non-duplicate entry with a valid `RuntimeAccount`. + let view = unsafe { AccountView::new_unchecked(raw) }; + // SAFETY: `raw` is valid; advances past header + data + padding. + ptr = unsafe { advance_past_account(ptr, raw) }; + + if T::REJECT_DUPLICATES { + if accounts + .declared + .iter() + .any(|declared| crate::keys_eq(declared.address(), view.address())) + { return Err(QuasarError::RemainingAccountDuplicate.into()); } - i += 1; + let mut i = 0usize; + while i < seen_len { + let seen_address = unsafe { seen[i].assume_init_ref() }; + if crate::keys_eq(seen_address, view.address()) { + return Err(QuasarError::RemainingAccountDuplicate.into()); + } + i += 1; + } + seen[seen_len].write(*view.address()); + seen_len += 1; + } + + chunk[chunk_len].write(view); + chunk_len += 1; + + if chunk_len == chunk_count { + let chunk_ptr = chunk.as_mut_ptr() as *mut AccountView; + let chunk_slice = + unsafe { core::slice::from_raw_parts_mut(chunk_ptr, chunk_count) }; + let item = unsafe { + T::parse_remaining_chunk(chunk_slice, accounts.program_id, accounts.data)? + }; + out.items[out.len].write(item); + out.len += 1; + chunk_len = 0; + } + } + + if chunk_len != 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + + Ok(out) + } + + #[inline(always)] + fn parse_single<'input>(accounts: RemainingAccounts<'input>) -> Result + where + T: RemainingItem<'input>, + { + let mut out = Self { + // SAFETY: An uninitialized `[MaybeUninit; N]` is valid. + items: unsafe { + core::mem::MaybeUninit::<[core::mem::MaybeUninit; N]>::uninit().assume_init() + }, + len: 0, + }; + let mut seen = unsafe { + core::mem::MaybeUninit::<[core::mem::MaybeUninit
; N]>::uninit().assume_init() + }; + let mut ptr = accounts.ptr; + while (ptr as *const u8) < accounts.boundary { + if out.len >= N { + return Err(QuasarError::RemainingAccountsOverflow.into()); } - if T::IS_SIGNER && !view.is_signer() { - return Err(ProgramError::MissingRequiredSignature); + let raw = ptr as *mut RuntimeAccount; + // SAFETY: `ptr` is within the SVM buffer (checked against boundary). + let borrow = unsafe { (*raw).borrow_state }; + if T::REJECT_DUPLICATES && borrow != NOT_BORROWED { + return Err(QuasarError::RemainingAccountDuplicate.into()); } - if T::IS_EXECUTABLE && !view.executable() { - return Err(ProgramError::InvalidAccountData); + + let view = if borrow == NOT_BORROWED { + // SAFETY: Non-duplicate entry with a valid `RuntimeAccount`. + let view = unsafe { AccountView::new_unchecked(raw) }; + // SAFETY: `raw` is valid; advances past header + data + padding. + ptr = unsafe { advance_past_account(ptr, raw) }; + view + } else { + // SAFETY: Duplicate entry — advances past the u64 index. + ptr = unsafe { advance_past_dup(ptr) }; + resolve_dup_walk( + borrow as usize, + accounts.declared, + accounts.ptr, + accounts.boundary, + )? + }; + + let address = *view.address(); + if T::REJECT_DUPLICATES { + if accounts + .declared + .iter() + .any(|declared| crate::keys_eq(declared.address(), &address)) + { + return Err(QuasarError::RemainingAccountDuplicate.into()); + } + let mut i = 0usize; + while i < out.len { + let seen_address = unsafe { seen[i].assume_init_ref() }; + if crate::keys_eq(seen_address, &address) { + return Err(QuasarError::RemainingAccountDuplicate.into()); + } + i += 1; + } + seen[out.len].write(address); } - let item = T::load_checked(view)?; - seen[seen_len].write(*view.address()); - seen_len += 1; + let item = unsafe { T::parse_remaining_one(view, accounts.program_id, accounts.data)? }; out.items[out.len].write(item); out.len += 1; } @@ -448,6 +776,9 @@ impl Remaining { impl Drop for Remaining { fn drop(&mut self) { + if !core::mem::needs_drop::() { + return; + } let mut i = 0usize; while i < self.len { unsafe { self.items[i].assume_init_drop() }; diff --git a/lang/src/traits.rs b/lang/src/traits.rs index c5953f5b..85c043f3 100644 --- a/lang/src/traits.rs +++ b/lang/src/traits.rs @@ -151,6 +151,11 @@ pub trait AccountBumps { type Bumps: Copy; } +/// Marker for fixed account groups that can be composed or parsed as a typed +/// remaining-account chunk. +#[doc(hidden)] +pub trait AccountGroup: AccountCount + AccountBumps {} + /// Internal exact-length parsing fast path used by dispatch and nested /// composite account parsing. /// diff --git a/lang/tests/compile_pass/remaining_typed.rs b/lang/tests/compile_pass/remaining_typed.rs index a35990d3..e6683b4f 100644 --- a/lang/tests/compile_pass/remaining_typed.rs +++ b/lang/tests/compile_pass/remaining_typed.rs @@ -1,10 +1,21 @@ #![allow(unexpected_cfgs)] +use quasar_derive::Accounts; use quasar_lang::prelude::*; +#[derive(Accounts)] +pub struct SignerPair { + pub first: Signer, + pub second: Signer, +} + fn parse_signers(accounts: RemainingAccounts<'_>) -> Result, ProgramError> { accounts.parse() } +fn parse_pairs(accounts: RemainingAccounts<'_>) -> Result, ProgramError> { + accounts.parse() +} + fn main() { fn _assert_remaining() { let _ = core::mem::size_of::>(); @@ -12,6 +23,8 @@ fn main() { _assert_remaining::(); _assert_remaining::(); + _assert_remaining::(); let _ = parse_signers; + let _ = parse_pairs; } diff --git a/lang/tests/miri.rs b/lang/tests/miri.rs index 3121f10d..9c158f64 100644 --- a/lang/tests/miri.rs +++ b/lang/tests/miri.rs @@ -96,6 +96,16 @@ use { std::mem::{align_of, size_of, MaybeUninit}, }; +mod remaining_group_fixture { + use quasar_lang::prelude::*; + + #[derive(quasar_derive::Accounts)] + pub struct RemainingPair { + pub first: UncheckedAccount, + pub second: UncheckedAccount, + } +} + // =========================================================================== // Sweep constants -- reused across parameterized tests // =========================================================================== @@ -1042,6 +1052,91 @@ fn bounds_remaining_duplicate_checked_borrow_conflicts() { assert!(second.try_borrow_data().is_err()); } +#[test] +fn bounds_typed_remaining_rejects_remaining_duplicate() { + let mut buf = MultiAccountBuffer::new(&[ + MultiAccountEntry::account(0x01, 0), + MultiAccountEntry::duplicate(0), + ]); + let remaining = RemainingAccounts::new(buf.as_mut_ptr(), buf.boundary(), &[]); + + let err = match remaining.parse::() { + Ok(_) => panic!("typed remaining must reject duplicate aliases"), + Err(err) => err, + }; + assert_eq!(err, QuasarError::RemainingAccountDuplicate.into()); +} + +#[test] +fn bounds_typed_remaining_rejects_declared_duplicate() { + let mut declared_buf = AccountBuffer::new(0); + declared_buf.init([0x01; 32], [0xAA; 32], 1_000_000, 0, false, false); + let declared = [unsafe { declared_buf.view() }]; + + let mut buf = MultiAccountBuffer::new(&[MultiAccountEntry::account(0x01, 0)]); + let remaining = RemainingAccounts::new(buf.as_mut_ptr(), buf.boundary(), &declared); + + let err = match remaining.parse::() { + Ok(_) => panic!("typed remaining must reject aliases to declared accounts"), + Err(err) => err, + }; + assert_eq!(err, QuasarError::RemainingAccountDuplicate.into()); +} + +#[test] +fn bounds_typed_remaining_group_parses_variable_chunks() { + let mut buf = MultiAccountBuffer::new(&[ + MultiAccountEntry::account(0x01, 0), + MultiAccountEntry::account(0x02, 0), + MultiAccountEntry::account(0x03, 0), + MultiAccountEntry::account(0x04, 0), + ]); + let program_id = Address::new_from_array([0x55; 32]); + let remaining = RemainingAccounts::new_with_context( + buf.as_mut_ptr(), + buf.boundary(), + &[], + &program_id, + &[], + ); + + let parsed = remaining + .parse::() + .unwrap(); + assert_eq!(parsed.len(), 2); + assert_eq!( + parsed.as_slice()[0].first.address(), + &Address::new_from_array([0x01; 32]) + ); + assert_eq!( + parsed.as_slice()[1].second.address(), + &Address::new_from_array([0x04; 32]) + ); +} + +#[test] +fn bounds_typed_remaining_group_rejects_partial_chunk() { + let mut buf = MultiAccountBuffer::new(&[ + MultiAccountEntry::account(0x01, 0), + MultiAccountEntry::account(0x02, 0), + MultiAccountEntry::account(0x03, 0), + ]); + let program_id = Address::new_from_array([0x55; 32]); + let remaining = RemainingAccounts::new_with_context( + buf.as_mut_ptr(), + buf.boundary(), + &[], + &program_id, + &[], + ); + + let err = match remaining.parse::() { + Ok(_) => panic!("remaining groups must consume complete chunks"), + Err(err) => err, + }; + assert_eq!(err, ProgramError::NotEnoughAccountKeys); +} + #[test] fn bounds_remaining_iterator_overflow_returns_error() { const LIMIT: usize = 64; From 39d25ccebc3d0f51878f8c13051d4342d16b5812 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 11 May 2026 16:11:26 -0400 Subject: [PATCH 9/9] Use typed remaining signers in multisig Parse bounded signer tails at the instruction boundary so the example exercises the new Remaining API instead of raw remaining-account iteration. --- examples/multisig/src/instructions/create.rs | 15 ++++----------- .../multisig/src/instructions/execute_transfer.rs | 13 ++++--------- examples/multisig/src/lib.rs | 7 ++++--- 3 files changed, 12 insertions(+), 23 deletions(-) diff --git a/examples/multisig/src/instructions/create.rs b/examples/multisig/src/instructions/create.rs index 54e62259..7b1c4402 100644 --- a/examples/multisig/src/instructions/create.rs +++ b/examples/multisig/src/instructions/create.rs @@ -1,6 +1,6 @@ use { crate::state::{MultisigConfig, MultisigConfigInner}, - quasar_lang::{prelude::*, remaining::RemainingAccounts}, + quasar_lang::prelude::*, }; #[derive(Accounts)] @@ -19,22 +19,15 @@ impl Create { &mut self, threshold: u8, bumps: &CreateBumps, - remaining: RemainingAccounts, + signers: Remaining, ) -> Result<(), ProgramError> { let mut addrs = core::mem::MaybeUninit::<[Address; 10]>::uninit(); let addrs_ptr = addrs.as_mut_ptr() as *mut Address; let mut count = 0usize; - for account in remaining.iter() { - let account = account?; - if count >= 10 { - return Err(ProgramError::InvalidArgument); - } - if !account.is_signer() { - return Err(ProgramError::MissingRequiredSignature); - } + for signer in signers.iter() { // SAFETY: count < 10, so addrs_ptr.add(count) is within the 10-element array. - unsafe { core::ptr::write(addrs_ptr.add(count), *account.address()) }; + unsafe { core::ptr::write(addrs_ptr.add(count), *signer.address()) }; count = count.wrapping_add(1); } diff --git a/examples/multisig/src/instructions/execute_transfer.rs b/examples/multisig/src/instructions/execute_transfer.rs index f50f1397..50f8440d 100644 --- a/examples/multisig/src/instructions/execute_transfer.rs +++ b/examples/multisig/src/instructions/execute_transfer.rs @@ -1,8 +1,4 @@ -use { - super::deposit::MultisigVaultPda, - crate::state::MultisigConfig, - quasar_lang::{prelude::*, remaining::RemainingAccounts}, -}; +use {super::deposit::MultisigVaultPda, crate::state::MultisigConfig, quasar_lang::prelude::*}; #[derive(Accounts)] pub struct ExecuteTransfer { @@ -25,16 +21,15 @@ impl ExecuteTransfer { &self, amount: u64, bumps: &ExecuteTransferBumps, - remaining: RemainingAccounts<'_>, + signers: Remaining, ) -> Result<(), ProgramError> { let stored_signers = self.config.signers(); let threshold = self.config.threshold as u32; let mut approvals = 0u32; for stored in stored_signers { - for account in remaining.iter() { - let account = account?; - if account.is_signer() && quasar_lang::keys_eq(account.address(), stored) { + for signer in signers.iter() { + if quasar_lang::keys_eq(signer.address(), stored) { approvals = approvals.wrapping_add(1); break; } diff --git a/examples/multisig/src/lib.rs b/examples/multisig/src/lib.rs index e91d4d79..cad121ec 100644 --- a/examples/multisig/src/lib.rs +++ b/examples/multisig/src/lib.rs @@ -17,8 +17,8 @@ mod quasar_multisig { #[instruction(discriminator = 0)] pub fn create(ctx: CtxWithRemaining, threshold: u8) -> Result<(), ProgramError> { - ctx.accounts - .create_multisig(threshold, &ctx.bumps, ctx.remaining_accounts()) + let signers = ctx.remaining_accounts().parse::()?; + ctx.accounts.create_multisig(threshold, &ctx.bumps, signers) } #[instruction(discriminator = 1)] @@ -36,7 +36,8 @@ mod quasar_multisig { ctx: CtxWithRemaining, amount: u64, ) -> Result<(), ProgramError> { + let signers = ctx.remaining_accounts().parse::()?; ctx.accounts - .verify_and_transfer(amount, &ctx.bumps, ctx.remaining_accounts()) + .verify_and_transfer(amount, &ctx.bumps, signers) } }