use super::parse::InfraFieldType;
pub(super) struct ValidationContext<'a> {
pub struct_name: &'a syn::Ident,
pub has_pdas: bool,
pub has_mints: bool,
pub has_tokens: bool,
pub has_tokens_with_init: bool,
pub has_atas: bool,
pub has_atas_with_init: bool,
pub has_fee_payer: bool,
pub has_compression_config: bool,
pub has_pda_rent_sponsor: bool,
pub has_light_token_config: bool,
pub has_light_token_rent_sponsor: bool,
pub has_light_token_cpi_authority: bool,
pub has_instruction_args: bool,
pub has_direct_proof_arg: bool,
pub total_account_count: usize,
}
pub(super) fn validate_struct(ctx: &ValidationContext) -> Result<(), syn::Error> {
validate_account_count(ctx)?;
validate_light_account_fields_required(ctx)?;
validate_infra_fields(ctx)?;
validate_proof_availability(ctx)?;
Ok(())
}
fn validate_account_count(ctx: &ValidationContext) -> Result<(), syn::Error> {
if ctx.total_account_count > 255 {
return Err(syn::Error::new_spanned(
ctx.struct_name,
format!(
"Too many compression fields ({} total, maximum 255). \
Light Protocol uses u8 for account indices.",
ctx.total_account_count
),
));
}
Ok(())
}
fn validate_light_account_fields_required(ctx: &ValidationContext) -> Result<(), syn::Error> {
let has_light_account_fields = ctx.has_pdas || ctx.has_mints || ctx.has_tokens || ctx.has_atas;
if ctx.has_instruction_args && !has_light_account_fields {
return Err(syn::Error::new_spanned(
ctx.struct_name,
"#[derive(LightAccounts)] with #[instruction(...)] requires at least one \
#[light_account] field.\n\
\n\
This derive macro is only needed for instructions that create light accounts \
(rent-free PDAs, mints, token accounts, or ATAs).\n\
\n\
Either:\n\
1. Add #[light_account(init)] to fields that should be light accounts\n\
2. Remove #[derive(LightAccounts)] if this instruction doesn't create light accounts",
));
}
Ok(())
}
fn validate_infra_fields(ctx: &ValidationContext) -> Result<(), syn::Error> {
if !ctx.has_pdas && !ctx.has_mints && !ctx.has_tokens && !ctx.has_atas {
return Ok(());
}
let mut missing = Vec::new();
if !ctx.has_fee_payer {
missing.push(InfraFieldType::FeePayer);
}
if ctx.has_pdas {
if !ctx.has_compression_config {
missing.push(InfraFieldType::CompressionConfig);
}
if !ctx.has_pda_rent_sponsor {
missing.push(InfraFieldType::PdaRentSponsor);
}
}
let needs_token_infra = ctx.has_mints || ctx.has_tokens_with_init || ctx.has_atas_with_init;
if needs_token_infra {
if !ctx.has_light_token_config {
missing.push(InfraFieldType::LightTokenConfig);
}
if !ctx.has_light_token_rent_sponsor {
missing.push(InfraFieldType::LightTokenRentSponsor);
}
if ctx.has_mints && !ctx.has_light_token_cpi_authority {
missing.push(InfraFieldType::LightTokenCpiAuthority);
}
}
if !missing.is_empty() {
let mut types = Vec::new();
if ctx.has_pdas {
types.push("PDA");
}
if ctx.has_mints {
types.push("mint");
}
if ctx.has_tokens {
types.push("token account");
}
if ctx.has_atas {
types.push("ATA");
}
let context = types.join(", ");
let mut msg = format!(
"#[derive(LightAccounts)] with {} fields requires the following infrastructure fields:\n",
context
);
for field_type in &missing {
msg.push_str(&format!(
"\n - {} (add one of: {})",
field_type.description(),
field_type.accepted_names().join(", ")
));
}
return Err(syn::Error::new_spanned(ctx.struct_name, msg));
}
Ok(())
}
fn validate_proof_availability(ctx: &ValidationContext) -> Result<(), syn::Error> {
let needs_proof =
ctx.has_pdas || ctx.has_mints || ctx.has_tokens_with_init || ctx.has_atas_with_init;
if !needs_proof {
return Ok(());
}
if !ctx.has_direct_proof_arg && !ctx.has_instruction_args {
return Err(syn::Error::new_spanned(
ctx.struct_name,
"CreateAccountsProof is required for #[light_account(init)] fields.\n\
\n\
Provide it either:\n\
1. As a direct argument: #[instruction(proof: CreateAccountsProof)]\n\
2. As a field on params: #[instruction(params: MyParams)] where MyParams has a `create_accounts_proof: CreateAccountsProof` field",
));
}
Ok(())
}