use anchor_lang::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MerchantTier {
Free = 0,
Pro = 1,
Enterprise = 2,
}
impl MerchantTier {
#[must_use]
pub const fn fee_bps(self) -> u16 {
match self {
Self::Free => 200, Self::Pro => 150, Self::Enterprise => 100, }
}
#[must_use]
pub const fn from_discriminant(value: u8) -> Option<Self> {
match value {
0 => Some(Self::Free),
1 => Some(Self::Pro),
2 => Some(Self::Enterprise),
_ => None,
}
}
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Merchant {
pub authority: Pubkey,
pub usdc_mint: Pubkey,
pub treasury_ata: Pubkey,
pub platform_fee_bps: u16,
pub tier: u8,
pub bump: u8,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Plan {
pub merchant: Pubkey,
pub plan_id: [u8; 32],
pub price_usdc: u64,
pub period_secs: u64,
pub grace_secs: u64,
pub name: [u8; 32],
pub active: bool,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Subscription {
pub plan: Pubkey,
pub subscriber: Pubkey,
pub next_renewal_ts: i64,
pub active: bool,
pub renewals: u32,
pub created_ts: i64,
pub last_amount: u64,
pub last_renewed_ts: i64,
pub trial_ends_at: Option<i64>,
pub in_trial: bool,
pub bump: u8,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct InitMerchantArgs {
pub usdc_mint: Pubkey,
pub treasury_ata: Pubkey,
pub platform_fee_bps: u16,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct CreatePlanArgs {
pub plan_id: String,
pub plan_id_bytes: [u8; 32],
pub price_usdc: u64,
pub period_secs: u64,
pub grace_secs: u64,
pub name: String,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct StartSubscriptionArgs {
pub allowance_periods: u8,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct RenewSubscriptionArgs {
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct UpdatePlanArgs {
pub name: Option<String>,
pub active: Option<bool>,
pub price_usdc: Option<u64>,
pub period_secs: Option<u64>,
pub grace_secs: Option<u64>,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct CancelSubscriptionArgs;
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct AdminWithdrawFeesArgs {
pub amount: u64,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct Config {
pub platform_authority: Pubkey,
pub pending_authority: Option<Pubkey>,
pub max_platform_fee_bps: u16,
pub min_platform_fee_bps: u16,
pub min_period_seconds: u64,
pub default_allowance_periods: u8,
pub allowed_mint: Pubkey,
pub max_withdrawal_amount: u64,
pub max_grace_period_seconds: u64,
pub paused: bool,
pub keeper_fee_bps: u16,
pub bump: u8,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct InitConfigArgs {
pub platform_authority: Pubkey,
pub max_platform_fee_bps: u16,
pub fee_basis_points_divisor: u16,
pub min_period_seconds: u64,
pub default_allowance_periods: u8,
}
impl Plan {
#[must_use]
pub fn plan_id_str(&self) -> String {
String::from_utf8_lossy(&self.plan_id)
.trim_end_matches('\0')
.to_string()
}
#[must_use]
pub fn name_str(&self) -> String {
String::from_utf8_lossy(&self.name)
.trim_end_matches('\0')
.to_string()
}
#[must_use]
pub fn plan_id_string(&self) -> String {
self.plan_id_str()
}
#[must_use]
pub fn name_string(&self) -> String {
self.name_str()
}
#[must_use]
#[allow(clippy::cast_precision_loss)]
pub fn price_usdc_formatted(&self) -> f64 {
self.price_usdc as f64 / 1_000_000.0
}
#[must_use]
pub fn period_formatted(&self) -> String {
let days = self.period_secs / 86400;
if days == 1 {
"1 day".to_string()
} else if days == 7 {
"1 week".to_string()
} else if days == 30 {
"1 month".to_string()
} else if days == 365 {
"1 year".to_string()
} else {
format!("{days} days")
}
}
}
impl CreatePlanArgs {
#[must_use]
pub fn plan_id_bytes(&self) -> [u8; 32] {
let mut bytes = [0u8; 32];
let id_bytes = self.plan_id.as_bytes();
let len = id_bytes.len().min(32);
bytes[..len].copy_from_slice(&id_bytes[..len]);
bytes
}
#[must_use]
pub fn name_bytes(&self) -> [u8; 32] {
let mut bytes = [0u8; 32];
let name_bytes = self.name.as_bytes();
let len = name_bytes.len().min(32);
bytes[..len].copy_from_slice(&name_bytes[..len]);
bytes
}
}
impl UpdatePlanArgs {
#[must_use]
pub const fn new() -> Self {
Self {
name: None,
active: None,
price_usdc: None,
period_secs: None,
grace_secs: None,
}
}
#[must_use]
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub const fn with_active(mut self, active: bool) -> Self {
self.active = Some(active);
self
}
#[must_use]
pub const fn with_price_usdc(mut self, price_usdc: u64) -> Self {
self.price_usdc = Some(price_usdc);
self
}
#[must_use]
pub const fn with_period_secs(mut self, period_secs: u64) -> Self {
self.period_secs = Some(period_secs);
self
}
#[must_use]
pub const fn with_grace_secs(mut self, grace_secs: u64) -> Self {
self.grace_secs = Some(grace_secs);
self
}
#[must_use]
pub const fn has_updates(&self) -> bool {
self.name.is_some()
|| self.active.is_some()
|| self.price_usdc.is_some()
|| self.period_secs.is_some()
|| self.grace_secs.is_some()
}
#[must_use]
pub fn name_bytes(&self) -> Option<[u8; 32]> {
self.name.as_ref().map(|name| {
let mut bytes = [0u8; 32];
let name_bytes = name.as_bytes();
let len = name_bytes.len().min(32);
bytes[..len].copy_from_slice(&name_bytes[..len]);
bytes
})
}
}
impl Default for UpdatePlanArgs {
fn default() -> Self {
Self::new()
}
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct CloseSubscriptionArgs {
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct TransferAuthorityArgs {
pub new_authority: Pubkey,
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct AcceptAuthorityArgs {
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct CancelAuthorityTransferArgs {
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct PauseArgs {}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct UnpauseArgs {}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct UpdateConfigArgs {
pub keeper_fee_bps: Option<u16>,
pub max_withdrawal_amount: Option<u64>,
pub max_grace_period_seconds: Option<u64>,
pub min_platform_fee_bps: Option<u16>,
pub max_platform_fee_bps: Option<u16>,
pub min_period_seconds: Option<u64>,
pub default_allowance_periods: Option<u8>,
}
#[derive(
Clone, Debug, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct UpdateMerchantTierArgs {
pub new_tier: u8,
}
#[derive(
Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, AnchorSerialize, AnchorDeserialize,
)]
pub struct UpdatePlanTermsArgs {
pub price_usdc: Option<u64>,
pub period_secs: Option<u64>,
pub grace_secs: Option<u64>,
pub name: Option<String>,
}
impl UpdatePlanTermsArgs {
#[must_use]
pub const fn new() -> Self {
Self {
price_usdc: None,
period_secs: None,
grace_secs: None,
name: None,
}
}
#[must_use]
pub const fn with_price_usdc(mut self, price_usdc: u64) -> Self {
self.price_usdc = Some(price_usdc);
self
}
#[must_use]
pub const fn with_period_secs(mut self, period_secs: u64) -> Self {
self.period_secs = Some(period_secs);
self
}
#[must_use]
pub const fn with_grace_secs(mut self, grace_secs: u64) -> Self {
self.grace_secs = Some(grace_secs);
self
}
#[must_use]
pub fn with_name(mut self, name: String) -> Self {
self.name = Some(name);
self
}
#[must_use]
pub const fn has_updates(&self) -> bool {
self.price_usdc.is_some()
|| self.period_secs.is_some()
|| self.grace_secs.is_some()
|| self.name.is_some()
}
#[must_use]
pub fn name_bytes(&self) -> Option<[u8; 32]> {
self.name.as_ref().map(|name| {
let mut bytes = [0u8; 32];
let name_bytes = name.as_bytes();
let len = name_bytes.len().min(32);
bytes[..len].copy_from_slice(&name_bytes[..len]);
bytes
})
}
}