airdrop 0.1.0

Mint and Airdrop Framework on Solana for Sovereign Individuals
Documentation
use airdrop_api::{
    consts::{ALLOCATION, CAMPAIGN_STATUS_ACTIVE},
    instruction::Allocate,
    loaders::AirdropAccountInfoValidation,
    pda::{allocation_pda, campaign_pda},
    prelude::*,
};
use airdrop_api::{Allocation, Campaign, Config};
use solana_program::{clock::Clock, program::invoke, program_error::ProgramError};
use steel::*;

pub fn process_allocate(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = Allocate::try_from_bytes(data)?;
    let campaign_id = args.campaign_id;
    let count = args.count as usize;

    // Load accounts
    // [owner, campaign, config, fee_account, treasury_tokens, token_program, system_program, ...allocation_accounts]
    let [owner_info, campaign_info, config_info, fee_account_info, treasury_tokens_info, token_program, system_program, allocation_accounts @ ..] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Validate owner (campaign owner)
    owner_info.is_signer()?;

    // Load and validate config (mutable for updating total_fees_collected)
    let config = config_info
        .is_config()?
        .is_writable()?
        .as_account_mut::<Config>(&airdrop_api::ID)?;

    // Verify fee account
    fee_account_info
        .is_writable()?
        .has_address(&config.fee_account)?;

    // Verify campaign PDA matches campaign_id
    let (expected_campaign, _campaign_bump) = campaign_pda(&campaign_id);
    campaign_info
        .is_campaign()?
        .has_address(&expected_campaign)?;

    // Load and validate campaign
    let campaign = campaign_info
        .as_account_mut::<Campaign>(&airdrop_api::ID)?
        .assert_mut(|c| c.owner == *owner_info.key)?
        .assert_mut(|c| c.status == CAMPAIGN_STATUS_ACTIVE)?;

    // Verify treasury token account
    treasury_tokens_info
        .is_writable()?
        .has_address(&campaign.treasury)?;

    token_program.is_program(&spl_token::ID)?;
    system_program.is_program(&system_program::ID)?;

    // Calculate and collect allocation fees (creates accounts)
    // Collect fees BEFORE operation (prevents fee payment without service)
    let total_fee = config
        .allocation_fee_per_recipient_lamports
        .checked_mul(count as u64)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    // Transfer fees from owner to fee account using System Program transfer (idiomatic Solana way)
    if total_fee > 0 {
        invoke(
            &solana_program::system_instruction::transfer(
                owner_info.key,
                fee_account_info.key,
                total_fee,
            ),
            &[
                owner_info.clone(),
                fee_account_info.clone(),
                system_program.clone(),
            ],
        )?;

        // Update config total fees collected
        config.total_fees_collected = config
            .total_fees_collected
            .checked_add(total_fee)
            .ok_or(ProgramError::ArithmeticOverflow)?;
    }

    // Parse allocation data
    let mut offset = std::mem::size_of::<Allocate>();
    let mut allocations = Vec::new();

    for _ in 0..count {
        if offset + 32 + 8 > data.len() {
            return Err(ProgramError::InvalidInstructionData);
        }

        let recipient_bytes: [u8; 32] = data[offset..offset + 32]
            .try_into()
            .map_err(|_| ProgramError::InvalidInstructionData)?;
        let recipient =
            Pubkey::try_from(recipient_bytes).map_err(|_| ProgramError::InvalidInstructionData)?;

        let amount_bytes: [u8; 8] = data[offset + 32..offset + 32 + 8]
            .try_into()
            .map_err(|_| ProgramError::InvalidInstructionData)?;
        let amount = u64::from_le_bytes(amount_bytes);

        if amount == 0 {
            return Err(AirdropError::InvalidAmount.into());
        }

        allocations.push((recipient, amount));
        offset += 32 + 8;
    }

    // Verify we have enough allocation accounts
    if allocation_accounts.len() < count {
        return Err(ProgramError::NotEnoughAccountKeys);
    }

    // Create Allocation accounts (on-chain mode only)
    let clock = Clock::get()?;
    for (i, (recipient, amount)) in allocations.iter().enumerate() {
        let allocation_info = &allocation_accounts[i];

        // Verify allocation account PDA
        let (expected_allocation, _) = allocation_pda(*campaign_info.key, *recipient);
        allocation_info
            .is_empty()?
            .is_writable()?
            .has_address(&expected_allocation)?;

        // Create allocation account
        create_program_account::<Allocation>(
            allocation_info,
            system_program,
            owner_info,
            &airdrop_api::ID,
            &[ALLOCATION, campaign_info.key.as_ref(), recipient.as_ref()],
        )?;

        // Initialize allocation data
        let allocation = allocation_info.as_account_mut::<Allocation>(&airdrop_api::ID)?;
        allocation.campaign = *campaign_info.key;
        allocation.recipient = *recipient;
        allocation.amount = *amount;
        allocation.claimed = 0;
        allocation.allocated_at = clock.unix_timestamp;
        allocation.claimed_at = 0;

        // Update campaign totals
        campaign.total_allocated = campaign
            .total_allocated
            .checked_add(*amount)
            .ok_or(ProgramError::ArithmeticOverflow)?;

        // Emit event
        TokensAllocatedEvent {
            campaign: *campaign_info.key,
            recipient: *recipient,
            amount: *amount,
        }
        .log();
    }

    Ok(())
}