use syn::{
parse::{Parse, ParseStream},
Error, Expr, Field, Ident, Token, Type,
};
use super::mint::LightMintField;
use crate::light_pdas::light_account_keywords::{
is_boolean_flag_key, is_shorthand_key, is_standalone_keyword, missing_namespace_error,
valid_keys_for_namespace, validate_namespaced_key,
};
pub(super) use crate::light_pdas::seeds::extract_account_inner_type;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum LightAccountType {
#[default]
Pda, Mint, Token, AssociatedToken, }
impl LightAccountType {
pub fn namespace(&self) -> &'static str {
match self {
LightAccountType::Pda => "pda",
LightAccountType::Mint => "mint",
LightAccountType::Token => "token",
LightAccountType::AssociatedToken => "associated_token",
}
}
}
#[derive(Debug)]
pub enum LightAccountField {
Pda(Box<PdaField>),
Mint(Box<LightMintField>),
TokenAccount(Box<TokenAccountField>),
AssociatedToken(Box<AtaField>),
}
#[derive(Debug)]
pub struct PdaField {
pub ident: Ident,
pub address_tree_info: Expr,
pub output_tree: Expr,
pub is_boxed: bool,
pub is_zero_copy: bool,
}
#[derive(Clone, Debug)]
pub struct TokenAccountField {
pub field_ident: Ident,
pub has_init: bool,
pub seeds: Vec<Expr>,
pub mint: Option<Expr>,
pub owner: Option<Expr>,
pub bump: Option<Expr>,
}
#[derive(Clone, Debug)]
pub struct AtaField {
pub field_ident: Ident,
pub has_init: bool,
pub owner: Expr,
pub mint: Expr,
pub idempotent: bool,
}
struct NamespacedKeyValue {
namespace: Ident,
key: Ident,
value: Expr,
}
impl Parse for NamespacedKeyValue {
fn parse(input: ParseStream) -> syn::Result<Self> {
let namespace: Ident = input.parse()?;
input.parse::<Token![::]>()?;
let key: Ident = input.parse()?;
let namespace_str = namespace.to_string();
let key_str = key.to_string();
let value: Expr = if input.peek(Token![=]) {
if is_boolean_flag_key(&namespace_str, &key_str) {
return Err(Error::new_spanned(
&key,
format!(
"`{}::{}` is a boolean flag — write it without a value (e.g., `{}::{}`)",
namespace_str, key_str, namespace_str, key_str
),
));
}
input.parse::<Token![=]>()?;
if (key == "authority" || key == "seeds") && input.peek(syn::token::Bracket) {
let content;
syn::bracketed!(content in input);
let mut elements = Vec::new();
while !content.is_empty() {
let elem: Expr = content.parse()?;
elements.push(elem);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
syn::parse_quote!([#(#elements),*])
} else {
input.parse()?
}
} else {
if is_shorthand_key(&namespace_str, &key_str) {
syn::parse_quote!(#key)
} else if is_boolean_flag_key(&namespace_str, &key_str) {
syn::parse_quote!(true)
} else {
return Err(Error::new_spanned(
&key,
format!(
"`{}::{}` requires a value (e.g., `{}::{} = ...`)",
namespace_str, key_str, namespace_str, key_str
),
));
}
};
Ok(Self {
namespace,
key,
value,
})
}
}
struct LightAccountArgs {
has_init: bool,
has_zero_copy: bool,
account_type: LightAccountType,
key_values: Vec<NamespacedKeyValue>,
}
#[derive(Default)]
struct SeenKeywords {
init: Option<Ident>,
zero_copy: Option<Ident>,
account_type_keyword: Option<Ident>,
}
impl SeenKeywords {
fn set_init(&mut self, ident: &Ident) -> syn::Result<()> {
if self.init.is_some() {
return Err(Error::new_spanned(ident, "Duplicate `init` keyword"));
}
self.init = Some(ident.clone());
Ok(())
}
fn set_zero_copy(&mut self, ident: &Ident) -> syn::Result<()> {
if self.zero_copy.is_some() {
return Err(Error::new_spanned(ident, "Duplicate `zero_copy` keyword"));
}
self.zero_copy = Some(ident.clone());
Ok(())
}
fn set_account_type(&mut self, ident: &Ident) -> syn::Result<()> {
if let Some(ref prev) = self.account_type_keyword {
let prev_name = prev.to_string();
let new_name = ident.to_string();
if prev_name == new_name {
return Err(Error::new_spanned(
ident,
format!("Duplicate `{}` keyword", new_name),
));
} else {
return Err(Error::new_spanned(
ident,
format!(
"Conflicting account type: `{}` was already specified, cannot also use `{}`",
prev_name, new_name
),
));
}
}
self.account_type_keyword = Some(ident.clone());
Ok(())
}
}
impl Parse for LightAccountArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let mut seen = SeenKeywords::default();
let first: Ident = input.parse()?;
if input.peek(Token![::]) {
let account_type = infer_type_from_namespace(&first)?;
if account_type != LightAccountType::Token
&& account_type != LightAccountType::AssociatedToken
{
return Err(Error::new_spanned(
&first,
format!(
"#[light_account({}::...)] requires `init` keyword. \
Use: #[light_account(init, {}::...)]",
first, first
),
));
}
input.parse::<Token![::]>()?;
let key: Ident = input.parse()?;
let namespace_str = first.to_string();
let key_str = key.to_string();
if let Err(err_msg) = validate_namespaced_key(&namespace_str, &key_str) {
return Err(Error::new_spanned(&key, err_msg));
}
let value: Expr = if input.peek(Token![=]) {
if is_boolean_flag_key(&namespace_str, &key_str) {
return Err(Error::new_spanned(
&key,
format!(
"`{}::{}` is a boolean flag — write it without a value (e.g., `{}::{}`)",
namespace_str, key_str, namespace_str, key_str
),
));
}
input.parse::<Token![=]>()?;
if (key_str == "authority" || key_str == "seeds") && input.peek(syn::token::Bracket)
{
let content;
syn::bracketed!(content in input);
let mut elements = Vec::new();
while !content.is_empty() {
let elem: Expr = content.parse()?;
elements.push(elem);
if content.peek(Token![,]) {
content.parse::<Token![,]>()?;
}
}
syn::parse_quote!([#(#elements),*])
} else {
input.parse()?
}
} else {
if is_shorthand_key(&namespace_str, &key_str) {
syn::parse_quote!(#key)
} else if is_boolean_flag_key(&namespace_str, &key_str) {
syn::parse_quote!(true)
} else {
return Err(Error::new_spanned(
&key,
format!(
"`{}::{}` requires a value (e.g., `{}::{} = ...`)",
namespace_str, key_str, namespace_str, key_str
),
));
}
};
let first_kv = NamespacedKeyValue {
namespace: first,
key,
value,
};
let first_key = first_kv.key.to_string();
let mut key_values = vec![first_kv];
let remaining = parse_namespaced_key_values(input, account_type, &[&first_key])?;
key_values.extend(remaining);
return Ok(Self {
has_init: false,
has_zero_copy: false,
account_type,
key_values,
});
}
if first != "init" {
return Err(Error::new_spanned(
&first,
"First argument to #[light_account] must be `init` or a namespaced key like `token::seeds`",
));
}
seen.set_init(&first)?;
let mut account_type = LightAccountType::Pda;
let mut key_values = Vec::new();
let mut has_zero_copy = false;
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
if input.peek(Ident) {
let lookahead = input.fork();
let ident: Ident = lookahead.parse()?;
if ident == "init" {
let consumed: Ident = input.parse()?;
seen.set_init(&consumed)?;
continue;
}
if ident == "zero_copy" {
let consumed: Ident = input.parse()?;
seen.set_zero_copy(&consumed)?;
has_zero_copy = true;
continue;
}
if lookahead.peek(Token![::]) {
let inferred_type = infer_type_from_namespace(&ident)?;
if account_type != LightAccountType::Pda {
seen.set_account_type(&ident)?; } else {
seen.set_account_type(&ident)?;
account_type = inferred_type;
}
let kv: NamespacedKeyValue = input.parse()?;
key_values.push(kv);
let first_key = key_values
.last()
.map(|kv| kv.key.to_string())
.unwrap_or_default();
let remaining =
parse_namespaced_key_values(input, account_type, &[&first_key])?;
key_values.extend(remaining);
break;
}
if ident == "mint" {
let consumed: Ident = input.parse()?;
seen.set_account_type(&consumed)?;
account_type = LightAccountType::Mint;
key_values = parse_namespaced_key_values(input, account_type, &[])?;
break;
}
if ident == "token" || ident == "associated_token" {
return Err(Error::new_spanned(
&ident,
format!(
"Standalone `{}` keyword is not allowed. Use namespaced syntax: `{}::seeds = [...]`",
ident, ident
),
));
}
return Err(Error::new_spanned(
&ident,
format!(
"Unknown keyword `{}`. Use namespaced syntax like `token::seeds` or `mint::signer`",
ident
),
));
}
}
Ok(Self {
has_init: true,
has_zero_copy,
account_type,
key_values,
})
}
}
fn infer_type_from_namespace(namespace: &Ident) -> Result<LightAccountType, syn::Error> {
let ns = namespace.to_string();
match ns.as_str() {
"token" => Ok(LightAccountType::Token),
"associated_token" => Ok(LightAccountType::AssociatedToken),
"mint" => Ok(LightAccountType::Mint),
_ => Err(Error::new_spanned(
namespace,
format!(
"Unknown namespace `{}`. Expected: token, associated_token, or mint",
ns
),
)),
}
}
fn parse_namespaced_key_values(
input: ParseStream,
account_type: LightAccountType,
already_seen: &[&str],
) -> syn::Result<Vec<NamespacedKeyValue>> {
let mut key_values = Vec::new();
let mut seen_keys: std::collections::HashSet<String> =
already_seen.iter().map(|s| s.to_string()).collect();
let expected_namespace = account_type.namespace();
while !input.is_empty() {
input.parse::<Token![,]>()?;
if input.is_empty() {
break;
}
let fork = input.fork();
let maybe_key: Ident = fork.parse()?;
if fork.peek(Token![=]) && !input.peek2(Token![::]) {
if !is_standalone_keyword(&maybe_key.to_string()) {
return Err(Error::new_spanned(
&maybe_key,
missing_namespace_error(&maybe_key.to_string(), expected_namespace),
));
}
}
let kv: NamespacedKeyValue = input.parse()?;
let namespace_str = kv.namespace.to_string();
let key_str = kv.key.to_string();
if namespace_str != expected_namespace {
return Err(Error::new_spanned(
&kv.namespace,
format!(
"Namespace `{}` doesn't match account type `{}`. Use `{}::{}` instead.",
namespace_str, expected_namespace, expected_namespace, key_str
),
));
}
if !seen_keys.insert(key_str.clone()) {
return Err(Error::new_spanned(
&kv.key,
format!(
"Duplicate key `{}::{}` in #[light_account({}, ...)]. Each key can only appear once.",
namespace_str,
key_str,
expected_namespace
),
));
}
if let Err(err_msg) = validate_namespaced_key(&namespace_str, &key_str) {
return Err(Error::new_spanned(&kv.key, err_msg));
}
key_values.push(kv);
}
Ok(key_values)
}
pub(crate) fn parse_light_account_attr(
field: &Field,
field_ident: &Ident,
direct_proof_arg: &Option<Ident>,
) -> Result<Option<LightAccountField>, syn::Error> {
for attr in &field.attrs {
if attr.path().is_ident("light_account") {
let args: LightAccountArgs = attr.parse_args()?;
if !args.has_init
&& (args.account_type == LightAccountType::Pda
|| args.account_type == LightAccountType::Mint)
{
return Err(Error::new_spanned(
attr,
"#[light_account] requires `init` as the first argument for PDA/Mint",
));
}
return match args.account_type {
LightAccountType::Pda => {
Ok(Some(LightAccountField::Pda(Box::new(build_pda_field(
field,
field_ident,
&args.key_values,
direct_proof_arg,
args.has_zero_copy,
)?))))
}
LightAccountType::Mint => Ok(Some(LightAccountField::Mint(Box::new(
build_mint_field(field_ident, &args.key_values, attr)?,
)))),
LightAccountType::Token => Ok(Some(LightAccountField::TokenAccount(Box::new(
build_token_account_field(field_ident, &args.key_values, args.has_init, attr)?,
)))),
LightAccountType::AssociatedToken => {
Ok(Some(LightAccountField::AssociatedToken(Box::new(
build_ata_field(field_ident, &args.key_values, args.has_init, attr)?,
))))
}
};
}
}
Ok(None)
}
fn is_account_loader_type(ty: &Type) -> bool {
if let Type::Path(type_path) = ty {
return type_path
.path
.segments
.iter()
.any(|seg| seg.ident == "AccountLoader");
}
false
}
fn build_pda_field(
field: &Field,
field_ident: &Ident,
key_values: &[NamespacedKeyValue],
direct_proof_arg: &Option<Ident>,
has_zero_copy: bool,
) -> Result<PdaField, syn::Error> {
if !key_values.is_empty() {
let keys: Vec<_> = key_values
.iter()
.map(|kv| format!("{}::{}", kv.namespace, kv.key))
.collect();
return Err(Error::new_spanned(
&key_values[0].key,
format!(
"Unexpected arguments for PDA: {}. \
#[light_account(init)] takes no additional arguments. \
address_tree_info and output_tree are automatically sourced from CreateAccountsProof.",
keys.join(", ")
),
));
}
let (address_tree_info, output_tree) = if let Some(proof_ident) = direct_proof_arg {
(
syn::parse_quote!(#proof_ident.address_tree_info),
syn::parse_quote!(#proof_ident.output_state_tree_index),
)
} else {
(
syn::parse_quote!(params.create_accounts_proof.address_tree_info),
syn::parse_quote!(params.create_accounts_proof.output_state_tree_index),
)
};
let is_account_loader = is_account_loader_type(&field.ty);
if is_account_loader && !has_zero_copy {
return Err(Error::new_spanned(
&field.ty,
"AccountLoader fields require #[light_account(init, zero_copy)]. \
AccountLoader uses zero-copy (Pod) serialization which is incompatible \
with the default Borsh decompression path.",
));
}
if !is_account_loader && has_zero_copy {
return Err(Error::new_spanned(
&field.ty,
"zero_copy can only be used with AccountLoader fields. \
For Account<'info, T> fields, remove the zero_copy keyword.",
));
}
let (is_boxed, _inner_type) =
extract_account_inner_type(&field.ty).map_err(|e| e.into_syn_error(&field.ty))?;
Ok(PdaField {
ident: field_ident.clone(),
address_tree_info,
output_tree,
is_boxed,
is_zero_copy: has_zero_copy,
})
}
fn build_mint_field(
field_ident: &Ident,
key_values: &[NamespacedKeyValue],
attr: &syn::Attribute,
) -> Result<LightMintField, syn::Error> {
let mut mint_signer: Option<Expr> = None;
let mut authority: Option<Expr> = None;
let mut decimals: Option<Expr> = None;
let mut mint_seeds: Option<Expr> = None;
let mut freeze_authority: Option<Ident> = None;
let mut authority_seeds: Option<Expr> = None;
let mut mint_bump: Option<Expr> = None;
let mut authority_bump: Option<Expr> = None;
let mut name: Option<Expr> = None;
let mut symbol: Option<Expr> = None;
let mut uri: Option<Expr> = None;
let mut update_authority: Option<Ident> = None;
let mut additional_metadata: Option<Expr> = None;
for kv in key_values {
match kv.key.to_string().as_str() {
"signer" => mint_signer = Some(kv.value.clone()),
"authority" => authority = Some(kv.value.clone()),
"decimals" => decimals = Some(kv.value.clone()),
"seeds" => mint_seeds = Some(kv.value.clone()),
"bump" => mint_bump = Some(kv.value.clone()),
"freeze_authority" => {
freeze_authority = Some(expr_to_ident(&kv.value, "mint::freeze_authority")?);
}
"authority_seeds" => authority_seeds = Some(kv.value.clone()),
"authority_bump" => authority_bump = Some(kv.value.clone()),
"name" => name = Some(kv.value.clone()),
"symbol" => symbol = Some(kv.value.clone()),
"uri" => uri = Some(kv.value.clone()),
"update_authority" => {
update_authority = Some(expr_to_ident(&kv.value, "mint::update_authority")?);
}
"additional_metadata" => additional_metadata = Some(kv.value.clone()),
other => {
return Err(Error::new_spanned(
&kv.key,
format!(
"Unknown key `mint::{}`. Allowed: {}",
other,
valid_keys_for_namespace("mint").join(", ")
),
));
}
}
}
let mint_signer = mint_signer.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account(init, mint, ...)] requires `mint::signer`",
)
})?;
let authority = authority.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account(init, mint, ...)] requires `mint::authority`",
)
})?;
let decimals = decimals.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account(init, mint, ...)] requires `mint::decimals`",
)
})?;
let mint_seeds = mint_seeds.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account(init, mint, ...)] requires `mint::seeds`",
)
})?;
validate_metadata_fields(
&name,
&symbol,
&uri,
&update_authority,
&additional_metadata,
attr,
)?;
Ok(LightMintField {
field_ident: field_ident.clone(),
mint_signer,
authority,
decimals,
freeze_authority,
mint_seeds,
mint_bump,
authority_seeds,
authority_bump,
name,
symbol,
uri,
update_authority,
additional_metadata,
})
}
fn build_token_account_field(
field_ident: &Ident,
key_values: &[NamespacedKeyValue],
has_init: bool,
attr: &syn::Attribute,
) -> Result<TokenAccountField, syn::Error> {
let mut seeds: Option<Expr> = None;
let mut mint: Option<Expr> = None;
let mut owner: Option<Expr> = None;
let mut bump: Option<Expr> = None;
for kv in key_values {
match kv.key.to_string().as_str() {
"seeds" => seeds = Some(kv.value.clone()),
"mint" => mint = Some(kv.value.clone()),
"owner" => owner = Some(kv.value.clone()),
"bump" => bump = Some(kv.value.clone()),
"owner_seeds" => {
if let Expr::Array(_) = &kv.value {
} else {
return Err(Error::new_spanned(
&kv.value,
"token::owner_seeds must be an array, e.g., [b\"seed\", CONSTANT.as_bytes()]",
));
}
}
other => {
return Err(Error::new_spanned(
&kv.key,
format!(
"Unknown key `token::{}`. Expected: seeds, mint, owner, bump, owner_seeds",
other
),
));
}
}
}
let has_owner_seeds = key_values.iter().any(|kv| kv.key == "owner_seeds");
if has_init {
if seeds.is_none() {
return Err(Error::new_spanned(
attr,
"#[light_account(init, token::...)] requires `token::seeds = [...]` parameter. \
Token accounts must be PDAs and need seeds for CPI signing.",
));
}
if mint.is_none() {
return Err(Error::new_spanned(
attr,
"#[light_account(init, token::...)] requires `token::mint` parameter",
));
}
if owner.is_none() {
return Err(Error::new_spanned(
attr,
"#[light_account(init, token::...)] requires `token::owner` parameter",
));
}
if !has_owner_seeds {
return Err(Error::new_spanned(
attr,
"#[light_account(init, token::...)] requires `token::owner_seeds = [...]` parameter \
for decompression support. The token owner must be a PDA with constant seeds.",
));
}
} else {
if seeds.is_none() {
return Err(Error::new_spanned(
attr,
"#[light_account(token::...)] requires `token::seeds = [...]` parameter \
for seed struct generation.",
));
}
if !has_owner_seeds {
return Err(Error::new_spanned(
attr,
"#[light_account(token::...)] requires `token::owner_seeds = [...]` parameter \
for decompression support. The token owner must be a PDA with constant seeds.",
));
}
if mint.is_some() {
return Err(Error::new_spanned(
attr,
"`token::mint` is not allowed in mark-only mode (#[light_account(token::...)] without init). \
Only `token::seeds` and `token::owner_seeds` are permitted. \
Remove `token::mint` or add `init` keyword.",
));
}
if owner.is_some() {
return Err(Error::new_spanned(
attr,
"`token::owner` is not allowed in mark-only mode (#[light_account(token::...)] without init). \
Only `token::seeds` and `token::owner_seeds` are permitted. \
Remove `token::owner` or add `init` keyword.",
));
}
}
let seeds_vec = if let Some(ref seeds_expr) = seeds {
let extracted = extract_array_elements(seeds_expr)?;
if has_init && extracted.is_empty() {
return Err(Error::new_spanned(
seeds_expr,
"Empty seeds `token::seeds = []` not allowed for token account initialization. \
Token accounts must be PDAs and need at least one seed for CPI signing.",
));
}
extracted
} else {
Vec::new()
};
Ok(TokenAccountField {
field_ident: field_ident.clone(),
has_init,
seeds: seeds_vec,
mint,
owner,
bump,
})
}
fn build_ata_field(
field_ident: &Ident,
key_values: &[NamespacedKeyValue],
has_init: bool,
attr: &syn::Attribute,
) -> Result<AtaField, syn::Error> {
let mut owner: Option<Expr> = None; let mut mint: Option<Expr> = None;
let mut idempotent: bool = false;
for kv in key_values {
match kv.key.to_string().as_str() {
"authority" => owner = Some(kv.value.clone()), "mint" => mint = Some(kv.value.clone()),
"idempotent" => {
idempotent = true;
}
"bump" => {
return Err(Error::new_spanned(
&kv.key,
"`associated_token::bump` is no longer supported. The bump is derived on-chain.",
));
}
other => {
return Err(Error::new_spanned(
&kv.key,
format!(
"Unknown key `associated_token::{}`. Allowed: authority, mint, idempotent",
other
),
));
}
}
}
let owner = owner.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account([init,] associated_token, ...)] requires `associated_token::authority` parameter",
)
})?;
let mint = mint.ok_or_else(|| {
Error::new_spanned(
attr,
"#[light_account([init,] associated_token, ...)] requires `associated_token::mint` parameter",
)
})?;
Ok(AtaField {
field_ident: field_ident.clone(),
has_init,
owner,
mint,
idempotent,
})
}
fn expr_to_ident(expr: &Expr, field_name: &str) -> Result<Ident, syn::Error> {
match expr {
Expr::Path(path) => path.path.get_ident().cloned().ok_or_else(|| {
Error::new_spanned(expr, format!("`{field_name}` must be a simple identifier"))
}),
_ => Err(Error::new_spanned(
expr,
format!("`{field_name}` must be a simple identifier"),
)),
}
}
fn extract_array_elements(expr: &Expr) -> Result<Vec<Expr>, syn::Error> {
match expr {
Expr::Array(arr) => Ok(arr.elems.iter().cloned().collect()),
Expr::Reference(r) => extract_array_elements(&r.expr),
_ => Err(Error::new_spanned(
expr,
"Expected array expression like `[b\"seed\", other.key()]`",
)),
}
}
fn validate_metadata_fields(
name: &Option<Expr>,
symbol: &Option<Expr>,
uri: &Option<Expr>,
update_authority: &Option<Ident>,
additional_metadata: &Option<Expr>,
attr: &syn::Attribute,
) -> Result<(), syn::Error> {
let has_name = name.is_some();
let has_symbol = symbol.is_some();
let has_uri = uri.is_some();
let has_update_authority = update_authority.is_some();
let has_additional_metadata = additional_metadata.is_some();
let core_metadata_count = [has_name, has_symbol, has_uri]
.iter()
.filter(|&&x| x)
.count();
if core_metadata_count > 0 && core_metadata_count < 3 {
return Err(Error::new_spanned(
attr,
"TokenMetadata requires all of `mint::name`, `mint::symbol`, and `mint::uri` to be specified together",
));
}
if (has_update_authority || has_additional_metadata) && core_metadata_count == 0 {
return Err(Error::new_spanned(
attr,
"`mint::update_authority` and `mint::additional_metadata` require `mint::name`, `mint::symbol`, and `mint::uri` to also be specified",
));
}
Ok(())
}
impl From<PdaField> for crate::light_pdas::parsing::ParsedPdaField {
fn from(pda: PdaField) -> Self {
Self {
field_name: pda.ident,
is_boxed: pda.is_boxed,
is_zero_copy: pda.is_zero_copy,
address_tree_info: Some(pda.address_tree_info),
output_tree: Some(pda.output_tree),
}
}
}
#[cfg(test)]
mod tests {
use syn::parse_quote;
use super::*;
#[test]
fn test_parse_light_account_pda_bare() {
let field: syn::Field = parse_quote! {
#[light_account(init)]
pub record: Account<'info, MyRecord>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Pda(pda) => {
assert_eq!(pda.ident.to_string(), "record");
assert!(!pda.is_boxed);
}
_ => panic!("Expected PDA field"),
}
}
#[test]
fn test_parse_pda_tree_keywords_rejected() {
let field: syn::Field = parse_quote! {
#[light_account(init, pda::address_tree_info = custom_tree)]
pub record: Account<'info, MyRecord>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
}
#[test]
fn test_parse_light_account_mint() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"test"]
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert_eq!(mint.field_ident.to_string(), "cmint");
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_light_account_mint_with_metadata() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"test"],
mint::name = params.name.clone(),
mint::symbol = params.symbol.clone(),
mint::uri = params.uri.clone()
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert!(mint.name.is_some());
assert!(mint.symbol.is_some());
assert!(mint.uri.is_some());
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_light_account_missing_init() {
let field: syn::Field = parse_quote! {
#[light_account(mint, mint::decimals = 9)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
}
#[test]
fn test_parse_light_account_mint_missing_required() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint, mint::decimals = 9)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
}
#[test]
fn test_parse_light_account_partial_metadata_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"test"],
mint::name = params.name.clone()
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
}
#[test]
fn test_no_light_account_attr_returns_none() {
let field: syn::Field = parse_quote! {
pub record: Account<'info, MyRecord>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
assert!(result.unwrap().is_none());
}
#[test]
fn test_parse_token_mark_only_creates_field_with_has_init_false() {
let field: syn::Field = parse_quote! {
#[light_account(token::seeds = [b"vault"], token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok(), "Mark-only mode should parse successfully");
let result = result.unwrap();
assert!(
result.is_some(),
"Mark-only mode should return Some(TokenAccountField)"
);
match result.unwrap() {
LightAccountField::TokenAccount(token) => {
assert_eq!(token.field_ident.to_string(), "vault");
assert!(!token.has_init, "Mark-only should have has_init = false");
assert!(!token.seeds.is_empty(), "Should have seeds");
assert!(token.mint.is_none(), "Mark-only should not have mint");
assert!(token.owner.is_none(), "Mark-only should not have owner");
}
_ => panic!("Expected TokenAccount field"),
}
}
#[test]
fn test_parse_token_mark_only_forbids_mint() {
let field: syn::Field = parse_quote! {
#[light_account(token::seeds = [b"vault"], token::owner_seeds = [b"auth"], token::mint = some_mint)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err(), "Mark-only with mint should fail");
let err = result.err().unwrap().to_string();
assert!(
err.contains("not allowed in mark-only mode"),
"Expected error about mint not allowed, got: {}",
err
);
}
#[test]
fn test_parse_token_mark_only_forbids_owner() {
let field: syn::Field = parse_quote! {
#[light_account(token::seeds = [b"vault"], token::owner_seeds = [b"auth"], token::owner = some_owner)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err(), "Mark-only with owner should fail");
let err = result.err().unwrap().to_string();
assert!(
err.contains("not allowed in mark-only mode"),
"Expected error about owner not allowed, got: {}",
err
);
}
#[test]
fn test_parse_token_mark_only_requires_owner_seeds() {
let field: syn::Field = parse_quote! {
#[light_account(token::seeds = [b"vault"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err(), "Mark-only without owner_seeds should fail");
let err = result.err().unwrap().to_string();
assert!(
err.contains("owner_seeds"),
"Expected error about missing owner_seeds, got: {}",
err
);
}
#[test]
fn test_parse_token_init_creates_field() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = token_mint, token::owner = vault_authority, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Token init should parse successfully: {:?}",
result.err()
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::TokenAccount(token) => {
assert_eq!(token.field_ident.to_string(), "vault");
assert!(token.has_init);
assert!(!token.seeds.is_empty());
assert!(token.mint.is_some());
assert!(token.owner.is_some());
}
_ => panic!("Expected TokenAccount field"),
}
}
#[test]
fn test_parse_token_init_requires_owner_seeds() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = token_mint, token::owner = vault_authority)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_err(),
"Token init without owner_seeds should fail"
);
let err = result.err().unwrap().to_string();
assert!(
err.contains("owner_seeds"),
"Expected error about missing owner_seeds, got: {}",
err
);
}
#[test]
fn test_parse_token_init_missing_seeds_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("seeds"));
}
#[test]
fn test_parse_token_init_missing_mint_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::owner = vault_authority, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("mint"),
"Expected error about missing mint, got: {}",
err
);
}
#[test]
fn test_parse_token_init_missing_owner_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = token_mint, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("owner"),
"Expected error about missing owner, got: {}",
err
);
}
#[test]
fn test_parse_associated_token_mark_only_creates_field_with_has_init_false() {
let field: syn::Field = parse_quote! {
#[light_account(associated_token::authority = owner, associated_token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok(), "Mark-only mode should parse successfully");
let result = result.unwrap();
assert!(
result.is_some(),
"Mark-only mode should return Some(AtaField)"
);
match result.unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert_eq!(ata.field_ident.to_string(), "user_ata");
assert!(!ata.has_init, "Mark-only should have has_init = false");
assert!(!ata.idempotent, "idempotent should default to false");
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_associated_token_init_creates_field() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner, associated_token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert_eq!(ata.field_ident.to_string(), "user_ata");
assert!(ata.has_init);
assert!(!ata.idempotent, "idempotent should default to false");
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_associated_token_idempotent_absent_is_false() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner, associated_token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
match result.unwrap().unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert!(!ata.idempotent, "absent idempotent flag should be false");
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_associated_token_idempotent_flag_is_true() {
let field: syn::Field = parse_quote! {
#[light_account(init,
associated_token::authority = owner,
associated_token::mint = mint,
associated_token::idempotent)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"idempotent flag should parse successfully: {:?}",
result.err()
);
match result.unwrap().unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert!(ata.idempotent, "present idempotent flag should be true");
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_associated_token_idempotent_with_value_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init,
associated_token::authority = owner,
associated_token::mint = mint,
associated_token::idempotent = true)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_err(),
"idempotent with explicit value should be rejected"
);
let err = result.err().unwrap().to_string();
assert!(
err.contains("boolean flag"),
"Expected boolean flag error, got: {}",
err
);
}
#[test]
fn test_parse_associated_token_init_missing_authority_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("authority"));
}
#[test]
fn test_parse_associated_token_init_missing_mint_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("mint"));
}
#[test]
fn test_parse_token_unknown_argument_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"], token::unknown = foo)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("unknown"));
}
#[test]
fn test_parse_associated_token_unknown_argument_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner, associated_token::mint = mint, associated_token::unknown = foo)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(err.contains("unknown"));
}
#[test]
fn test_parse_associated_token_shorthand_syntax() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority, associated_token::mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert_eq!(ata.field_ident.to_string(), "user_ata");
assert!(ata.has_init);
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_associated_token_bump_rejected() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority, associated_token::mint, associated_token::bump)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
}
#[test]
fn test_parse_token_duplicate_key_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault1"], token::seeds = [b"vault2"], token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate key"),
"Expected error about duplicate key, got: {}",
err
);
}
#[test]
fn test_parse_associated_token_duplicate_key_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = foo, associated_token::authority = bar, associated_token::mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate key"),
"Expected error about duplicate key, got: {}",
err
);
}
#[test]
fn test_parse_token_init_empty_seeds_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [], token::mint = token_mint, token::owner = vault_authority, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Empty seeds"),
"Expected error about empty seeds, got: {}",
err
);
}
#[test]
fn test_parse_token_mark_only_missing_seeds_fails() {
let field: syn::Field = parse_quote! {
#[light_account(token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err(), "Mark-only without seeds should fail");
let err = result.err().unwrap().to_string();
assert!(
err.contains("seeds"),
"Expected error about missing seeds, got: {}",
err
);
}
#[test]
fn test_parse_pda_with_direct_proof_arg_uses_proof_ident_for_defaults() {
let field: syn::Field = parse_quote! {
#[light_account(init)]
pub record: Account<'info, MyRecord>
};
let field_ident = field.ident.clone().unwrap();
let proof_ident: Ident = parse_quote!(proof);
let direct_proof_arg = Some(proof_ident.clone());
let result = parse_light_account_attr(&field, &field_ident, &direct_proof_arg);
assert!(
result.is_ok(),
"Should parse successfully with direct proof arg"
);
let result = result.unwrap();
assert!(result.is_some(), "Should return Some for init PDA");
match result.unwrap() {
LightAccountField::Pda(pda) => {
assert_eq!(pda.ident.to_string(), "record");
let addr_tree_info = &pda.address_tree_info;
let addr_tree_str = quote::quote!(#addr_tree_info).to_string();
assert!(
addr_tree_str.contains("proof"),
"address_tree_info should reference 'proof', got: {}",
addr_tree_str
);
assert!(
addr_tree_str.contains("address_tree_info"),
"address_tree_info should access .address_tree_info field, got: {}",
addr_tree_str
);
let output_tree = &pda.output_tree;
let output_tree_str = quote::quote!(#output_tree).to_string();
assert!(
output_tree_str.contains("proof"),
"output_tree should reference 'proof', got: {}",
output_tree_str
);
assert!(
output_tree_str.contains("output_state_tree_index"),
"output_tree should access .output_state_tree_index field, got: {}",
output_tree_str
);
}
_ => panic!("Expected PDA field"),
}
}
#[test]
fn test_parse_mint_with_direct_proof_arg_uses_proof_ident_for_defaults() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"test"]
)]
pub cmint: UncheckedAccount<'info>
};
let field_ident = field.ident.clone().unwrap();
let proof_ident: Ident = parse_quote!(create_proof);
let direct_proof_arg = Some(proof_ident.clone());
let result = parse_light_account_attr(&field, &field_ident, &direct_proof_arg);
assert!(
result.is_ok(),
"Should parse successfully with direct proof arg"
);
let result = result.unwrap();
assert!(result.is_some(), "Should return Some for init mint");
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert_eq!(mint.field_ident.to_string(), "cmint");
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_token_with_bump_parameter() {
let field: syn::Field = parse_quote! {
#[light_account(init,
token::seeds = [b"vault", self.offer.key()],
token::mint = token_mint,
token::owner = vault_authority,
token::owner_seeds = [b"auth"],
token::bump = params.vault_bump
)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully with bump parameter"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::TokenAccount(token) => {
assert_eq!(token.field_ident.to_string(), "vault");
assert!(token.has_init);
assert!(!token.seeds.is_empty());
assert!(token.bump.is_some(), "bump should be Some when provided");
}
_ => panic!("Expected TokenAccount field"),
}
}
#[test]
fn test_parse_token_without_bump_backwards_compatible() {
let field: syn::Field = parse_quote! {
#[light_account(init,
token::seeds = [b"vault", self.offer.key()],
token::mint = token_mint,
token::owner = vault_authority,
token::owner_seeds = [b"auth"]
)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully without bump parameter"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::TokenAccount(token) => {
assert_eq!(token.field_ident.to_string(), "vault");
assert!(token.has_init);
assert!(!token.seeds.is_empty());
assert!(
token.bump.is_none(),
"bump should be None when not provided"
);
}
_ => panic!("Expected TokenAccount field"),
}
}
#[test]
fn test_parse_mint_with_mint_bump() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"mint"],
mint::bump = params.mint_bump
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully with mint::bump parameter"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert_eq!(mint.field_ident.to_string(), "cmint");
assert!(
mint.mint_bump.is_some(),
"mint_bump should be Some when provided"
);
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_mint_with_authority_bump() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"mint"],
mint::authority_seeds = &[b"auth"],
mint::authority_bump = params.auth_bump
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully with authority_bump parameter"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert_eq!(mint.field_ident.to_string(), "cmint");
assert!(
mint.authority_seeds.is_some(),
"authority_seeds should be Some"
);
assert!(
mint.authority_bump.is_some(),
"authority_bump should be Some when provided"
);
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_mint_without_bumps_backwards_compatible() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"mint"],
mint::authority_seeds = &[b"auth"]
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully without bump parameters"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::Mint(mint) => {
assert_eq!(mint.field_ident.to_string(), "cmint");
assert!(
mint.mint_bump.is_none(),
"mint_bump should be None when not provided"
);
assert!(
mint.authority_seeds.is_some(),
"authority_seeds should be Some"
);
assert!(
mint.authority_bump.is_none(),
"authority_bump should be None when not provided"
);
}
_ => panic!("Expected Mint field"),
}
}
#[test]
fn test_parse_token_bump_shorthand_syntax() {
let field: syn::Field = parse_quote! {
#[light_account(init,
token::seeds = [b"vault"],
token::mint = token_mint,
token::owner = vault_authority,
token::owner_seeds = [b"auth"],
token::bump
)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(
result.is_ok(),
"Should parse successfully with bump shorthand"
);
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::TokenAccount(token) => {
assert!(
token.bump.is_some(),
"bump should be Some with shorthand syntax"
);
}
_ => panic!("Expected TokenAccount field"),
}
}
#[test]
fn test_parse_wrong_namespace_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"], mint::decimals = 9)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("doesn't match account type"),
"Expected namespace mismatch error, got: {}",
err
);
}
#[test]
fn test_old_syntax_gives_helpful_error() {
let field: syn::Field = parse_quote! {
#[light_account(init, mint, authority = some_authority)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Missing namespace prefix") || err.contains("mint::authority"),
"Expected helpful migration error, got: {}",
err
);
}
#[test]
fn test_parse_associated_token_with_init_succeeds() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner, associated_token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_ok());
let result = result.unwrap();
assert!(result.is_some());
match result.unwrap() {
LightAccountField::AssociatedToken(ata) => {
assert!(ata.has_init, "Should have has_init=true");
}
_ => panic!("Expected AssociatedToken field"),
}
}
#[test]
fn test_parse_mixed_token_and_associated_token_prefix_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, associated_token::authority = owner, token::mint = mint)]
pub user_ata: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("doesn't match account type"),
"Expected namespace mismatch error, got: {}",
err
);
}
#[test]
fn test_parse_mixed_associated_token_and_token_prefix_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"], associated_token::mint = mint)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("doesn't match account type"),
"Expected namespace mismatch error, got: {}",
err
);
}
#[test]
fn test_parse_init_mixed_token_and_mint_prefix_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token::seeds = [b"vault"], mint::decimals = 9)]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("doesn't match account type"),
"Expected namespace mismatch error, got: {}",
err
);
}
#[test]
fn test_parse_duplicate_init_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, init)]
pub record: Account<'info, MyRecord>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate") && err.contains("init"),
"Expected duplicate init error, got: {}",
err
);
}
#[test]
fn test_parse_duplicate_zero_copy_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, zero_copy, zero_copy)]
pub record: AccountLoader<'info, MyRecord>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate") && err.contains("zero_copy"),
"Expected duplicate zero_copy error, got: {}",
err
);
}
#[test]
fn test_parse_duplicate_token_type_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init, token, token, token::seeds = [b"vault"], token::mint = mint, token::owner = owner, token::owner_seeds = [b"auth"])]
pub vault: Account<'info, CToken>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Duplicate") || err.contains("::"),
"Expected duplicate or syntax error, got: {}",
err
);
}
#[test]
fn test_parse_conflicting_type_via_namespace_fails() {
let field: syn::Field = parse_quote! {
#[light_account(init,
mint::signer = mint_signer,
mint::authority = authority,
mint::decimals = 9,
mint::seeds = &[b"test"],
token::mint = some_mint
)]
pub cmint: UncheckedAccount<'info>
};
let ident = field.ident.clone().unwrap();
let result = parse_light_account_attr(&field, &ident, &None);
assert!(result.is_err());
let err = result.err().unwrap().to_string();
assert!(
err.contains("Conflicting") || err.contains("doesn't match account type"),
"Expected conflicting or namespace mismatch error, got: {}",
err
);
}
}