Skip to main content

solmail_program/instructions/credits/
spend.rs

1use pinocchio::{
2    account_info::AccountInfo,
3    program_error::ProgramError,
4    pubkey::{find_program_address, Pubkey},
5    ProgramResult,
6};
7
8use crate::{
9    constants::{CREDITS_SEED, CREDITS_VAULT_SOL_SEED, ORACLE_PUBKEY, TREASURY_SEED},
10    error::SolMailError,
11    state::credits::UserCredits,
12};
13
14/// Spend credits (oracle-only) - deduct for mail order
15///
16/// Accounts:
17/// 0. `[signer]` Oracle - authorized backend service
18/// 1. `[writable]` User credits account (PDA)
19/// 2. `[writable]` Credits vault (PDA)
20/// 3. `[writable]` Treasury (PDA) - receives payment
21/// 4. `[]` System program
22///
23/// Data:
24/// - user_pubkey: [u8; 32] (32 bytes) - whose credits to spend
25/// - amount: u64 (8 bytes)
26/// - is_usdc: bool (1 byte)
27pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
28    // Parse instruction data
29    if data.len() < 41 {
30        return Err(ProgramError::InvalidInstructionData);
31    }
32
33    let user_pubkey: [u8; 32] = data[0..32].try_into().unwrap();
34    let amount = u64::from_le_bytes(data[32..40].try_into().unwrap());
35    let is_usdc = data[40] != 0;
36
37    // Parse accounts
38    let [oracle, user_credits, credits_vault, treasury, _system_program] = accounts else {
39        return Err(ProgramError::NotEnoughAccountKeys);
40    };
41
42    // Verify oracle is signer
43    if !oracle.is_signer() {
44        return Err(SolMailError::MissingRequiredSignature.into());
45    }
46
47    // Verify oracle is authorized
48    if oracle.key().as_ref() != &ORACLE_PUBKEY {
49        return Err(SolMailError::UnauthorizedOracle.into());
50    }
51
52    // Derive and verify user credits PDA
53    let (credits_pda, _) =
54        find_program_address(&[CREDITS_SEED, &user_pubkey], program_id);
55
56    if user_credits.key() != &credits_pda {
57        return Err(SolMailError::InvalidPda.into());
58    }
59
60    // Derive and verify credits vault PDA
61    let (vault_pda, _vault_bump) =
62        find_program_address(&[CREDITS_VAULT_SOL_SEED], program_id);
63
64    if credits_vault.key() != &vault_pda {
65        return Err(SolMailError::InvalidPda.into());
66    }
67
68    // Derive and verify treasury PDA
69    let (treasury_pda, _) = find_program_address(&[TREASURY_SEED], program_id);
70
71    if treasury.key() != &treasury_pda {
72        return Err(SolMailError::InvalidPda.into());
73    }
74
75    // Update user credits balance
76    let credits_data = unsafe { user_credits.borrow_mut_data_unchecked() };
77    let credits = UserCredits::from_bytes_mut(credits_data)?;
78
79    // Verify owner matches
80    if credits.owner != user_pubkey {
81        return Err(SolMailError::InvalidPayer.into());
82    }
83
84    // Deduct credits
85    if is_usdc {
86        credits.sub_usdc(amount)?;
87    } else {
88        credits.sub_sol(amount)?;
89    }
90
91    // Transfer SOL from vault to treasury (if SOL payment)
92    if !is_usdc {
93        let vault_balance = credits_vault.lamports();
94        if vault_balance < amount {
95            return Err(SolMailError::InsufficientFunds.into());
96        }
97        unsafe {
98            *credits_vault.borrow_mut_lamports_unchecked() -= amount;
99            *treasury.borrow_mut_lamports_unchecked() += amount;
100        }
101    }
102    // Note: USDC transfers would use pinocchio_token here
103
104    Ok(())
105}