airdrop 0.1.0

Mint and Airdrop Framework on Solana for Sovereign Individuals
Documentation
use airdrop_api::Config;
use airdrop_api::{
    consts::{MINT, MINT_AUTHORITY},
    instruction::CreateMint,
    loaders::AirdropAccountInfoValidation,
    pda::{mint_authority_pda, mint_pda, mint_treasury_pda},
    prelude::*,
};
use mpl_token_metadata::instructions::CreateMetadataAccountV3Cpi;
use solana_program::{
    clock::Clock,
    program::{invoke, invoke_signed},
    program_pack::Pack,
    sysvar,
};
use spl_token::{
    instruction::{set_authority, AuthorityType},
    state::Mint,
};
use steel::*;

/// Process CreateMint instruction
/// Creates ONLY the mint and metadata, NOT a campaign
/// Use CreateCampaign to create campaigns using this mint
///
/// Comparison with miracle-copy:
/// - Miracle-copy: Creates mint + treasury + campaign in one step (Initialize)
/// - Our approach: Separates mint creation from campaign creation (more flexible)
/// - Both use Metaplex Token Metadata (mpl-token-metadata)
/// - Both use PDA-based mint authority
pub fn process_create_mint(accounts: &[AccountInfo<'_>], data: &[u8]) -> ProgramResult {
    // Parse instruction data
    let args = CreateMint::try_from_bytes(data)?;
    let mint_id = args.mint_id;
    let noise = args.noise;
    let mint_address = args.mint;
    let decimals = args.decimals;
    let max_supply = u64::from_le_bytes(args.max_supply);

    // Validate max_supply (must be > 0)
    if max_supply == 0 {
        return Err(AirdropError::InvalidAmount.into());
    }

    // Load accounts
    // [payer, config, mint, mint_authority, mint_metadata, mint_treasury, mint_treasury_tokens, fee_account, system_program, token_program, associated_token_program, metadata_program, rent_sysvar]
    let [payer_info, config_info, mint_info, mint_authority_info, mint_metadata_info, mint_treasury_info, mint_treasury_tokens_info, fee_account_info, system_program, token_program, associated_token_program, metadata_program, rent_sysvar] =
        accounts
    else {
        return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Validate accounts
    payer_info.is_signer()?;

    // Load config (no admin check - anyone can create mints, just pay the fee)
    // is_config() validates address + type, as_account() deserializes the struct
    let config = config_info
        .is_config()?
        .as_account::<Config>(&airdrop_api::ID)?;

    // Verify fee account early (before checking if mint exists)
    fee_account_info
        .is_writable()?
        .has_address(&config.fee_account)?;

    // Determine mint address
    // Strategy: Always derive expected PDA from mint_id + noise first
    // If mint_address is provided, verify it matches the derived PDA
    //   - KOL scenario: Frontend can pre-compute mint address from mint_id + noise (KOL wallet bytes)
    //   - Default scenario: Frontend passes zero, we derive PDA from random mint_id + noise
    //   - Reuse scenario: Frontend passes existing mint address that matches derived PDA
    //   - Address grinding: Frontend can grind different noise values offline to find desired pattern
    let (expected_mint, mint_bump) = mint_pda(&mint_id, &noise);

    let (mint_pubkey, mint_bump) = if mint_address == Pubkey::default() {
        // No address provided: use derived PDA
        mint_info
            .is_empty()?
            .is_writable()?
            .has_address(&expected_mint)?;
        (expected_mint, mint_bump)
    } else {
        // Address provided: verify it matches derived PDA
        if mint_address != expected_mint {
            return Err(AirdropError::InvalidMint.into());
        }
        mint_info.is_writable()?.has_address(&mint_address)?;

        // Check if mint already exists
        if !mint_info.data_is_empty() {
            // Mint exists: verify it's valid and skip creation (no fee for reusing existing mint)
            let mint_data = mint_info.as_mint()?;
            let (expected_mint_authority, _) = mint_authority_pda(&mint_address);
            if mint_data.mint_authority()
                != solana_program::program_option::COption::Some(expected_mint_authority)
            {
                return Err(AirdropError::InvalidMintAuthority.into());
            }
            // Mint already exists and is valid, skip creation
            // Note: No fee charged for reusing existing mint (intentional)
            return Ok(());
        }
        // Mint doesn't exist but address matches derived PDA: proceed to create
        mint_info.is_empty()?;
        (mint_address, mint_bump)
    };

    // Verify mint authority PDA (shared across campaigns using same mint)
    let (expected_mint_authority, mint_authority_bump) = mint_authority_pda(&mint_pubkey);
    mint_authority_info.has_address(&expected_mint_authority)?;

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

    // Collect mint fee from payer using System Program transfer (idiomatic Solana way)
    // This is safer and more consistent with reference programs (escrow-copy, universalsettle-copy)
    let mint_fee = config.mint_fee_lamports;
    if mint_fee > 0 {
        invoke(
            &solana_program::system_instruction::transfer(
                payer_info.key,
                fee_account_info.key,
                mint_fee,
            ),
            &[
                payer_info.clone(),
                fee_account_info.clone(),
                system_program.clone(),
            ],
        )?;
    }

    // Update config total fees collected
    // Collect fees BEFORE operation (prevents fee payment without service)
    let config_mut = config_info
        .is_config()?
        .is_writable()?
        .as_account_mut::<Config>(&airdrop_api::ID)?;
    config_mut.total_fees_collected = config_mut
        .total_fees_collected
        .checked_add(mint_fee)
        .ok_or(ProgramError::ArithmeticOverflow)?;

    // Create mint account (as PDA)
    // Use Mint::LEN from spl_token instead of hardcoding 82
    allocate_account_with_bump(
        mint_info,
        system_program,
        payer_info,
        Mint::LEN,
        &spl_token::ID,
        &[MINT, &mint_id, &noise],
        mint_bump,
    )?;

    // Initialize mint with mint authority PDA
    // Note: seeds and bump are for the MINT PDA (not mint_authority PDA)
    // The mint account needs to sign because it's a PDA being initialized
    initialize_mint_signed_with_bump(
        mint_info,
        mint_authority_info,
        None, // Freeze authority = None (matches miracle-copy)
        token_program,
        rent_sysvar,
        decimals,
        &[MINT, &mint_id, &noise], // Mint PDA seeds (not mint_authority seeds!)
        mint_bump,                 // Mint PDA bump (not mint_authority bump!)
    )?;

    // Create metadata account using Metaplex Token Metadata (same as miracle-copy)
    let name = String::from_utf8_lossy(&args.name)
        .trim_end_matches('\0')
        .to_string();
    let symbol = String::from_utf8_lossy(&args.symbol)
        .trim_end_matches('\0')
        .to_string();
    let uri = String::from_utf8_lossy(&args.uri)
        .trim_end_matches('\0')
        .to_string();

    // Check if metadata already exists (for shared mints)
    if mint_metadata_info.data_is_empty() {
        CreateMetadataAccountV3Cpi {
            __program: metadata_program,
            metadata: mint_metadata_info,
            mint: mint_info,
            mint_authority: mint_authority_info,
            payer: payer_info,
            update_authority: (mint_authority_info, true), // Mint authority can update metadata
            system_program,
            rent: Some(rent_sysvar),
            __args: mpl_token_metadata::instructions::CreateMetadataAccountV3InstructionArgs {
                data: mpl_token_metadata::types::DataV2 {
                    name,
                    symbol,
                    uri,
                    seller_fee_basis_points: 0,
                    creators: None,
                    collection: None,
                    uses: None,
                },
                is_mutable: true,
                collection_details: None,
            },
        }
        .invoke_signed(&[&[
            MINT_AUTHORITY,
            mint_pubkey.as_ref(),
            &[mint_authority_bump],
        ]])?;
    }
    // If metadata exists, skip creation (mint is being reused)

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

    // Create mint treasury token account (associated token account)
    // This is the mint-specific treasury that holds all minted tokens
    create_associated_token_account(
        payer_info,
        mint_treasury_info,
        mint_treasury_tokens_info,
        mint_info,
        system_program,
        token_program,
        associated_token_program,
    )?;

    // Mint all tokens upfront to mint treasury
    // Mint max_supply tokens directly (max_supply must be > 0)
    // Mint tokens to mint treasury
    // Note: mint_to_signed() derives bump automatically from seeds, so don't include bump in seeds array
    mint_to_signed(
        mint_info,
        mint_treasury_tokens_info,
        mint_authority_info,
        token_program,
        max_supply,
        &[MINT_AUTHORITY, mint_pubkey.as_ref()], // Seeds only - bump is derived automatically
    )?;

    // Revoke mint authority after minting (immutable cap, standard SPL way)
    // This prevents any further minting, enforcing max_supply at the SPL Token level
    invoke_signed(
        &set_authority(
            token_program.key,
            mint_info.key,
            None, // Set to None = REVOKE mint authority
            AuthorityType::MintTokens,
            mint_authority_info.key,
            &[mint_authority_info.key],
        )?,
        &[
            token_program.clone(),
            mint_info.clone(),
            mint_authority_info.clone(),
        ],
        &[&[MINT_AUTHORITY, mint_pubkey.as_ref(), &[mint_authority_bump]]],
    )?;

    // Emit event
    let clock = Clock::get()?;
    airdrop_api::event::MintCreatedEvent {
        mint: mint_pubkey,
        mint_authority: *mint_authority_info.key,
        admin: *payer_info.key, // Payer is the one who created the mint
        max_supply,
        created_at: clock.unix_timestamp,
    }
    .log();

    Ok(())
}