cardcli-demo / src /fee_program.rs
CardCLI Bot
Deploy CardCLI Hugging Face Space
d2948d0
use crate::error::{AppError, ErrorCode};
use borsh::{to_vec, BorshSerialize};
use sha2::{Digest, Sha256};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
pub const CARDCLI_FEE_VAULT_PROGRAM_ID: &str = "EpB6hUZUf1vvvTVAYvEN57pjUWfYswaAuKGGQDHP5iH";
pub const DEFAULT_FIXED_FEE_CENTS: u64 = 10;
pub const DEFAULT_VARIABLE_FEE_BPS: u16 = 20;
const FEE_VAULT_SEED: &[u8] = b"fee_vault";
#[derive(BorshSerialize)]
struct InitializeArgs {
fee_bps: u16,
fixed_fee_usd_cents: u16,
}
#[derive(BorshSerialize)]
struct CollectFeeArgs {
card_amount_usd_cents: u64,
fee_usd_cents: u64,
fee_lamports: u64,
fee_reference: [u8; 16],
}
pub fn card_fee_cents(card_amount_cents: u64, fixed_fee_cents: u64, fee_bps: u16) -> u64 {
let variable_fee = (card_amount_cents * fee_bps as u64).div_ceil(10_000);
fixed_fee_cents + variable_fee
}
pub fn usd_cents_to_lamports(fee_usd_cents: u64, sol_price_usd: &str) -> Result<u64, AppError> {
let price_cents = parse_money_to_cents(sol_price_usd)? as u128;
if price_cents == 0 {
return Err(AppError::new(
ErrorCode::Usage,
"SOL spot price must be greater than zero",
));
}
let lamports = (fee_usd_cents as u128 * 1_000_000_000u128).div_ceil(price_cents) as u64;
Ok(lamports.max(1))
}
pub fn derive_treasury_vault_pda(program_id: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(&[FEE_VAULT_SEED], program_id)
}
pub fn initialize_fee_vault_instruction(
program_id: &Pubkey,
authority: &Pubkey,
fee_bps: u16,
fixed_fee_cents: u64,
) -> Result<(Instruction, Pubkey), AppError> {
let (fee_vault, _bump) = derive_treasury_vault_pda(program_id);
let fixed_fee_usd_cents = u16::try_from(fixed_fee_cents).map_err(|_| {
AppError::new(
ErrorCode::Usage,
"fixed_fee_cents must fit into a u16 for Anchor initialization",
)
})?;
let args = InitializeArgs {
fee_bps,
fixed_fee_usd_cents,
};
let mut data = anchor_discriminator("initialize").to_vec();
data.extend(to_vec(&args).map_err(|e| AppError::new(ErrorCode::General, e.to_string()))?);
Ok((
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(fee_vault, false),
AccountMeta::new(*authority, true),
AccountMeta::new_readonly(
"11111111111111111111111111111111"
.parse::<Pubkey>()
.expect("valid system program id"),
false,
),
],
data,
},
fee_vault,
))
}
pub fn collect_fee_instruction(
program_id: &Pubkey,
payer: &Pubkey,
card_amount_usd_cents: u64,
fee_usd_cents: u64,
fee_lamports: u64,
fee_reference: [u8; 16],
) -> Result<(Instruction, Pubkey), AppError> {
let (fee_vault, _bump) = derive_treasury_vault_pda(program_id);
let args = CollectFeeArgs {
card_amount_usd_cents,
fee_usd_cents,
fee_lamports,
fee_reference,
};
let mut data = anchor_discriminator("collect_fee").to_vec();
data.extend(to_vec(&args).map_err(|e| AppError::new(ErrorCode::General, e.to_string()))?);
Ok((
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(fee_vault, false),
AccountMeta::new(*payer, true),
AccountMeta::new_readonly(
"11111111111111111111111111111111"
.parse::<Pubkey>()
.expect("valid system program id"),
false,
),
],
data,
},
fee_vault,
))
}
fn anchor_discriminator(name: &str) -> [u8; 8] {
let mut hasher = Sha256::new();
hasher.update(format!("global:{name}").as_bytes());
let hash = hasher.finalize();
let mut out = [0u8; 8];
out.copy_from_slice(&hash[..8]);
out
}
fn parse_money_to_cents(value: &str) -> Result<i64, AppError> {
let parsed = value.trim().parse::<f64>().map_err(|_| {
AppError::new(
ErrorCode::Usage,
format!("Invalid money value for SOL price: {value}"),
)
})?;
Ok((parsed * 100.0).round() as i64)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hybrid_fee_formula_is_stable() {
assert_eq!(
card_fee_cents(1_000, DEFAULT_FIXED_FEE_CENTS, DEFAULT_VARIABLE_FEE_BPS),
12
);
}
#[test]
fn fee_lamports_rounds_up() {
assert_eq!(usd_cents_to_lamports(12, "80.00").unwrap(), 1_500_000);
}
#[test]
fn fee_vault_space_matches_anchor_account() {
assert_eq!(8 + 61, 69);
}
#[test]
fn collect_fee_discriminator_is_stable() {
assert_eq!(
anchor_discriminator("collect_fee"),
[60, 173, 247, 103, 4, 93, 130, 48]
);
}
#[test]
fn initialize_discriminator_is_stable() {
assert_eq!(
anchor_discriminator("initialize"),
[175, 175, 109, 31, 13, 152, 155, 237]
);
}
}