use crate::{
ata::{get_associated_token_address_with_program, get_token_account_info, TokenProgram},
error::{Result, TallyError},
program_types::{Plan, UpdatePlanArgs},
SimpleTallyClient,
};
use anchor_client::solana_sdk::pubkey::Pubkey;
use std::str::FromStr;
pub fn get_usdc_mint(usdc_mint_str: Option<&str>) -> Result<Pubkey> {
let mint_str = usdc_mint_str.unwrap_or("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"); Pubkey::from_str(mint_str)
.map_err(|e| TallyError::Generic(format!("Invalid USDC mint address '{mint_str}': {e}")))
}
pub fn validate_plan_parameters(price_usdc: u64, period_secs: i64, grace_secs: i64) -> Result<()> {
if price_usdc == 0 {
return Err(TallyError::Generic(
"Plan price must be greater than 0".to_string(),
));
}
if period_secs < 86400 {
return Err(TallyError::Generic(format!(
"Plan period must be at least 24 hours (86400 seconds), got: {period_secs}"
)));
}
if period_secs <= 0 {
return Err(TallyError::Generic(format!(
"Plan period must be positive, got: {period_secs}"
)));
}
if grace_secs < 0 {
return Err(TallyError::Generic(format!(
"Grace period must be non-negative, got: {grace_secs}"
)));
}
let max_grace_secs = 2_i64.saturating_mul(period_secs);
if grace_secs > max_grace_secs {
return Err(TallyError::Generic(format!(
"Grace period ({grace_secs} seconds) cannot exceed 2x the billing period ({period_secs} seconds). Maximum allowed: {max_grace_secs}"
)));
}
Ok(())
}
pub fn validate_platform_fee_bps(fee_bps: u16) -> Result<()> {
if fee_bps > 1000 {
return Err(TallyError::Generic(format!(
"Platform fee basis points must be between 0-1000 (0-10%), got: {fee_bps}"
)));
}
Ok(())
}
pub fn validate_withdrawal_amount(amount: u64) -> Result<()> {
if amount == 0 {
return Err(TallyError::Generic(
"Withdrawal amount must be greater than 0".to_string(),
));
}
Ok(())
}
pub fn validate_usdc_token_account(
tally_client: &SimpleTallyClient,
token_account: &Pubkey,
usdc_mint: &Pubkey,
expected_owner: &Pubkey,
account_type: &str, ) -> Result<()> {
let token_account_info = get_token_account_info(tally_client.rpc(), token_account)?
.ok_or_else(|| {
TallyError::Generic(format!("{account_type} ATA {token_account} does not exist"))
})?;
let (token_account_data, _token_program) = token_account_info;
if token_account_data.mint != *usdc_mint {
return Err(TallyError::Generic(format!(
"{} ATA mint mismatch: expected {}, found {}",
account_type, usdc_mint, token_account_data.mint
)));
}
if token_account_data.owner != *expected_owner {
return Err(TallyError::Generic(format!(
"{} ATA owner mismatch: expected {}, found {}",
account_type, expected_owner, token_account_data.owner
)));
}
let expected_ata =
get_associated_token_address_with_program(expected_owner, usdc_mint, TokenProgram::Token)?;
if *token_account != expected_ata {
return Err(TallyError::Generic(format!(
"{account_type} ATA address mismatch: expected {expected_ata}, provided {token_account}"
)));
}
Ok(())
}
pub fn validate_merchant_authority(authority: &Pubkey, expected_merchant: &Pubkey) -> Result<()> {
let computed_merchant = crate::pda::merchant_address(authority)?;
if *expected_merchant != computed_merchant {
return Err(TallyError::Generic(format!(
"Authority mismatch: expected merchant PDA {computed_merchant} for authority {authority}, but got {expected_merchant}"
)));
}
Ok(())
}
pub fn validate_merchant_authority_with_program_id(
authority: &Pubkey,
expected_merchant: &Pubkey,
program_id: &Pubkey,
) -> Result<()> {
let computed_merchant = crate::pda::merchant_address_with_program_id(authority, program_id);
if *expected_merchant != computed_merchant {
return Err(TallyError::Generic(format!(
"Authority mismatch: expected merchant PDA {computed_merchant} for authority {authority}, but got {expected_merchant}"
)));
}
Ok(())
}
pub fn validate_plan_update_args(
update_args: &UpdatePlanArgs,
current_plan: Option<&Plan>,
) -> Result<()> {
if !update_args.has_updates() {
return Err(TallyError::Generic(
"At least one field must be specified for update".to_string(),
));
}
if let Some(price_usdc) = update_args.price_usdc {
if price_usdc == 0 {
return Err(TallyError::Generic(
"Plan price must be greater than 0".to_string(),
));
}
}
if let Some(period_secs) = update_args.period_secs {
if period_secs < 86400 {
return Err(TallyError::Generic(format!(
"Plan period must be at least 24 hours (86400 seconds), got: {period_secs}"
)));
}
}
if let Some(grace_secs) = update_args.grace_secs {
if grace_secs > 0 {
let period_to_check = if let Some(new_period) = update_args.period_secs {
new_period
} else if let Some(current) = current_plan {
current.period_secs
} else {
return Err(TallyError::Generic(
"Cannot validate grace period without current plan data or new period"
.to_string(),
));
};
let max_grace_secs = 2_u64.saturating_mul(period_to_check);
if grace_secs > max_grace_secs {
return Err(TallyError::Generic(format!(
"Grace period ({grace_secs} seconds) cannot exceed 2x the billing period ({period_to_check} seconds). Maximum allowed: {max_grace_secs}"
)));
}
}
}
if let Some(name) = &update_args.name {
if name.len() > 32 {
return Err(TallyError::Generic(format!(
"Plan name cannot exceed 32 bytes, got: {} bytes",
name.len()
)));
}
if name.is_empty() {
return Err(TallyError::Generic("Plan name cannot be empty".to_string()));
}
}
if let (Some(period_secs), Some(grace_secs)) = (update_args.period_secs, update_args.grace_secs)
{
let max_grace_secs = 2_u64.saturating_mul(period_secs);
if grace_secs > max_grace_secs {
return Err(TallyError::Generic(format!(
"Grace period ({grace_secs} seconds) cannot exceed 2x the billing period ({period_secs} seconds). Maximum allowed: {max_grace_secs}"
)));
}
}
Ok(())
}
pub const fn validate_plan_update_safety(
update_args: &UpdatePlanArgs,
current_plan: &Plan,
has_active_subscriptions: bool,
) -> Result<()> {
if !has_active_subscriptions {
return Ok(());
}
if let Some(new_period) = update_args.period_secs {
if new_period < current_plan.period_secs {
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_plan_parameters() {
assert!(validate_plan_parameters(5_000_000, 2_592_000, 432_000).is_ok());
assert!(validate_plan_parameters(0, 2_592_000, 432_000).is_err());
assert!(validate_plan_parameters(5_000_000, 3600, 432_000).is_err());
assert!(validate_plan_parameters(5_000_000, -1, 432_000).is_err());
assert!(validate_plan_parameters(5_000_000, 2_592_000, -1).is_err());
assert!(validate_plan_parameters(5_000_000, 2_592_000, 6_000_000).is_err());
}
#[test]
fn test_validate_platform_fee_bps() {
assert!(validate_platform_fee_bps(50).is_ok());
assert!(validate_platform_fee_bps(0).is_ok());
assert!(validate_platform_fee_bps(1000).is_ok());
assert!(validate_platform_fee_bps(1001).is_err());
}
#[test]
fn test_validate_withdrawal_amount() {
assert!(validate_withdrawal_amount(1000).is_ok());
assert!(validate_withdrawal_amount(0).is_err());
}
#[test]
fn test_get_usdc_mint() {
let mint = get_usdc_mint(None).unwrap();
assert_eq!(
mint.to_string(),
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"
);
let custom_mint = "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU";
let mint = get_usdc_mint(Some(custom_mint)).unwrap();
assert_eq!(mint.to_string(), custom_mint);
assert!(get_usdc_mint(Some("invalid")).is_err());
}
#[test]
fn test_validate_plan_update_args() {
use crate::program_types::UpdatePlanArgs;
let update_args = UpdatePlanArgs::new().with_name("New Name".to_string());
assert!(validate_plan_update_args(&update_args, None).is_ok());
let update_args = UpdatePlanArgs::new()
.with_name("New Name".to_string())
.with_active(true)
.with_price_usdc(5_000_000)
.with_period_secs(86400)
.with_grace_secs(3600);
assert!(validate_plan_update_args(&update_args, None).is_ok());
let empty_args = UpdatePlanArgs::new();
assert!(validate_plan_update_args(&empty_args, None).is_err());
let invalid_price = UpdatePlanArgs::new().with_price_usdc(0);
assert!(validate_plan_update_args(&invalid_price, None).is_err());
let invalid_period = UpdatePlanArgs::new().with_period_secs(3600); assert!(validate_plan_update_args(&invalid_period, None).is_err());
let long_name = "a".repeat(33);
let invalid_name = UpdatePlanArgs::new().with_name(long_name);
assert!(validate_plan_update_args(&invalid_name, None).is_err());
let empty_name = UpdatePlanArgs::new().with_name(String::new());
assert!(validate_plan_update_args(&empty_name, None).is_err());
}
#[test]
fn test_validate_plan_update_args_with_current_plan() {
use crate::program_types::{Plan, UpdatePlanArgs};
use anchor_client::solana_sdk::signature::{Keypair, Signer};
let current_plan = Plan {
merchant: Pubkey::from(Keypair::new().pubkey().to_bytes()),
plan_id: [0u8; 32],
price_usdc: 5_000_000,
period_secs: 86400, grace_secs: 3600, name: [0u8; 32],
active: true,
};
let valid_grace = UpdatePlanArgs::new().with_grace_secs(172_800); assert!(validate_plan_update_args(&valid_grace, Some(¤t_plan)).is_ok());
let invalid_grace = UpdatePlanArgs::new().with_grace_secs(172_801); assert!(validate_plan_update_args(&invalid_grace, Some(¤t_plan)).is_err());
let valid_combo = UpdatePlanArgs::new()
.with_period_secs(172_800) .with_grace_secs(345_600); assert!(validate_plan_update_args(&valid_combo, Some(¤t_plan)).is_ok());
let invalid_combo = UpdatePlanArgs::new()
.with_period_secs(86400) .with_grace_secs(345_600); assert!(validate_plan_update_args(&invalid_combo, Some(¤t_plan)).is_err());
}
#[test]
fn test_validate_plan_update_safety() {
use crate::program_types::{Plan, UpdatePlanArgs};
use anchor_client::solana_sdk::signature::{Keypair, Signer};
let current_plan = Plan {
merchant: Pubkey::from(Keypair::new().pubkey().to_bytes()),
plan_id: [0u8; 32],
price_usdc: 5_000_000,
period_secs: 86400, grace_secs: 3600, name: [0u8; 32],
active: true,
};
let price_increase = UpdatePlanArgs::new().with_price_usdc(10_000_000);
assert!(validate_plan_update_safety(&price_increase, ¤t_plan, false).is_ok());
assert!(validate_plan_update_safety(&price_increase, ¤t_plan, true).is_ok());
let period_change = UpdatePlanArgs::new().with_period_secs(172_800); assert!(validate_plan_update_safety(&period_change, ¤t_plan, true).is_ok());
let shorter_period = UpdatePlanArgs::new().with_period_secs(43200); assert!(validate_plan_update_args(&shorter_period, Some(¤t_plan)).is_err());
let grace_change = UpdatePlanArgs::new().with_grace_secs(7200); assert!(validate_plan_update_safety(&grace_change, ¤t_plan, true).is_ok());
let name_change = UpdatePlanArgs::new().with_name("New Name".to_string());
let active_change = UpdatePlanArgs::new().with_active(false);
assert!(validate_plan_update_safety(&name_change, ¤t_plan, true).is_ok());
assert!(validate_plan_update_safety(&active_change, ¤t_plan, true).is_ok());
}
}