airdrop-api 0.1.0

Mint and Airdrop Framework on Solana for Sovereign Individuals
Documentation
use solana_program::pubkey::Pubkey;
use steel::*;

/// Initialize program instruction data
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Initialize {
    /// Program admin authority
    pub admin: Pubkey,

    /// Fee for creating a new mint (in lamports)
    pub mint_fee_lamports: [u8; 8],

    /// Fee per recipient allocation (in lamports)
    pub allocation_fee_per_recipient_lamports: [u8; 8],

    /// Fee per recipient for merkle tree campaigns (in lamports)
    /// Recommended default: 0.0001 SOL = 100_000 lamports
    pub merkle_fee_per_recipient_lamports: [u8; 8],

    /// Fee per recipient for direct transfers (in lamports)
    /// Recommended default: 0.0005 SOL = 500_000 lamports
    pub direct_transfer_fee_per_recipient_lamports: [u8; 8],
}

/// CreateMint instruction data - creates a new SPL token mint with metadata
/// This creates ONLY the mint, not a campaign. Use CreateCampaign to create campaigns.
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct CreateMint {
    /// Mint ID (unique identifier for this mint, max 32 bytes)
    /// Used to derive mint PDA if mint address is not provided
    pub mint_id: [u8; 32],

    /// Noise for mint PDA derivation (for address grinding/pattern matching)
    /// Allows users to grind for custom mint addresses (e.g., starts-with "Kol..." or ends-with "...XYZ")
    /// Example: [0, 0, 0, 0] = no noise (default)
    ///          [174, 198, 166, 179] = noise that produces desired pattern
    ///
    /// How it works:
    /// - PDA seeds: [MINT, mint_id, noise] (see crate::consts::MINT)
    /// - Users can try different noise values offline until they find one that produces desired address pattern
    /// - This becomes a value-add service: "Custom mint addresses with patterns"
    pub noise: [u8; 4],

    /// Mint address (optional: if provided, use this address; if zero, create new PDA)
    pub mint: Pubkey,

    /// Token name (truncated to 32 bytes if needed)
    /// Padding: Trailing null bytes (`\0`)
    /// Example: "My Token" -> [b'M', b'y', b' ', b'T', ..., 0, 0, ...]
    pub name: [u8; 32],

    /// Token symbol (truncated to 10 bytes if needed)
    /// Padding: Trailing null bytes (`\0`)
    /// Example: "USDC" -> [b'U', b'S', b'D', b'C', 0, 0, 0, 0, 0, 0]
    /// Example: "MAT" -> [b'M', b'A', b'T', 0, 0, 0, 0, 0, 0, 0]
    pub symbol: [u8; 10],

    /// Metadata URI pointing to JSON file with token metadata
    /// Padding: Trailing null bytes (`\0`)
    /// JSON format (Metaplex standard):
    /// {
    ///   "name": "Token Name",
    ///   "symbol": "SYMBOL",
    ///   "description": "Token description",
    ///   "image": "https://example.com/icon.png",
    ///   "category": "image",
    ///   "properties": {
    ///     "files": [{"uri": "...", "type": "image/png"}]
    ///   }
    /// }
    /// Required fields: name, description, image, category
    /// Recommended: symbol (should match on-chain), properties.files
    pub uri: [u8; 128],

    /// Token decimals (0-9)
    pub decimals: u8,

    /// Max supply (must be > 0)
    /// All tokens up to max_supply will be minted upfront to mint treasury
    /// Mint authority will be revoked after minting (immutable cap, standard SPL way)
    pub max_supply: [u8; 8],

    /// Padding
    pub _padding: [u8; 3],
}

/// CreateCampaign instruction data - creates a new campaign using an existing mint
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct CreateCampaign {
    /// Campaign ID (unique identifier, max 32 bytes)
    pub campaign_id: [u8; 32],

    /// Mint address (must be existing mint created by CreateMint)
    pub mint: Pubkey,

    /// Merkle root for this campaign (single merkle tree per campaign)
    /// Zero = on-chain allocation mode (no merkle tree)
    pub merkle_root: [u8; 32],

    /// Recipient count (required if merkle_root != 0, for fee calculation)
    /// Only used for merkle fee calculation, not a campaign limit
    pub recipient_count: [u8; 8],

    /// Amount to transfer from mint treasury to campaign treasury (optional)
    /// Tokens are already minted in mint treasury, this just transfers them
    pub initial_supply: [u8; 8],

    /// Maximum supply cap for this campaign (0 = unlimited)
    /// Note: Tokens are already minted in mint treasury, this is for tracking/documentation
    pub max_supply: [u8; 8],

    /// Padding
    pub _padding: [u8; 7],
}

/// Replenish instruction data - allows campaign owner to transfer more tokens from mint treasury to campaign treasury
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Replenish {
    /// Campaign ID
    pub campaign_id: [u8; 32],

    /// Amount to transfer from mint treasury to campaign treasury
    pub amount: [u8; 8],

    /// Padding
    pub _padding: [u8; 7],
}

/// MintTokens instruction data - allows campaign owner to mint more tokens
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct MintTokens {
    /// Campaign ID
    pub campaign_id: [u8; 32],

    /// Amount to mint to campaign treasury
    pub amount: [u8; 8],

    /// Padding
    pub _padding: [u8; 7],
}

/// Allocate instruction data - creates on-chain allocation accounts
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Allocate {
    /// Campaign ID
    pub campaign_id: [u8; 32],

    /// Number of allocations (for variable-length data)
    pub count: u8,

    /// Padding
    pub _padding: [u8; 7],
    // Variable-length data follows:
    // Vec<(Pubkey recipient, u64 amount)> - count * (32 + 8) bytes
}

/// Claim instruction data
/// Claim methods align with campaign fulfill_method:
/// - Method 0: fulfill_method='allocation' - Claim against on-chain Allocation account
/// - Method 1: fulfill_method='record' - Claim against off-chain DB record (with signature)
/// - Method 2: fulfill_method='merkle' - Claim against merkle proof
/// Note: fulfill_method='transfer' uses DirectTransfer instruction (not a claim method)
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct Claim {
    /// Campaign ID
    pub campaign_id: [u8; 32],

    /// Claim method (0=allocation, 1=record, 2=merkle)
    pub method: u8,

    /// Amount to claim (for record/merkle modes)
    pub amount: [u8; 8],

    /// Merkle proof length (for merkle mode, 0-255)
    pub merkle_proof_length: u8,

    /// Padding
    pub _padding: [u8; 6],
    // Variable-length data follows:
    // For merkle mode: merkle_proof_path (Vec<[u8; 32]>) + merkle_proof_indices (Vec<u8>)
    // For record mode: [u8; 64] signature
}

/// UpdateConfig instruction data
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct UpdateConfig {
    /// New admin (Pubkey::default() = keep current)
    pub new_admin: Pubkey,

    /// New mint fee (u64::MAX = keep current)
    pub mint_fee_lamports: [u8; 8],

    /// New allocation fee per recipient (u64::MAX = keep current)
    pub allocation_fee_per_recipient_lamports: [u8; 8],

    /// New merkle fee per recipient (u64::MAX = keep current)
    pub merkle_fee_per_recipient_lamports: [u8; 8],

    /// New direct transfer fee per recipient (u64::MAX = keep current)
    pub direct_transfer_fee_per_recipient_lamports: [u8; 8],
}

/// DirectTransfer instruction data - allows campaign owner to transfer tokens directly to recipients
/// Batch size limited to 10 recipients per transaction (Solana tx size limit)
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct DirectTransfer {
    /// Campaign ID
    pub campaign_id: [u8; 32],

    /// Number of recipients (1-10)
    pub count: u8,

    /// Padding
    pub _padding: [u8; 7],
    // Variable-length data follows:
    // Vec<(Pubkey recipient, u64 amount)> - count * (32 + 8) bytes
}
#[repr(C)]
#[derive(Clone, Copy, Debug, Pod, Zeroable)]
pub struct WithdrawFees {
    /// Amount to withdraw (0 = withdraw all)
    pub amount: [u8; 8],
}

/// Instruction enum
/// Instructions are split into two groups:
/// - Public instructions (0-99): Can be called by any user (including campaign owners)
/// - Admin instructions (100+): Require program admin authority
#[repr(u8)]
#[derive(Clone, Copy, Debug, Eq, PartialEq, TryFromPrimitive)]
pub enum AirdropInstruction {
    // Public Instructions (0-99)
    CreateMint = 0,
    CreateCampaign = 1,
    Allocate = 2,       // On-chain allocation (creates accounts)
    Claim = 3,          // Claim tokens (on-chain, off-chain, or merkle)
    DirectTransfer = 4, // Direct batch transfer
    Replenish = 5,      // Transfer from mint treasury to campaign treasury
    MintTokens = 6,     // Mint additional tokens (if authority not revoked)

    // Admin Instructions (100+)
    Initialize = 100,   // Initialize program
    UpdateConfig = 101, // Update config and fees
    WithdrawFees = 102, // Withdraw accumulated fees
}

instruction!(AirdropInstruction, CreateMint);
instruction!(AirdropInstruction, CreateCampaign);
instruction!(AirdropInstruction, Allocate);
instruction!(AirdropInstruction, Claim);
instruction!(AirdropInstruction, DirectTransfer);
instruction!(AirdropInstruction, Replenish);
instruction!(AirdropInstruction, MintTokens);
instruction!(AirdropInstruction, Initialize);
instruction!(AirdropInstruction, UpdateConfig);
instruction!(AirdropInstruction, WithdrawFees);