airdrop 0.1.0

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

/// Process CreateCampaign instruction
/// Creates a new campaign using an existing mint
/// Multiple campaigns can share the same mint
pub fn process_create_campaign(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = CreateCampaign::try_from_bytes(data)?;
    let campaign_id = args.campaign_id;
    let mint = args.mint;
    let merkle_root = args.merkle_root;
    let recipient_count = u64::from_le_bytes(args.recipient_count);
    let initial_supply = u64::from_le_bytes(args.initial_supply);
    let max_supply = u64::from_le_bytes(args.max_supply);

    // Load accounts
    // [payer, campaign_owner, config, campaign, mint, mint_treasury, mint_treasury_tokens, campaign_treasury, campaign_treasury_tokens, fee_account, token_program, associated_token_program, system_program, rent_sysvar]
    let [payer_info, campaign_owner_info, config_info, campaign_info, mint_info, mint_treasury_info, mint_treasury_tokens_info, campaign_treasury_info, campaign_treasury_tokens_info, fee_account_info, token_program, associated_token_program, system_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Validate accounts
    payer_info.is_signer()?;
    campaign_owner_info.is_signer()?;

    // Verify config
    // Note: No program admin check needed - anyone can create campaigns (just pay fees)
    // Config is mutable for updating total_fees_collected when merkle fees are charged
    let config = config_info
        .is_config()?
        .is_writable()?
        .as_account_mut::<Config>(&airdrop_api::ID)?;

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

    // Verify mint exists and is valid
    mint_info.has_address(&mint)?;
    let _mint_data = mint_info.as_mint()?; // Verify it's actually a mint account

    // Verify mint treasury PDA (holds all minted tokens for this mint)
    let (expected_mint_treasury, _mint_treasury_bump) = mint_treasury_pda(&mint);
    mint_treasury_info
        .is_writable()?
        .has_address(&expected_mint_treasury)?;

    // Verify mint treasury token account (associated token account)
    let expected_mint_treasury_tokens =
        spl_associated_token_account::get_associated_token_address_with_program_id(
            &expected_mint_treasury,
            &mint,
            &spl_token::ID,
        );
    mint_treasury_tokens_info
        .is_writable()?
        .has_address(&expected_mint_treasury_tokens)?;

    // Verify campaign treasury PDA
    let (expected_campaign_treasury, _campaign_treasury_bump) = campaign_treasury_pda(&campaign_id);
    campaign_treasury_info.has_address(&expected_campaign_treasury)?;

    // Verify campaign treasury token account (will be created, so check if empty or matches expected address)
    let expected_campaign_treasury_tokens =
        spl_associated_token_account::get_associated_token_address_with_program_id(
            &expected_campaign_treasury,
            &mint,
            &spl_token::ID,
        );
    campaign_treasury_tokens_info
        .is_writable()?
        .has_address(&expected_campaign_treasury_tokens)?;

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

    token_program.is_program(&spl_token::ID)?;
    associated_token_program.is_program(&spl_associated_token_account::ID)?;
    system_program.is_program(&system_program::ID)?;
    rent_sysvar.is_sysvar(&sysvar::rent::ID)?;

    // Calculate and collect merkle fees (if merkle root is set)
    // Collect fees BEFORE operation (prevents fee payment without service)
    let merkle_fee = if merkle_root != [0u8; 32] {
        // Merkle mode: Charge merkle fees based on recipient count
        if recipient_count == 0 {
            return Err(AirdropError::InvalidAmount.into());
        }
        config
            .merkle_fee_per_recipient_lamports
            .checked_mul(recipient_count)
            .ok_or(ProgramError::ArithmeticOverflow)?
    } else {
        // No merkle root: No fees
        0
    };

    // Transfer merkle fees from payer to fee account (if applicable)
    if merkle_fee > 0 {
        invoke(
            &solana_program::system_instruction::transfer(
                payer_info.key,
                fee_account_info.key,
                merkle_fee,
            ),
            &[
                payer_info.clone(),
                fee_account_info.clone(),
                system_program.clone(),
            ],
        )?;

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

    // Create campaign account
    let clock = Clock::get()?;
    create_program_account::<Campaign>(
        campaign_info,
        system_program,
        payer_info,
        &airdrop_api::ID,
        &[CAMPAIGN, &campaign_id],
    )?;

    // Initialize campaign
    let campaign = campaign_info.as_account_mut::<Campaign>(&airdrop_api::ID)?;
    campaign.owner = *campaign_owner_info.key;
    campaign.mint = *mint_info.key;
    campaign.treasury = *campaign_treasury_tokens_info.key;
    campaign.merkle_root = merkle_root;
    campaign.status = CAMPAIGN_STATUS_ACTIVE;
    campaign.total_allocated = 0;
    campaign.total_claimed = 0;
    campaign.max_supply = max_supply;
    campaign.created_at = clock.unix_timestamp;

    // Create campaign treasury token account (associated token account)
    create_associated_token_account(
        payer_info,
        campaign_treasury_info,
        campaign_treasury_tokens_info,
        mint_info,
        system_program,
        token_program,
        associated_token_program,
    )?;

    // Transfer tokens from mint treasury to campaign treasury (NO MINTING)
    // Tokens are already minted in mint treasury, we just transfer them
    if initial_supply > 0 {
        // Transfer tokens from mint treasury to campaign treasury
        // Mint treasury PDA signs the transfer
        transfer_signed(
            mint_treasury_info,
            mint_treasury_tokens_info,
            campaign_treasury_tokens_info,
            token_program,
            initial_supply,
            &[TREASURY, mint.as_ref()], // Mint treasury PDA seeds
        )?;
    }

    // Emit event
    airdrop_api::event::CampaignCreatedEvent {
        campaign: *campaign_info.key,
        mint: *mint_info.key,
        owner: *campaign_owner_info.key,
        merkle_root,
        initial_supply,
        created_at: clock.unix_timestamp,
    }
    .log();

    Ok(())
}