airdrop 0.1.0

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

/// Process MintTokens instruction
/// Allows campaign owner to mint more tokens to campaign treasury
/// Enforces max_supply limit if set
pub fn process_mint_tokens(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = MintTokens::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_authority, treasury_tokens, token_program]
    let [campaign_owner_info, campaign_info, mint_info, mint_authority_info, 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 exists and is valid
    mint_info.has_address(&campaign.mint)?;
    let mint_data = mint_info.as_mint()?;
    
    // Verify mint authority PDA
    let (expected_mint_authority, _mint_authority_bump) = mint_authority_pda(&campaign.mint);
    mint_authority_info.has_address(&expected_mint_authority)?;
    
    // Verify mint authority matches
    // Check if mint authority has been revoked (set to None)
    if mint_data.mint_authority() == solana_program::program_option::COption::None {
        return Err(AirdropError::MintAuthorityRevoked.into());
    }
    
    if mint_data.mint_authority() != solana_program::program_option::COption::Some(expected_mint_authority) {
        return Err(AirdropError::InvalidMintAuthority.into());
    }

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

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

    // Check max supply before minting
    let current_supply = mint_data.supply();
    let new_supply = current_supply
        .checked_add(amount)
        .ok_or(ProgramError::ArithmeticOverflow)?;
    
    if campaign.max_supply > 0 && new_supply > campaign.max_supply {
        return Err(AirdropError::MaxSupplyExceeded.into());
    }

    // Mint tokens to treasury
    // Note: mint_to_signed() derives bump automatically from seeds, so don't include bump in seeds array
    mint_to_signed(
        mint_info,
        treasury_tokens_info,
        mint_authority_info,
        token_program,
        amount,
        &[MINT_AUTHORITY, campaign.mint.as_ref()], // Seeds only - bump is derived automatically
    )?;

    // Get updated supply after minting
    let mint_data_after = mint_info.as_mint()?;
    let final_supply = mint_data_after.supply();

    // Emit event
    let clock = Clock::get()?;
    airdrop_api::event::TokensMintedEvent {
        campaign: *campaign_info.key,
        mint: campaign.mint,
        amount,
        new_supply: final_supply,
        minted_at: clock.unix_timestamp,
    }
    .log();

    Ok(())
}