Spaces:
Sleeping
Sleeping
| 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"; | |
| struct InitializeArgs { | |
| fee_bps: u16, | |
| fixed_fee_usd_cents: u16, | |
| } | |
| 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) | |
| } | |
| mod tests { | |
| use super::*; | |
| fn hybrid_fee_formula_is_stable() { | |
| assert_eq!( | |
| card_fee_cents(1_000, DEFAULT_FIXED_FEE_CENTS, DEFAULT_VARIABLE_FEE_BPS), | |
| 12 | |
| ); | |
| } | |
| fn fee_lamports_rounds_up() { | |
| assert_eq!(usd_cents_to_lamports(12, "80.00").unwrap(), 1_500_000); | |
| } | |
| fn fee_vault_space_matches_anchor_account() { | |
| assert_eq!(8 + 61, 69); | |
| } | |
| fn collect_fee_discriminator_is_stable() { | |
| assert_eq!( | |
| anchor_discriminator("collect_fee"), | |
| [60, 173, 247, 103, 4, 93, 130, 48] | |
| ); | |
| } | |
| fn initialize_discriminator_is_stable() { | |
| assert_eq!( | |
| anchor_discriminator("initialize"), | |
| [175, 175, 109, 31, 13, 152, 155, 237] | |
| ); | |
| } | |
| } | |