SHiNE-server/shine-solana/shine/programs/shine_payments/src/lib.rs

1331 lines
48 KiB
Rust

use solana_program::{
account_info::{next_account_info, AccountInfo},
clock::Clock,
entrypoint,
entrypoint::ProgramResult,
program::{invoke, invoke_signed},
program_error::ProgramError,
pubkey::Pubkey,
rent::Rent,
system_instruction, system_program,
sysvar::Sysvar,
};
use std::str::FromStr;
pub mod settings;
solana_program::declare_id!("c4yTa4JT9EtQDCBX9LmWFK6T2gp4JGsuymFbom2EudW");
entrypoint!(process_instruction);
const IX_INIT: u8 = 1;
const IX_UPDATE_COEF_LIMIT: u8 = 2;
const IX_GRANT_MANAGER_LIMITS: u8 = 3;
const IX_BUY_TICKET: u8 = 4;
const IX_BUY_TICKET_USD: u8 = 5;
const IX_BUY_TICKET_SOL: u8 = 6;
const IX_MANAGER_ADD_TICKET: u8 = 7;
const IX_STEP_PAYOUT: u8 = 8;
const IX_CHANGE_TICKET_RECIPIENT: u8 = 9;
#[repr(u32)]
#[derive(Clone, Copy, Debug)]
enum PaymentsError {
InvalidInstruction = 1,
InvalidSigner = 2,
InvalidPdaAddress = 3,
EmptyState = 4,
InvalidAccountData = 5,
InvalidSettingsWallet = 6,
InvalidInflowVault = 7,
InvalidDaoWallet = 8,
UnauthorizedManager = 9,
UnauthorizedDao = 10,
InvalidCoefficient = 11,
InvalidLimit = 12,
InvalidCallReward = 13,
InvalidAmount = 14,
QueueTemporarilyPaused = 15,
InvalidPayoutAmount = 16,
NotEnoughInflowForStep = 17,
TicketAlreadyPaid = 18,
InvalidTicketRecipient = 19,
InvalidTicketIndex = 20,
InvalidTicketQueue = 21,
InvalidManagerWallet = 22,
ManagerLimitExceeded = 23,
UnauthorizedTicketOwner = 24,
CannotChangeRecipientForNextPayoutTicket = 25,
InvalidOracleAccount = 26,
InvalidOracleFeed = 27,
InvalidOracleFeedConfig = 28,
OraclePriceTooOld = 29,
InvalidOraclePrice = 30,
SlippageExceeded = 31,
MathOverflow = 32,
SystemAlreadyInitialized = 33,
MissingRequiredSignature = 34,
InvalidSystemProgram = 35,
PdaAlreadyExists = 36,
}
impl From<PaymentsError> for ProgramError {
fn from(value: PaymentsError) -> Self {
ProgramError::Custom(value as u32)
}
}
macro_rules! require {
($cond:expr, $err:expr) => {
if !($cond) {
return Err(ProgramError::from($err));
}
};
}
macro_rules! require_keys_eq {
($left:expr, $right:expr, $err:expr) => {
if $left != $right {
return Err(ProgramError::from($err));
}
};
}
#[derive(Clone, Debug)]
pub struct UpdateCoefLimitArgs {
pub coef_ppm: u64,
pub limit_usd_cents: u64,
pub call_reward_lamports: u64,
}
#[derive(Clone, Debug)]
pub struct GrantManagerLimitsArgs {
pub manager_wallet: Pubkey,
pub add_q1_usd_cents: u64,
pub add_q2_usd_cents: u64,
pub add_q3_usd_cents: u64,
}
#[derive(Clone, Debug)]
pub struct BuyTicketArgs {
pub amount_lamports: u64,
pub recipient_wallet: Pubkey,
}
#[derive(Clone, Debug)]
pub struct BuyTicketUsdArgs {
pub amount_usd_cents: u64,
pub max_pay_lamports: u64,
pub recipient_wallet: Pubkey,
}
#[derive(Clone, Debug)]
pub struct BuyTicketSolArgs {
pub amount_lamports: u64,
pub min_expected_usd_cents: u64,
pub recipient_wallet: Pubkey,
}
#[derive(Clone, Debug)]
pub struct ManagerAddTicketArgs {
pub queue_id: u8,
pub recipient_wallet: Pubkey,
pub payout_usd_cents: u64,
}
#[derive(Clone, Debug)]
pub struct ChangeTicketRecipientArgs {
pub new_recipient_wallet: Pubkey,
}
#[derive(Clone, Debug)]
pub struct ConfigState {
pub version: u8,
pub dao_wallet: Pubkey,
pub inflow_vault: Pubkey,
}
#[derive(Clone, Debug)]
pub struct CoefLimitState {
pub version: u8,
pub coef_ppm: u64,
pub limit_usd_cents: u64,
pub call_reward_lamports: u64,
}
#[derive(Clone, Debug)]
pub struct QueuesState {
pub version: u8,
pub q1_tickets_total: u64,
pub q1_tickets_paid: u64,
pub q1_sum_total_usd_cents: u64,
pub q1_sum_paid_usd_cents: u64,
pub q2_tickets_total: u64,
pub q2_tickets_paid: u64,
pub q2_sum_total_usd_cents: u64,
pub q2_sum_paid_usd_cents: u64,
pub q3_tickets_total: u64,
pub q3_tickets_paid: u64,
pub q3_sum_total_usd_cents: u64,
pub q3_sum_paid_usd_cents: u64,
}
#[derive(Clone, Debug)]
pub struct TicketState {
pub version: u8,
pub queue_id: u8,
pub index: u64,
pub is_paid: bool,
pub recipient_wallet: Pubkey,
pub payout_usd_cents: u64,
pub debt_before_usd_cents: u64,
}
#[derive(Clone, Debug)]
pub struct ManagerAllowanceState {
pub version: u8,
pub manager_wallet: Pubkey,
pub q1_available_usd_cents: u64,
pub q2_available_usd_cents: u64,
pub q3_available_usd_cents: u64,
}
#[derive(Clone, Debug)]
pub struct VaultState {
pub version: u8,
}
struct SolUsdPrice {
price_num: u128,
price_den: u128,
}
struct Reader<'a> {
data: &'a [u8],
cursor: usize,
error: ProgramError,
}
impl<'a> Reader<'a> {
fn new(data: &'a [u8], error: ProgramError) -> Self {
Self { data, cursor: 0, error }
}
fn read_u8(&mut self) -> Result<u8, ProgramError> {
let value = *self
.data
.get(self.cursor)
.ok_or(self.error.clone())?;
self.cursor += 1;
Ok(value)
}
fn read_u64(&mut self) -> Result<u64, ProgramError> {
let end = self.cursor.checked_add(8).ok_or(self.error.clone())?;
let slice = self
.data
.get(self.cursor..end)
.ok_or(self.error.clone())?;
self.cursor = end;
Ok(u64::from_le_bytes(
slice.try_into().map_err(|_| self.error.clone())?,
))
}
fn read_pubkey(&mut self) -> Result<Pubkey, ProgramError> {
let end = self.cursor.checked_add(32).ok_or(self.error.clone())?;
let slice = self
.data
.get(self.cursor..end)
.ok_or(self.error.clone())?;
self.cursor = end;
Ok(Pubkey::new_from_array(
slice.try_into().map_err(|_| self.error.clone())?,
))
}
fn finish(self) -> Result<(), ProgramError> {
require!(self.cursor == self.data.len(), PaymentsError::InvalidInstruction);
Ok(())
}
}
trait StateCodec: Sized {
fn encoded_len() -> usize;
fn encode(&self) -> Vec<u8>;
fn decode(data: &[u8]) -> Result<Self, ProgramError>;
}
impl StateCodec for ConfigState {
fn encoded_len() -> usize {
1 + 32 + 32
}
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(65);
out.push(self.version);
out.extend_from_slice(self.dao_wallet.as_ref());
out.extend_from_slice(self.inflow_vault.as_ref());
out
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
let version = reader.read_u8()?;
let dao_wallet = reader.read_pubkey()?;
let inflow_vault = reader.read_pubkey()?;
require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
Ok(Self { version, dao_wallet, inflow_vault })
}
}
impl StateCodec for CoefLimitState {
fn encoded_len() -> usize {
1 + 8 + 8 + 8
}
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(25);
out.push(self.version);
out.extend_from_slice(&self.coef_ppm.to_le_bytes());
out.extend_from_slice(&self.limit_usd_cents.to_le_bytes());
out.extend_from_slice(&self.call_reward_lamports.to_le_bytes());
out
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
let version = reader.read_u8()?;
let coef_ppm = reader.read_u64()?;
let limit_usd_cents = reader.read_u64()?;
let call_reward_lamports = reader.read_u64()?;
require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
Ok(Self { version, coef_ppm, limit_usd_cents, call_reward_lamports })
}
}
impl StateCodec for QueuesState {
fn encoded_len() -> usize {
1 + 12 * 8
}
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(97);
out.push(self.version);
for value in [
self.q1_tickets_total,
self.q1_tickets_paid,
self.q1_sum_total_usd_cents,
self.q1_sum_paid_usd_cents,
self.q2_tickets_total,
self.q2_tickets_paid,
self.q2_sum_total_usd_cents,
self.q2_sum_paid_usd_cents,
self.q3_tickets_total,
self.q3_tickets_paid,
self.q3_sum_total_usd_cents,
self.q3_sum_paid_usd_cents,
] {
out.extend_from_slice(&value.to_le_bytes());
}
out
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
let version = reader.read_u8()?;
Ok(Self {
version,
q1_tickets_total: reader.read_u64()?,
q1_tickets_paid: reader.read_u64()?,
q1_sum_total_usd_cents: reader.read_u64()?,
q1_sum_paid_usd_cents: reader.read_u64()?,
q2_tickets_total: reader.read_u64()?,
q2_tickets_paid: reader.read_u64()?,
q2_sum_total_usd_cents: reader.read_u64()?,
q2_sum_paid_usd_cents: reader.read_u64()?,
q3_tickets_total: reader.read_u64()?,
q3_tickets_paid: reader.read_u64()?,
q3_sum_total_usd_cents: reader.read_u64()?,
q3_sum_paid_usd_cents: reader.read_u64()?,
})
}
}
impl StateCodec for TicketState {
fn encoded_len() -> usize {
1 + 1 + 8 + 1 + 32 + 8 + 8
}
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(59);
out.push(self.version);
out.push(self.queue_id);
out.extend_from_slice(&self.index.to_le_bytes());
out.push(u8::from(self.is_paid));
out.extend_from_slice(self.recipient_wallet.as_ref());
out.extend_from_slice(&self.payout_usd_cents.to_le_bytes());
out.extend_from_slice(&self.debt_before_usd_cents.to_le_bytes());
out
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
let version = reader.read_u8()?;
let queue_id = reader.read_u8()?;
let index = reader.read_u64()?;
let is_paid = match reader.read_u8()? {
0 => false,
1 => true,
_ => return Err(PaymentsError::InvalidAccountData.into()),
};
let recipient_wallet = reader.read_pubkey()?;
let payout_usd_cents = reader.read_u64()?;
let debt_before_usd_cents = reader.read_u64()?;
require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
Ok(Self { version, queue_id, index, is_paid, recipient_wallet, payout_usd_cents, debt_before_usd_cents })
}
}
impl StateCodec for ManagerAllowanceState {
fn encoded_len() -> usize {
1 + 32 + 8 + 8 + 8
}
fn encode(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(57);
out.push(self.version);
out.extend_from_slice(self.manager_wallet.as_ref());
out.extend_from_slice(&self.q1_available_usd_cents.to_le_bytes());
out.extend_from_slice(&self.q2_available_usd_cents.to_le_bytes());
out.extend_from_slice(&self.q3_available_usd_cents.to_le_bytes());
out
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
let mut reader = Reader::new(data, PaymentsError::InvalidAccountData.into());
let version = reader.read_u8()?;
let manager_wallet = reader.read_pubkey()?;
let q1_available_usd_cents = reader.read_u64()?;
let q2_available_usd_cents = reader.read_u64()?;
let q3_available_usd_cents = reader.read_u64()?;
require!(reader.cursor == data.len(), PaymentsError::InvalidAccountData);
Ok(Self { version, manager_wallet, q1_available_usd_cents, q2_available_usd_cents, q3_available_usd_cents })
}
}
impl StateCodec for VaultState {
fn encoded_len() -> usize {
1
}
fn encode(&self) -> Vec<u8> {
vec![self.version]
}
fn decode(data: &[u8]) -> Result<Self, ProgramError> {
require!(data.len() == 1, PaymentsError::InvalidAccountData);
Ok(Self { version: data[0] })
}
}
#[derive(Clone, Debug)]
enum Instruction {
Init,
UpdateCoefLimit(UpdateCoefLimitArgs),
GrantManagerLimits(GrantManagerLimitsArgs),
BuyTicket(BuyTicketArgs),
BuyTicketUsd(BuyTicketUsdArgs),
BuyTicketSol(BuyTicketSolArgs),
ManagerAddTicket(ManagerAddTicketArgs),
StepPayout,
ChangeTicketRecipient(ChangeTicketRecipientArgs),
}
pub fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
match parse_instruction(instruction_data)? {
Instruction::Init => process_init(program_id, accounts),
Instruction::UpdateCoefLimit(args) => process_update_coef_limit(program_id, accounts, args),
Instruction::GrantManagerLimits(args) => process_grant_manager_limits(program_id, accounts, args),
Instruction::BuyTicket(args) => process_buy_ticket(program_id, accounts, args),
Instruction::BuyTicketUsd(args) => process_buy_ticket_usd(program_id, accounts, args),
Instruction::BuyTicketSol(args) => process_buy_ticket_sol(program_id, accounts, args),
Instruction::ManagerAddTicket(args) => process_manager_add_ticket(program_id, accounts, args),
Instruction::StepPayout => process_step_payout(program_id, accounts),
Instruction::ChangeTicketRecipient(args) => process_change_ticket_recipient(program_id, accounts, args),
}
}
fn parse_instruction(data: &[u8]) -> Result<Instruction, ProgramError> {
let (&tag, rest) = data
.split_first()
.ok_or(ProgramError::InvalidInstructionData)?;
let mut reader = Reader::new(rest, ProgramError::InvalidInstructionData);
let instruction = match tag {
IX_INIT => Instruction::Init,
IX_UPDATE_COEF_LIMIT => Instruction::UpdateCoefLimit(UpdateCoefLimitArgs {
coef_ppm: reader.read_u64()?,
limit_usd_cents: reader.read_u64()?,
call_reward_lamports: reader.read_u64()?,
}),
IX_GRANT_MANAGER_LIMITS => Instruction::GrantManagerLimits(GrantManagerLimitsArgs {
manager_wallet: reader.read_pubkey()?,
add_q1_usd_cents: reader.read_u64()?,
add_q2_usd_cents: reader.read_u64()?,
add_q3_usd_cents: reader.read_u64()?,
}),
IX_BUY_TICKET => Instruction::BuyTicket(BuyTicketArgs {
amount_lamports: reader.read_u64()?,
recipient_wallet: reader.read_pubkey()?,
}),
IX_BUY_TICKET_USD => Instruction::BuyTicketUsd(BuyTicketUsdArgs {
amount_usd_cents: reader.read_u64()?,
max_pay_lamports: reader.read_u64()?,
recipient_wallet: reader.read_pubkey()?,
}),
IX_BUY_TICKET_SOL => Instruction::BuyTicketSol(BuyTicketSolArgs {
amount_lamports: reader.read_u64()?,
min_expected_usd_cents: reader.read_u64()?,
recipient_wallet: reader.read_pubkey()?,
}),
IX_MANAGER_ADD_TICKET => Instruction::ManagerAddTicket(ManagerAddTicketArgs {
queue_id: reader.read_u8()?,
recipient_wallet: reader.read_pubkey()?,
payout_usd_cents: reader.read_u64()?,
}),
IX_STEP_PAYOUT => Instruction::StepPayout,
IX_CHANGE_TICKET_RECIPIENT => Instruction::ChangeTicketRecipient(ChangeTicketRecipientArgs {
new_recipient_wallet: reader.read_pubkey()?,
}),
_ => return Err(ProgramError::InvalidInstructionData),
};
reader.finish()?;
Ok(instruction)
}
fn process_init(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let payer = next_signer_account(account_iter)?;
let config_pda = next_account_info(account_iter)?;
let coef_limit_pda = next_account_info(account_iter)?;
let queues_pda = next_account_info(account_iter)?;
let inflow_vault_pda = next_account_info(account_iter)?;
let system_program_ai = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
require_system_program(system_program_ai)?;
ensure_expected_pdas(program_id, config_pda, coef_limit_pda, queues_pda, inflow_vault_pda)?;
require!(is_uninitialized_account(config_pda), PaymentsError::SystemAlreadyInitialized);
require!(is_uninitialized_account(coef_limit_pda), PaymentsError::SystemAlreadyInitialized);
require!(is_uninitialized_account(queues_pda), PaymentsError::SystemAlreadyInitialized);
require!(is_uninitialized_account(inflow_vault_pda), PaymentsError::SystemAlreadyInitialized);
let dao_wallet = Pubkey::from_str(settings::DAO_WALLET)
.map_err(|_| ProgramError::from(PaymentsError::InvalidSettingsWallet))?;
let config = ConfigState {
version: 1,
dao_wallet,
inflow_vault: *inflow_vault_pda.key,
};
create_and_store_state(
program_id,
payer,
system_program_ai,
config_pda,
settings::CONFIG_SEED,
settings::CONFIG_SPACE,
&config,
)?;
let coef_limit = CoefLimitState {
version: 1,
coef_ppm: settings::START_COEF_PPM,
limit_usd_cents: settings::START_LIMIT_USD_CENTS,
call_reward_lamports: settings::START_CALL_REWARD_LAMPORTS,
};
create_and_store_state(
program_id,
payer,
system_program_ai,
coef_limit_pda,
settings::COEF_LIMIT_SEED,
settings::COEF_LIMIT_SPACE,
&coef_limit,
)?;
let queues = QueuesState {
version: 1,
q1_tickets_total: 0,
q1_tickets_paid: 0,
q1_sum_total_usd_cents: 0,
q1_sum_paid_usd_cents: 0,
q2_tickets_total: 0,
q2_tickets_paid: 0,
q2_sum_total_usd_cents: 0,
q2_sum_paid_usd_cents: 0,
q3_tickets_total: 0,
q3_tickets_paid: 0,
q3_sum_total_usd_cents: 0,
q3_sum_paid_usd_cents: 0,
};
create_and_store_state(
program_id,
payer,
system_program_ai,
queues_pda,
settings::QUEUES_SEED,
settings::QUEUES_SPACE,
&queues,
)?;
let vault = VaultState { version: 1 };
create_and_store_state(
program_id,
payer,
system_program_ai,
inflow_vault_pda,
settings::INFLOW_VAULT_SEED,
settings::INFLOW_VAULT_SPACE,
&vault,
)?;
Ok(())
}
fn process_update_coef_limit(
_program_id: &Pubkey,
accounts: &[AccountInfo],
args: UpdateCoefLimitArgs,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let config_pda = next_account_info(account_iter)?;
let coef_limit_pda = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
let config = read_state::<ConfigState>(config_pda)?;
require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao);
require!(args.coef_ppm > 0, PaymentsError::InvalidCoefficient);
require!(args.limit_usd_cents > 0, PaymentsError::InvalidLimit);
require!(
args.call_reward_lamports <= settings::MAX_CALL_REWARD_LAMPORTS,
PaymentsError::InvalidCallReward
);
let mut coef_limit = read_state::<CoefLimitState>(coef_limit_pda)?;
coef_limit.coef_ppm = args.coef_ppm;
coef_limit.limit_usd_cents = args.limit_usd_cents;
coef_limit.call_reward_lamports = args.call_reward_lamports;
write_state(coef_limit_pda, &coef_limit)
}
fn process_grant_manager_limits(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: GrantManagerLimitsArgs,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let config_pda = next_account_info(account_iter)?;
let manager_allowance_pda = next_account_info(account_iter)?;
let system_program_ai = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
require_system_program(system_program_ai)?;
let config = read_state::<ConfigState>(config_pda)?;
require_keys_eq!(config.dao_wallet, *signer.key, PaymentsError::UnauthorizedDao);
require!(
args.add_q1_usd_cents > 0 || args.add_q2_usd_cents > 0 || args.add_q3_usd_cents > 0,
PaymentsError::InvalidAmount
);
let (expected_pda, bump) = find_manager_allowance_pda(program_id, &args.manager_wallet);
require_keys_eq!(expected_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);
let mut state = if is_uninitialized_account(manager_allowance_pda) {
let initial = ManagerAllowanceState {
version: 1,
manager_wallet: args.manager_wallet,
q1_available_usd_cents: 0,
q2_available_usd_cents: 0,
q3_available_usd_cents: 0,
};
create_state_with_seeds(
program_id,
signer,
system_program_ai,
manager_allowance_pda,
&[
settings::MANAGER_ALLOWANCE_SEED,
args.manager_wallet.as_ref(),
&[bump],
],
settings::MANAGER_ALLOWANCE_SPACE,
&initial,
)?;
initial
} else {
read_state::<ManagerAllowanceState>(manager_allowance_pda)?
};
require_keys_eq!(state.manager_wallet, args.manager_wallet, PaymentsError::InvalidManagerWallet);
state.q1_available_usd_cents = checked_add(state.q1_available_usd_cents, args.add_q1_usd_cents)?;
state.q2_available_usd_cents = checked_add(state.q2_available_usd_cents, args.add_q2_usd_cents)?;
state.q3_available_usd_cents = checked_add(state.q3_available_usd_cents, args.add_q3_usd_cents)?;
write_state(manager_allowance_pda, &state)
}
fn process_buy_ticket(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: BuyTicketArgs,
) -> ProgramResult {
let ctx = BuyTicketAccounts::parse(accounts)?;
let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?;
require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount);
buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet)
}
fn process_buy_ticket_usd(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: BuyTicketUsdArgs,
) -> ProgramResult {
let ctx = BuyTicketAccounts::parse(accounts)?;
require!(args.amount_usd_cents > 0, PaymentsError::InvalidAmount);
require!(args.max_pay_lamports > 0, PaymentsError::InvalidAmount);
let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
let pay_lamports = usd_cents_to_lamports_ceil(args.amount_usd_cents, &sol_usd)?;
require!(pay_lamports > 0, PaymentsError::InvalidAmount);
require!(pay_lamports <= args.max_pay_lamports, PaymentsError::SlippageExceeded);
buy_ticket_by_purchase_usd(program_id, &ctx, args.amount_usd_cents, pay_lamports, args.recipient_wallet)
}
fn process_buy_ticket_sol(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: BuyTicketSolArgs,
) -> ProgramResult {
let ctx = BuyTicketAccounts::parse(accounts)?;
require!(args.amount_lamports > 0, PaymentsError::InvalidAmount);
let sol_usd = read_sol_usd_price(ctx.sol_usd_price_update, ctx.sol_usd_price_update.key)?;
let purchase_usd_cents = lamports_to_usd_cents_floor(args.amount_lamports, &sol_usd)?;
require!(purchase_usd_cents > 0, PaymentsError::InvalidAmount);
require!(purchase_usd_cents >= args.min_expected_usd_cents, PaymentsError::SlippageExceeded);
buy_ticket_by_purchase_usd(program_id, &ctx, purchase_usd_cents, args.amount_lamports, args.recipient_wallet)
}
fn process_manager_add_ticket(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: ManagerAddTicketArgs,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let manager_allowance_pda = next_account_info(account_iter)?;
let queues_pda = next_account_info(account_iter)?;
let ticket_pda = next_account_info(account_iter)?;
let system_program_ai = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
require_system_program(system_program_ai)?;
require!(args.payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);
require!(is_valid_queue_id(args.queue_id), PaymentsError::InvalidTicketQueue);
let (expected_manager_pda, _) = find_manager_allowance_pda(program_id, signer.key);
require_keys_eq!(expected_manager_pda, *manager_allowance_pda.key, PaymentsError::InvalidPdaAddress);
let mut allowance = read_state::<ManagerAllowanceState>(manager_allowance_pda)?;
require_keys_eq!(allowance.manager_wallet, *signer.key, PaymentsError::InvalidManagerWallet);
let mut queues = read_state::<QueuesState>(queues_pda)?;
let debt_before_total = queue_sum_total(&queues, args.queue_id);
require!(queue_allowance(&allowance, args.queue_id) >= args.payout_usd_cents, PaymentsError::ManagerLimitExceeded);
let ticket_index = checked_add(queue_total(&queues, args.queue_id), 1)?;
let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, args.queue_id, ticket_index);
require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress);
require!(is_uninitialized_account(ticket_pda), PaymentsError::PdaAlreadyExists);
let ticket = TicketState {
version: 1,
queue_id: args.queue_id,
index: ticket_index,
is_paid: false,
recipient_wallet: args.recipient_wallet,
payout_usd_cents: args.payout_usd_cents,
debt_before_usd_cents: debt_before_total,
};
create_state_with_seeds(
program_id,
signer,
system_program_ai,
ticket_pda,
&[
queue_seed(args.queue_id),
&ticket_index.to_le_bytes(),
&[ticket_bump],
],
settings::TICKET_SPACE,
&ticket,
)?;
sub_queue_allowance(&mut allowance, args.queue_id, args.payout_usd_cents)?;
set_queue_total(&mut queues, args.queue_id, ticket_index);
add_queue_sum_total(&mut queues, args.queue_id, args.payout_usd_cents)?;
write_state(manager_allowance_pda, &allowance)?;
write_state(queues_pda, &queues)
}
fn process_step_payout(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let config_pda = next_account_info(account_iter)?;
let queues_pda = next_account_info(account_iter)?;
let coef_limit_pda = next_account_info(account_iter)?;
let inflow_vault_pda = next_account_info(account_iter)?;
let next_ticket_pda = next_account_info(account_iter)?;
let ticket_recipient_wallet = next_account_info(account_iter)?;
let dao_wallet = next_account_info(account_iter)?;
let sol_usd_price_update = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
let config = read_state::<ConfigState>(config_pda)?;
let coef_limit = read_state::<CoefLimitState>(coef_limit_pda)?;
let mut queues = read_state::<QueuesState>(queues_pda)?;
let _vault_state = read_state::<VaultState>(inflow_vault_pda)?;
require_keys_eq!(*dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet);
require_keys_eq!(*inflow_vault_pda.key, config.inflow_vault, PaymentsError::InvalidInflowVault);
let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?;
let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?;
let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?;
if q1_pending == 0 && q2_pending == 0 && q3_pending == 0 {
transfer_all_available_to_dao(inflow_vault_pda, dao_wallet)?;
return Ok(());
}
let (target_queue, next_index) = if q1_pending > 0 {
(1, checked_add(queues.q1_tickets_paid, 1)?)
} else if q2_pending > 0 {
(2, checked_add(queues.q2_tickets_paid, 1)?)
} else {
(3, checked_add(queues.q3_tickets_paid, 1)?)
};
let (expected_ticket_pda, _) = find_ticket_pda(program_id, target_queue, next_index);
require_keys_eq!(expected_ticket_pda, *next_ticket_pda.key, PaymentsError::InvalidPdaAddress);
let mut ticket = read_state::<TicketState>(next_ticket_pda)?;
require!(ticket.queue_id == target_queue, PaymentsError::InvalidTicketQueue);
require!(ticket.index == next_index, PaymentsError::InvalidTicketIndex);
require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
require_keys_eq!(*ticket_recipient_wallet.key, ticket.recipient_wallet, PaymentsError::InvalidTicketRecipient);
let sol_usd = read_sol_usd_price(sol_usd_price_update, sol_usd_price_update.key)?;
let ticket_lamports = usd_cents_to_lamports_ceil(ticket.payout_usd_cents, &sol_usd)?;
let dao_multiplier = target_queue as u64;
let dao_usd_cents = checked_mul(ticket.payout_usd_cents, dao_multiplier)?;
let dao_lamports = usd_cents_to_lamports_ceil(dao_usd_cents, &sol_usd)?;
let needed = checked_add(checked_add(ticket_lamports, dao_lamports)?, coef_limit.call_reward_lamports)?;
require!(available_vault_lamports(inflow_vault_pda)? >= needed, PaymentsError::NotEnoughInflowForStep);
transfer_from_vault(inflow_vault_pda, ticket_recipient_wallet, ticket_lamports)?;
transfer_from_vault(inflow_vault_pda, dao_wallet, dao_lamports)?;
transfer_from_vault(inflow_vault_pda, signer, coef_limit.call_reward_lamports)?;
ticket.is_paid = true;
write_state(next_ticket_pda, &ticket)?;
mark_queue_paid(&mut queues, target_queue, ticket.payout_usd_cents)?;
write_state(queues_pda, &queues)
}
fn process_change_ticket_recipient(
program_id: &Pubkey,
accounts: &[AccountInfo],
args: ChangeTicketRecipientArgs,
) -> ProgramResult {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let queues_pda = next_account_info(account_iter)?;
let ticket_pda = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
let queues = read_state::<QueuesState>(queues_pda)?;
let mut ticket = read_state::<TicketState>(ticket_pda)?;
require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
require_keys_eq!(*signer.key, ticket.recipient_wallet, PaymentsError::UnauthorizedTicketOwner);
let (expected_ticket_pda, _) = find_ticket_pda(program_id, ticket.queue_id, ticket.index);
require_keys_eq!(expected_ticket_pda, *ticket_pda.key, PaymentsError::InvalidPdaAddress);
let q1_pending = checked_sub(queues.q1_tickets_total, queues.q1_tickets_paid)?;
let q2_pending = checked_sub(queues.q2_tickets_total, queues.q2_tickets_paid)?;
let q3_pending = checked_sub(queues.q3_tickets_total, queues.q3_tickets_paid)?;
let next = if q1_pending > 0 {
Some((1, checked_add(queues.q1_tickets_paid, 1)?))
} else if q2_pending > 0 {
Some((2, checked_add(queues.q2_tickets_paid, 1)?))
} else if q3_pending > 0 {
Some((3, checked_add(queues.q3_tickets_paid, 1)?))
} else {
None
};
if let Some((queue_id, next_index)) = next {
require!(
!(ticket.queue_id == queue_id && ticket.index == next_index),
PaymentsError::CannotChangeRecipientForNextPayoutTicket
);
}
ticket.recipient_wallet = args.new_recipient_wallet;
write_state(ticket_pda, &ticket)
}
struct BuyTicketAccounts<'a, 'info> {
signer: &'a AccountInfo<'info>,
config_pda: &'a AccountInfo<'info>,
coef_limit_pda: &'a AccountInfo<'info>,
queues_pda: &'a AccountInfo<'info>,
ticket_pda: &'a AccountInfo<'info>,
dao_wallet: &'a AccountInfo<'info>,
sol_usd_price_update: &'a AccountInfo<'info>,
system_program_ai: &'a AccountInfo<'info>,
}
impl<'a, 'info> BuyTicketAccounts<'a, 'info> {
fn parse(accounts: &'a [AccountInfo<'info>]) -> Result<Self, ProgramError> {
let account_iter = &mut accounts.iter();
let signer = next_signer_account(account_iter)?;
let config_pda = next_account_info(account_iter)?;
let coef_limit_pda = next_account_info(account_iter)?;
let queues_pda = next_account_info(account_iter)?;
let ticket_pda = next_account_info(account_iter)?;
let dao_wallet = next_account_info(account_iter)?;
let sol_usd_price_update = next_account_info(account_iter)?;
let system_program_ai = next_account_info(account_iter)?;
require!(account_iter.next().is_none(), PaymentsError::InvalidInstruction);
require_system_program(system_program_ai)?;
Ok(Self {
signer,
config_pda,
coef_limit_pda,
queues_pda,
ticket_pda,
dao_wallet,
sol_usd_price_update,
system_program_ai,
})
}
}
fn ensure_expected_pdas(
program_id: &Pubkey,
config_pda: &AccountInfo,
coef_limit_pda: &AccountInfo,
queues_pda: &AccountInfo,
inflow_vault_pda: &AccountInfo,
) -> ProgramResult {
let (config, _) = find_single_pda(program_id, settings::CONFIG_SEED);
let (coef, _) = find_single_pda(program_id, settings::COEF_LIMIT_SEED);
let (queues, _) = find_single_pda(program_id, settings::QUEUES_SEED);
let (inflow, _) = find_single_pda(program_id, settings::INFLOW_VAULT_SEED);
require_keys_eq!(config, *config_pda.key, PaymentsError::InvalidPdaAddress);
require_keys_eq!(coef, *coef_limit_pda.key, PaymentsError::InvalidPdaAddress);
require_keys_eq!(queues, *queues_pda.key, PaymentsError::InvalidPdaAddress);
require_keys_eq!(inflow, *inflow_vault_pda.key, PaymentsError::InvalidPdaAddress);
Ok(())
}
fn find_single_pda(program_id: &Pubkey, seed: &[u8]) -> (Pubkey, u8) {
Pubkey::find_program_address(&[seed], program_id)
}
fn find_ticket_pda(program_id: &Pubkey, queue_id: u8, index: u64) -> (Pubkey, u8) {
Pubkey::find_program_address(&[queue_seed(queue_id), &index.to_le_bytes()], program_id)
}
fn find_manager_allowance_pda(program_id: &Pubkey, manager_wallet: &Pubkey) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[settings::MANAGER_ALLOWANCE_SEED, manager_wallet.as_ref()],
program_id,
)
}
fn queue_seed(queue_id: u8) -> &'static [u8] {
match queue_id {
1 => settings::Q1_TICKET_SEED,
2 => settings::Q2_TICKET_SEED,
3 => settings::Q3_TICKET_SEED,
_ => settings::Q1_TICKET_SEED,
}
}
fn is_valid_queue_id(queue_id: u8) -> bool {
matches!(queue_id, 1 | 2 | 3)
}
fn buy_ticket_by_purchase_usd(
program_id: &Pubkey,
ctx: &BuyTicketAccounts<'_, '_>,
purchase_usd_cents: u64,
transfer_lamports: u64,
recipient_wallet: Pubkey,
) -> ProgramResult {
let config = read_state::<ConfigState>(ctx.config_pda)?;
let coef_limit = read_state::<CoefLimitState>(ctx.coef_limit_pda)?;
let mut queues = read_state::<QueuesState>(ctx.queues_pda)?;
require_keys_eq!(*ctx.dao_wallet.key, config.dao_wallet, PaymentsError::InvalidDaoWallet);
let queue1_sum_total_before = queues.q1_sum_total_usd_cents;
require!(queue1_sum_total_before < coef_limit.limit_usd_cents, PaymentsError::QueueTemporarilyPaused);
let ticket_index = checked_add(queues.q1_tickets_total, 1)?;
let (expected_ticket_pda, ticket_bump) = find_ticket_pda(program_id, 1, ticket_index);
require_keys_eq!(expected_ticket_pda, *ctx.ticket_pda.key, PaymentsError::InvalidPdaAddress);
require!(is_uninitialized_account(ctx.ticket_pda), PaymentsError::PdaAlreadyExists);
let payout_usd_cents = checked_mul(purchase_usd_cents, coef_limit.coef_ppm)? / settings::COEF_SCALE_PPM;
require!(payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);
transfer_from_signer_to_target(ctx.signer, ctx.dao_wallet, ctx.system_program_ai, transfer_lamports)?;
let ticket = TicketState {
version: 1,
queue_id: 1,
index: ticket_index,
is_paid: false,
recipient_wallet,
payout_usd_cents,
debt_before_usd_cents: queue1_sum_total_before,
};
create_state_with_seeds(
program_id,
ctx.signer,
ctx.system_program_ai,
ctx.ticket_pda,
&[
settings::Q1_TICKET_SEED,
&ticket_index.to_le_bytes(),
&[ticket_bump],
],
settings::TICKET_SPACE,
&ticket,
)?;
queues.q1_tickets_total = ticket_index;
queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, payout_usd_cents)?;
write_state(ctx.queues_pda, &queues)
}
fn transfer_from_signer_to_target<'info>(
signer: &AccountInfo<'info>,
target: &AccountInfo<'info>,
system_program_ai: &AccountInfo<'info>,
amount: u64,
) -> ProgramResult {
let ix = system_instruction::transfer(signer.key, target.key, amount);
invoke(&ix, &[signer.clone(), target.clone(), system_program_ai.clone()])
}
fn read_sol_usd_price(price_update: &AccountInfo, key: &Pubkey) -> Result<SolUsdPrice, ProgramError> {
let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT)
.map_err(|_| ProgramError::from(PaymentsError::InvalidOracleFeedConfig))?;
require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount);
let data = price_update.try_borrow_data()?;
let clock = Clock::get()?;
parse_pyth_price_update_v2(&data, &clock)
}
fn parse_pyth_price_update_v2(data: &[u8], clock: &Clock) -> Result<SolUsdPrice, ProgramError> {
require!(data.len() > 100, PaymentsError::InvalidOraclePrice);
let price = read_i64_at(data, 73)?;
let exponent = read_i32_at(data, 89)?;
let publish_time = read_i64_at(data, 93)?;
require!(
publish_time.saturating_add(settings::ORACLE_MAX_AGE_SECS as i64) >= clock.unix_timestamp,
PaymentsError::OraclePriceTooOld
);
require!(price > 0, PaymentsError::InvalidOraclePrice);
let mut num = checked_mul_u128(price as u128, settings::USD_CENTS_SCALE as u128)?;
let mut den: u128 = 1;
if exponent >= 0 {
let pow = 10u128
.checked_pow(exponent as u32)
.ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
num = checked_mul_u128(num, pow)?;
} else {
let pow = 10u128
.checked_pow((-exponent) as u32)
.ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
den = checked_mul_u128(den, pow)?;
}
require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice);
Ok(SolUsdPrice { price_num: num, price_den: den })
}
fn read_i32_at(data: &[u8], offset: usize) -> Result<i32, ProgramError> {
let end = offset
.checked_add(4)
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
let slice = data
.get(offset..end)
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
Ok(i32::from_le_bytes(
slice
.try_into()
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?,
))
}
fn read_i64_at(data: &[u8], offset: usize) -> Result<i64, ProgramError> {
let end = offset
.checked_add(8)
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
let slice = data
.get(offset..end)
.ok_or(ProgramError::from(PaymentsError::InvalidOraclePrice))?;
Ok(i64::from_le_bytes(
slice
.try_into()
.map_err(|_| ProgramError::from(PaymentsError::InvalidOraclePrice))?,
))
}
fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result<u64, ProgramError> {
let numerator = checked_mul_u128(lamports as u128, price.price_num)?;
let denominator = checked_mul_u128(settings::LAMPORTS_PER_SOL as u128, price.price_den)?;
require!(denominator > 0, PaymentsError::InvalidOraclePrice);
u64::try_from(numerator / denominator).map_err(|_| PaymentsError::MathOverflow.into())
}
fn usd_cents_to_lamports_ceil(usd_cents: u64, price: &SolUsdPrice) -> Result<u64, ProgramError> {
require!(usd_cents > 0, PaymentsError::InvalidAmount);
require!(price.price_num > 0, PaymentsError::InvalidOraclePrice);
let numerator = checked_mul_u128(
checked_mul_u128(usd_cents as u128, settings::LAMPORTS_PER_SOL as u128)?,
price.price_den,
)?;
let adjusted = numerator
.checked_add(price.price_num - 1)
.ok_or(ProgramError::from(PaymentsError::MathOverflow))?;
u64::try_from(adjusted / price.price_num).map_err(|_| PaymentsError::MathOverflow.into())
}
fn create_and_store_state<'info, T: StateCodec>(
program_id: &Pubkey,
payer: &AccountInfo<'info>,
system_program_ai: &AccountInfo<'info>,
pda: &AccountInfo<'info>,
seed: &[u8],
space: usize,
state: &T,
) -> ProgramResult {
let (_, bump) = find_single_pda(program_id, seed);
create_state_with_seeds(
program_id,
payer,
system_program_ai,
pda,
&[seed, &[bump]],
space,
state,
)
}
fn create_state_with_seeds<'info, T: StateCodec>(
program_id: &Pubkey,
payer: &AccountInfo<'info>,
system_program_ai: &AccountInfo<'info>,
pda: &AccountInfo<'info>,
seeds: &[&[u8]],
space: usize,
state: &T,
) -> ProgramResult {
create_pda_account(pda, payer, system_program_ai, program_id, seeds, space as u64)?;
write_state(pda, state)
}
fn create_pda_account<'info>(
pda: &AccountInfo<'info>,
payer: &AccountInfo<'info>,
system_program_ai: &AccountInfo<'info>,
program_id: &Pubkey,
seeds: &[&[u8]],
space: u64,
) -> ProgramResult {
require!(is_uninitialized_account(pda), PaymentsError::PdaAlreadyExists);
let lamports = Rent::get()?.minimum_balance(space as usize);
let create_ix = system_instruction::create_account(payer.key, pda.key, lamports, space, program_id);
invoke_signed(
&create_ix,
&[payer.clone(), pda.clone(), system_program_ai.clone()],
&[seeds],
)
}
fn write_state<T: StateCodec>(pda: &AccountInfo, state: &T) -> ProgramResult {
let bytes = state.encode();
let mut data = pda.try_borrow_mut_data()?;
require!(bytes.len() <= data.len(), PaymentsError::InvalidAccountData);
data.fill(0);
data[..bytes.len()].copy_from_slice(&bytes);
Ok(())
}
fn read_state<T: StateCodec>(pda: &AccountInfo) -> Result<T, ProgramError> {
require!(!is_uninitialized_account(pda), PaymentsError::EmptyState);
require_keys_eq!(*pda.owner, id(), PaymentsError::InvalidPdaAddress);
let data = pda.try_borrow_data()?;
let encoded_len = T::encoded_len();
require!(data.len() >= encoded_len, PaymentsError::InvalidAccountData);
T::decode(&data[..encoded_len])
}
fn is_uninitialized_account(account: &AccountInfo) -> bool {
account.lamports() == 0
&& account.data_len() == 0
&& (*account.owner == system_program::ID || *account.owner == Pubkey::default())
}
fn available_vault_lamports(vault: &AccountInfo) -> Result<u64, ProgramError> {
let total = vault.lamports();
let rent_min = Rent::get()?.minimum_balance(vault.data_len());
Ok(total.saturating_sub(rent_min))
}
fn transfer_from_vault(vault: &AccountInfo, recipient: &AccountInfo, amount: u64) -> ProgramResult {
if amount == 0 {
return Ok(());
}
let mut vault_lamports = vault.try_borrow_mut_lamports()?;
let mut recipient_lamports = recipient.try_borrow_mut_lamports()?;
require!(**vault_lamports >= amount, PaymentsError::NotEnoughInflowForStep);
**vault_lamports = checked_sub(**vault_lamports, amount)?;
**recipient_lamports = checked_add(**recipient_lamports, amount)?;
Ok(())
}
fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> ProgramResult {
let available = available_vault_lamports(vault)?;
transfer_from_vault(vault, dao_wallet, available)
}
fn next_signer_account<'a, 'info>(
account_iter: &mut std::slice::Iter<'a, AccountInfo<'info>>,
) -> Result<&'a AccountInfo<'info>, ProgramError> {
let signer = next_account_info(account_iter)?;
require!(signer.is_signer, PaymentsError::MissingRequiredSignature);
Ok(signer)
}
fn require_system_program(system_program_ai: &AccountInfo) -> ProgramResult {
require_keys_eq!(*system_program_ai.key, system_program::ID, PaymentsError::InvalidSystemProgram);
Ok(())
}
fn queue_total(queues: &QueuesState, queue_id: u8) -> u64 {
match queue_id {
1 => queues.q1_tickets_total,
2 => queues.q2_tickets_total,
3 => queues.q3_tickets_total,
_ => 0,
}
}
fn queue_sum_total(queues: &QueuesState, queue_id: u8) -> u64 {
match queue_id {
1 => queues.q1_sum_total_usd_cents,
2 => queues.q2_sum_total_usd_cents,
3 => queues.q3_sum_total_usd_cents,
_ => 0,
}
}
fn queue_allowance(allowance: &ManagerAllowanceState, queue_id: u8) -> u64 {
match queue_id {
1 => allowance.q1_available_usd_cents,
2 => allowance.q2_available_usd_cents,
3 => allowance.q3_available_usd_cents,
_ => 0,
}
}
fn sub_queue_allowance(
allowance: &mut ManagerAllowanceState,
queue_id: u8,
amount: u64,
) -> ProgramResult {
match queue_id {
1 => allowance.q1_available_usd_cents = checked_sub(allowance.q1_available_usd_cents, amount)?,
2 => allowance.q2_available_usd_cents = checked_sub(allowance.q2_available_usd_cents, amount)?,
3 => allowance.q3_available_usd_cents = checked_sub(allowance.q3_available_usd_cents, amount)?,
_ => return Err(PaymentsError::InvalidTicketQueue.into()),
}
Ok(())
}
fn set_queue_total(queues: &mut QueuesState, queue_id: u8, value: u64) {
match queue_id {
1 => queues.q1_tickets_total = value,
2 => queues.q2_tickets_total = value,
3 => queues.q3_tickets_total = value,
_ => {}
}
}
fn add_queue_sum_total(queues: &mut QueuesState, queue_id: u8, value: u64) -> ProgramResult {
match queue_id {
1 => queues.q1_sum_total_usd_cents = checked_add(queues.q1_sum_total_usd_cents, value)?,
2 => queues.q2_sum_total_usd_cents = checked_add(queues.q2_sum_total_usd_cents, value)?,
3 => queues.q3_sum_total_usd_cents = checked_add(queues.q3_sum_total_usd_cents, value)?,
_ => return Err(PaymentsError::InvalidTicketQueue.into()),
}
Ok(())
}
fn mark_queue_paid(queues: &mut QueuesState, queue_id: u8, payout_usd_cents: u64) -> ProgramResult {
match queue_id {
1 => {
queues.q1_tickets_paid = checked_add(queues.q1_tickets_paid, 1)?;
queues.q1_sum_paid_usd_cents = checked_add(queues.q1_sum_paid_usd_cents, payout_usd_cents)?;
}
2 => {
queues.q2_tickets_paid = checked_add(queues.q2_tickets_paid, 1)?;
queues.q2_sum_paid_usd_cents = checked_add(queues.q2_sum_paid_usd_cents, payout_usd_cents)?;
}
3 => {
queues.q3_tickets_paid = checked_add(queues.q3_tickets_paid, 1)?;
queues.q3_sum_paid_usd_cents = checked_add(queues.q3_sum_paid_usd_cents, payout_usd_cents)?;
}
_ => return Err(PaymentsError::InvalidTicketQueue.into()),
}
Ok(())
}
fn checked_add(left: u64, right: u64) -> Result<u64, ProgramError> {
left.checked_add(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_sub(left: u64, right: u64) -> Result<u64, ProgramError> {
left.checked_sub(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_mul(left: u64, right: u64) -> Result<u64, ProgramError> {
left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}
fn checked_mul_u128(left: u128, right: u128) -> Result<u128, ProgramError> {
left.checked_mul(right).ok_or(PaymentsError::MathOverflow.into())
}