airdrop 0.1.0

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

/// Process Replenish instruction
/// Allows campaign owner to transfer more tokens from mint treasury to campaign treasury
/// This is useful when initial_supply < max_supply and campaign needs more tokens
pub fn process_replenish(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = Replenish::try_from_bytes(data)?;
    let campaign_id = args.campaign_id;
    let amount = u64::from_le_bytes(args.amount);

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

    // Load accounts
    // [campaign_owner, campaign, mint, mint_treasury, mint_treasury_tokens, campaign_treasury, campaign_treasury_tokens, token_program]
    let [campaign_owner_info, campaign_info, mint_info, mint_treasury_info, mint_treasury_tokens_info, campaign_treasury_info, campaign_treasury_tokens_info, token_program] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Validate accounts
    campaign_owner_info.is_signer()?;

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

    // Load and verify campaign (using chainable pattern for consistency)
    let campaign = campaign_info
        .is_campaign()?
        .as_account_mut::<Campaign>(&airdrop_api::ID)?
        .assert_mut(|c| c.owner == *campaign_owner_info.key)?
        .assert_mut(|c| c.status == CAMPAIGN_STATUS_ACTIVE)?;

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

    // Verify mint treasury PDA
    let (expected_mint_treasury, _mint_treasury_bump) = mint_treasury_pda(&campaign.mint);
    mint_treasury_info
        .is_writable()?
        .has_address(&expected_mint_treasury)?;

    // Verify mint treasury token account
    let expected_mint_treasury_tokens =
        spl_associated_token_account::get_associated_token_address_with_program_id(
            &expected_mint_treasury,
            &campaign.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
    let expected_campaign_treasury_tokens =
        spl_associated_token_account::get_associated_token_address_with_program_id(
            &expected_campaign_treasury,
            &campaign.mint,
            &spl_token::ID,
        );
    campaign_treasury_tokens_info
        .is_writable()?
        .has_address(&expected_campaign_treasury_tokens)?;

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

    // Optional: Enforce max_supply limit
    // Get current campaign treasury balance
    let treasury_token_account = campaign_treasury_tokens_info.as_token_account()?;
    let current_balance = treasury_token_account.amount();
    let new_balance = current_balance
        .checked_add(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    if campaign.max_supply > 0 && new_balance > campaign.max_supply {
        return Err(AirdropError::MaxSupplyExceeded.into());
    }

    // 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,
        amount,
        &[TREASURY, campaign.mint.as_ref()], // Mint treasury PDA seeds
    )?;

    // Emit event
    let clock = Clock::get()?;
    airdrop_api::event::TokensReplenishedEvent {
        campaign: *campaign_info.key,
        mint: campaign.mint,
        amount,
        new_balance,
        replenished_at: clock.unix_timestamp,
    }
    .log();

    Ok(())
}