airdrop 0.1.0

Mint and Airdrop Framework on Solana for Sovereign Individuals
Documentation
use airdrop_api::{
    consts::{CAMPAIGN_STATUS_ACTIVE, TREASURY},
    instruction::DirectTransfer,
    loaders::AirdropAccountInfoValidation,
    pda::campaign_treasury_pda,
};
use airdrop_api::{AirdropError, Campaign, Config};
use solana_program::{clock::Clock, program::invoke, program_error::ProgramError, pubkey::Pubkey};
use steel::*;

/// Process DirectTransfer instruction
/// Allows campaign owner to transfer tokens directly to recipients in batches
/// Batch size limited to 10 recipients per transaction
pub fn process_direct_transfer(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = DirectTransfer::try_from_bytes(data)?;
    let campaign_id = args.campaign_id;
    let count = args.count as usize;

    // Validate batch size (1-10 recipients)
    if count == 0 || count > 10 {
        return Err(AirdropError::InvalidAmount.into());
    }

    // Load accounts
    // [owner, campaign, config, fee_account, treasury, treasury_tokens, mint, token_program, associated_token_program, system_program, ...recipient_token_accounts]
    // Note: Each recipient needs their token account, so we need count recipient accounts
    let [owner_info, campaign_info, config_info, fee_account_info, treasury_info, treasury_tokens_info, mint_info, token_program, associated_token_program, system_program, recipient_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)?;

    // Load and validate campaign
    let campaign = campaign_info
        .is_campaign()?
        .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 PDA
    let (expected_treasury, _treasury_bump) = campaign_treasury_pda(&campaign_id);
    treasury_info.has_address(&expected_treasury)?;

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

    // Verify mint matches
    mint_info.has_address(&campaign.mint)?;

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

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

    // Transfer fees from owner to fee account
    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 recipient data from instruction data
    let mut offset = std::mem::size_of::<DirectTransfer>();
    let mut transfers = 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());
        }

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

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

    // Process transfers
    let clock = Clock::get()?;
    let mut total_transferred = 0u64;

    for (i, (recipient, amount)) in transfers.iter().enumerate() {
        let recipient_tokens_info = &recipient_accounts[i];

        // Verify recipient token account (will be created if needed)
        recipient_tokens_info.is_writable()?;

        // Create recipient token account if needed
        if recipient_tokens_info.data_is_empty() {
            create_associated_token_account(
                owner_info,
                owner_info,
                recipient_tokens_info,
                mint_info,
                system_program,
                token_program,
                associated_token_program,
            )?;
        }

        // Verify recipient token account matches expected address
        let expected_recipient_tokens =
            spl_associated_token_account::get_associated_token_address_with_program_id(
                recipient,
                &campaign.mint,
                &spl_token::ID,
            );
        recipient_tokens_info.has_address(&expected_recipient_tokens)?;

        // Transfer tokens from treasury to recipient using PDA-signed transfer
        transfer_signed(
            treasury_info,
            treasury_tokens_info,
            recipient_tokens_info,
            token_program,
            *amount,
            &[TREASURY, &campaign_id],
        )?;

        total_transferred = total_transferred
            .checked_add(*amount)
            .ok_or(ProgramError::ArithmeticOverflow)?;

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

    // Emit event
    airdrop_api::event::TokensDirectTransferredEvent {
        campaign: *campaign_info.key,
        recipient_count: count as u8,
        _padding1: [0u8; 7],
        total_amount: total_transferred,
        transferred_at: clock.unix_timestamp,
        _padding2: [0u8; 8],
    }
    .log();

    Ok(())
}