Skip to main content

solmail_program/instructions/credits/
withdraw.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},
10    error::SolMailError,
11    state::credits::UserCredits,
12};
13
14/// Withdraw unused credits
15///
16/// Accounts:
17/// 0. `[signer, writable]` User - credits owner
18/// 1. `[writable]` User credits account (PDA)
19/// 2. `[writable]` Credits vault (PDA)
20/// 3. `[]` System program
21///
22/// Data:
23/// - amount: u64 (8 bytes)
24/// - is_usdc: bool (1 byte)
25pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
26    // Parse instruction data
27    if data.len() < 9 {
28        return Err(ProgramError::InvalidInstructionData);
29    }
30
31    let amount = u64::from_le_bytes(data[0..8].try_into().unwrap());
32    let is_usdc = data[8] != 0;
33
34    // Parse accounts
35    let [user, user_credits, credits_vault, _system_program] = accounts else {
36        return Err(ProgramError::NotEnoughAccountKeys);
37    };
38
39    // Verify user is signer
40    if !user.is_signer() {
41        return Err(SolMailError::MissingRequiredSignature.into());
42    }
43
44    // Derive and verify user credits PDA
45    let (credits_pda, _) =
46        find_program_address(&[CREDITS_SEED, user.key().as_ref()], program_id);
47
48    if user_credits.key() != &credits_pda {
49        return Err(SolMailError::InvalidPda.into());
50    }
51
52    // Derive and verify credits vault PDA
53    let (vault_pda, _vault_bump) =
54        find_program_address(&[CREDITS_VAULT_SOL_SEED], program_id);
55
56    if credits_vault.key() != &vault_pda {
57        return Err(SolMailError::InvalidPda.into());
58    }
59
60    // Update user credits balance
61    let credits_data = unsafe { user_credits.borrow_mut_data_unchecked() };
62    let credits = UserCredits::from_bytes_mut(credits_data)?;
63
64    // Verify owner matches
65    if credits.owner != user.key().as_ref() {
66        return Err(SolMailError::InvalidPayer.into());
67    }
68
69    // Deduct credits
70    if is_usdc {
71        credits.sub_usdc(amount)?;
72    } else {
73        credits.sub_sol(amount)?;
74    }
75
76    // Transfer SOL from vault to user (if SOL withdrawal)
77    if !is_usdc {
78        let vault_balance = credits_vault.lamports();
79        if vault_balance < amount {
80            return Err(SolMailError::InsufficientFunds.into());
81        }
82        unsafe {
83            *credits_vault.borrow_mut_lamports_unchecked() -= amount;
84            *user.borrow_mut_lamports_unchecked() += amount;
85        }
86    }
87    // Note: USDC transfers would use pinocchio_token here
88
89    Ok(())
90}