1148 lines
41 KiB
Rust
1148 lines
41 KiB
Rust
use anchor_lang::prelude::*;
|
||
use anchor_lang::solana_program::{program::invoke, system_instruction};
|
||
use common::utils::{create_pda, safe_read_pda, write_to_pda, ErrCode};
|
||
use pyth_solana_receiver_sdk::error::GetPriceError;
|
||
use pyth_solana_receiver_sdk::price_update::{get_feed_id_from_hex, Price, PriceUpdateV2};
|
||
use std::str::FromStr;
|
||
|
||
pub mod settings;
|
||
|
||
declare_id!("m48pWRGWrMj3TEHjuU4zsp5Gju4e7ZaPovk8RcVt7kR");
|
||
|
||
#[program]
|
||
pub mod shine_payments {
|
||
use super::*;
|
||
|
||
pub fn init(ctx: Context<Init>) -> Result<()> {
|
||
ensure_expected_pdas(ctx.program_id, &ctx.accounts)?;
|
||
require!(
|
||
ctx.accounts.config_pda.owner == &Pubkey::default(),
|
||
ErrCode::SystemAlreadyInitialized
|
||
);
|
||
require!(
|
||
ctx.accounts.coef_limit_pda.owner == &Pubkey::default(),
|
||
ErrCode::SystemAlreadyInitialized
|
||
);
|
||
require!(
|
||
ctx.accounts.queues_pda.owner == &Pubkey::default(),
|
||
ErrCode::SystemAlreadyInitialized
|
||
);
|
||
require!(
|
||
ctx.accounts.inflow_vault_pda.owner == &Pubkey::default(),
|
||
ErrCode::SystemAlreadyInitialized
|
||
);
|
||
|
||
let dao_wallet = Pubkey::from_str(settings::DAO_WALLET)
|
||
.map_err(|_| error!(PaymentsError::InvalidSettingsWallet))?;
|
||
let system_program_ai = ctx.accounts.system_program.to_account_info();
|
||
|
||
let config = ConfigState {
|
||
version: 1,
|
||
dao_wallet,
|
||
inflow_vault: ctx.accounts.inflow_vault_pda.key(),
|
||
};
|
||
create_and_store_state(
|
||
ctx.program_id,
|
||
&ctx.accounts.payer,
|
||
&system_program_ai,
|
||
&ctx.accounts.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(
|
||
ctx.program_id,
|
||
&ctx.accounts.payer,
|
||
&system_program_ai,
|
||
&ctx.accounts.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,
|
||
};
|
||
create_and_store_state(
|
||
ctx.program_id,
|
||
&ctx.accounts.payer,
|
||
&system_program_ai,
|
||
&ctx.accounts.queues_pda,
|
||
settings::QUEUES_SEED,
|
||
settings::QUEUES_SPACE,
|
||
&queues,
|
||
)?;
|
||
|
||
let vault = VaultState { version: 1 };
|
||
create_and_store_state(
|
||
ctx.program_id,
|
||
&ctx.accounts.payer,
|
||
&system_program_ai,
|
||
&ctx.accounts.inflow_vault_pda,
|
||
settings::INFLOW_VAULT_SEED,
|
||
settings::INFLOW_VAULT_SPACE,
|
||
&vault,
|
||
)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn update_coef_limit(
|
||
ctx: Context<UpdateCoefLimit>,
|
||
args: UpdateCoefLimitArgs,
|
||
) -> Result<()> {
|
||
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||
require_keys_eq!(
|
||
config.dao_wallet,
|
||
ctx.accounts.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>(&ctx.accounts.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(&ctx.accounts.coef_limit_pda, &coef_limit)?;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn grant_manager_limits(
|
||
ctx: Context<GrantManagerLimits>,
|
||
args: GrantManagerLimitsArgs,
|
||
) -> Result<()> {
|
||
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||
require_keys_eq!(
|
||
config.dao_wallet,
|
||
ctx.accounts.signer.key(),
|
||
PaymentsError::UnauthorizedDao
|
||
);
|
||
require!(
|
||
args.add_q1_usd_cents > 0 || args.add_q2_usd_cents > 0,
|
||
PaymentsError::InvalidAmount
|
||
);
|
||
|
||
let (expected_pda, bump) = find_manager_allowance_pda(ctx.program_id, &args.manager_wallet);
|
||
require_keys_eq!(
|
||
expected_pda,
|
||
ctx.accounts.manager_allowance_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
|
||
let system_program_ai = ctx.accounts.system_program.to_account_info();
|
||
let mut state = if ctx.accounts.manager_allowance_pda.owner == &Pubkey::default() {
|
||
let initial = ManagerAllowanceState {
|
||
version: 1,
|
||
manager_wallet: args.manager_wallet,
|
||
q1_available_usd_cents: 0,
|
||
q2_available_usd_cents: 0,
|
||
};
|
||
create_state_with_seeds(
|
||
ctx.program_id,
|
||
&ctx.accounts.signer,
|
||
&system_program_ai,
|
||
&ctx.accounts.manager_allowance_pda,
|
||
&[
|
||
settings::MANAGER_ALLOWANCE_SEED,
|
||
args.manager_wallet.as_ref(),
|
||
&[bump],
|
||
],
|
||
settings::MANAGER_ALLOWANCE_SPACE,
|
||
&initial,
|
||
)?;
|
||
initial
|
||
} else {
|
||
read_state::<ManagerAllowanceState>(&ctx.accounts.manager_allowance_pda)?
|
||
};
|
||
require_keys_eq!(
|
||
state.manager_wallet,
|
||
args.manager_wallet,
|
||
PaymentsError::InvalidManagerWallet
|
||
);
|
||
|
||
state.q1_available_usd_cents = state
|
||
.q1_available_usd_cents
|
||
.checked_add(args.add_q1_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
state.q2_available_usd_cents = state
|
||
.q2_available_usd_cents
|
||
.checked_add(args.add_q2_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
write_state(&ctx.accounts.manager_allowance_pda, &state)?;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn buy_ticket(ctx: Context<BuyTicket>, args: BuyTicketArgs) -> Result<()> {
|
||
let sol_usd = read_sol_usd_price(
|
||
&ctx.accounts.sol_usd_price_update,
|
||
&ctx.accounts.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(
|
||
&ctx,
|
||
purchase_usd_cents,
|
||
args.amount_lamports,
|
||
args.recipient_wallet,
|
||
)
|
||
}
|
||
|
||
pub fn buy_ticket_usd(ctx: Context<BuyTicket>, args: BuyTicketUsdArgs) -> Result<()> {
|
||
require!(args.amount_usd_cents > 0, PaymentsError::InvalidAmount);
|
||
require!(args.max_pay_lamports > 0, PaymentsError::InvalidAmount);
|
||
|
||
let sol_usd = read_sol_usd_price(
|
||
&ctx.accounts.sol_usd_price_update,
|
||
&ctx.accounts.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(&ctx, args.amount_usd_cents, pay_lamports, args.recipient_wallet)
|
||
}
|
||
|
||
pub fn buy_ticket_sol(ctx: Context<BuyTicket>, args: BuyTicketSolArgs) -> Result<()> {
|
||
require!(args.amount_lamports > 0, PaymentsError::InvalidAmount);
|
||
|
||
let sol_usd = read_sol_usd_price(
|
||
&ctx.accounts.sol_usd_price_update,
|
||
&ctx.accounts.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(
|
||
&ctx,
|
||
purchase_usd_cents,
|
||
args.amount_lamports,
|
||
args.recipient_wallet,
|
||
)
|
||
}
|
||
|
||
pub fn manager_add_ticket(
|
||
ctx: Context<ManagerAddTicket>,
|
||
args: ManagerAddTicketArgs,
|
||
) -> Result<()> {
|
||
require!(args.payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);
|
||
require!(
|
||
args.queue_id == 1 || args.queue_id == 2,
|
||
PaymentsError::InvalidTicketQueue
|
||
);
|
||
|
||
let (expected_manager_pda, _) =
|
||
find_manager_allowance_pda(ctx.program_id, ctx.accounts.signer.key);
|
||
require_keys_eq!(
|
||
expected_manager_pda,
|
||
ctx.accounts.manager_allowance_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
let mut allowance =
|
||
read_state::<ManagerAllowanceState>(&ctx.accounts.manager_allowance_pda)?;
|
||
require_keys_eq!(
|
||
allowance.manager_wallet,
|
||
ctx.accounts.signer.key(),
|
||
PaymentsError::InvalidManagerWallet
|
||
);
|
||
|
||
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||
let debt_before_total = if args.queue_id == 1 {
|
||
queues.q1_sum_total_usd_cents
|
||
} else {
|
||
queues.q2_sum_total_usd_cents
|
||
};
|
||
|
||
if args.queue_id == 1 {
|
||
require!(
|
||
allowance.q1_available_usd_cents >= args.payout_usd_cents,
|
||
PaymentsError::ManagerLimitExceeded
|
||
);
|
||
} else {
|
||
require!(
|
||
allowance.q2_available_usd_cents >= args.payout_usd_cents,
|
||
PaymentsError::ManagerLimitExceeded
|
||
);
|
||
}
|
||
|
||
let ticket_index = if args.queue_id == 1 {
|
||
queues
|
||
.q1_tickets_total
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
} else {
|
||
queues
|
||
.q2_tickets_total
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
};
|
||
let (expected_ticket_pda, ticket_bump) =
|
||
find_ticket_pda(ctx.program_id, args.queue_id, ticket_index);
|
||
require_keys_eq!(
|
||
expected_ticket_pda,
|
||
ctx.accounts.ticket_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
require!(
|
||
ctx.accounts.ticket_pda.owner == &Pubkey::default(),
|
||
ErrCode::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,
|
||
};
|
||
let seed_prefix = if args.queue_id == 1 {
|
||
settings::Q1_TICKET_SEED
|
||
} else {
|
||
settings::Q2_TICKET_SEED
|
||
};
|
||
create_state_with_seeds(
|
||
ctx.program_id,
|
||
&ctx.accounts.signer,
|
||
&ctx.accounts.system_program.to_account_info(),
|
||
&ctx.accounts.ticket_pda,
|
||
&[seed_prefix, &ticket_index.to_le_bytes(), &[ticket_bump]],
|
||
settings::TICKET_SPACE,
|
||
&ticket,
|
||
)?;
|
||
|
||
if args.queue_id == 1 {
|
||
allowance.q1_available_usd_cents = allowance
|
||
.q1_available_usd_cents
|
||
.checked_sub(args.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
queues.q1_tickets_total = ticket_index;
|
||
queues.q1_sum_total_usd_cents = queues
|
||
.q1_sum_total_usd_cents
|
||
.checked_add(args.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
} else {
|
||
allowance.q2_available_usd_cents = allowance
|
||
.q2_available_usd_cents
|
||
.checked_sub(args.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
queues.q2_tickets_total = ticket_index;
|
||
queues.q2_sum_total_usd_cents = queues
|
||
.q2_sum_total_usd_cents
|
||
.checked_add(args.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
}
|
||
write_state(&ctx.accounts.manager_allowance_pda, &allowance)?;
|
||
write_state(&ctx.accounts.queues_pda, &queues)?;
|
||
Ok(())
|
||
}
|
||
|
||
pub fn step_payout(ctx: Context<StepPayout>) -> Result<()> {
|
||
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||
let coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
|
||
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||
let _vault_state = read_state::<VaultState>(&ctx.accounts.inflow_vault_pda)?;
|
||
|
||
require_keys_eq!(
|
||
ctx.accounts.dao_wallet.key(),
|
||
config.dao_wallet,
|
||
PaymentsError::InvalidDaoWallet
|
||
);
|
||
require_keys_eq!(
|
||
ctx.accounts.inflow_vault_pda.key(),
|
||
config.inflow_vault,
|
||
PaymentsError::InvalidInflowVault
|
||
);
|
||
|
||
let q1_pending = queues
|
||
.q1_tickets_total
|
||
.checked_sub(queues.q1_tickets_paid)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let q2_pending = queues
|
||
.q2_tickets_total
|
||
.checked_sub(queues.q2_tickets_paid)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
|
||
if q1_pending == 0 && q2_pending == 0 {
|
||
transfer_all_available_to_dao(
|
||
&ctx.accounts.inflow_vault_pda,
|
||
&ctx.accounts.dao_wallet,
|
||
)?;
|
||
return Ok(());
|
||
}
|
||
|
||
let target_queue = if q1_pending > 0 { 1 } else { 2 };
|
||
let next_index = if target_queue == 1 {
|
||
queues
|
||
.q1_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
} else {
|
||
queues
|
||
.q2_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
};
|
||
let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, target_queue, next_index);
|
||
require_keys_eq!(
|
||
expected_ticket_pda,
|
||
ctx.accounts.next_ticket_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
|
||
let mut ticket = read_state::<TicketState>(&ctx.accounts.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!(
|
||
ctx.accounts.ticket_recipient_wallet.key(),
|
||
ticket.recipient_wallet,
|
||
PaymentsError::InvalidTicketRecipient
|
||
);
|
||
|
||
let sol_usd = read_sol_usd_price(
|
||
&ctx.accounts.sol_usd_price_update,
|
||
&ctx.accounts.sol_usd_price_update.key(),
|
||
)?;
|
||
let ticket_lamports = usd_cents_to_lamports_ceil(ticket.payout_usd_cents, &sol_usd)?;
|
||
let dao_multiplier: u64 = if target_queue == 1 { 1 } else { 2 };
|
||
let dao_usd_cents = ticket
|
||
.payout_usd_cents
|
||
.checked_mul(dao_multiplier)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let dao_lamports = usd_cents_to_lamports_ceil(dao_usd_cents, &sol_usd)?;
|
||
|
||
let needed = ticket_lamports
|
||
.checked_add(dao_lamports)
|
||
.and_then(|v| v.checked_add(coef_limit.call_reward_lamports))
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
require!(
|
||
available_vault_lamports(&ctx.accounts.inflow_vault_pda)? >= needed,
|
||
PaymentsError::NotEnoughInflowForStep
|
||
);
|
||
|
||
transfer_from_vault(
|
||
&ctx.accounts.inflow_vault_pda,
|
||
&ctx.accounts.ticket_recipient_wallet,
|
||
ticket_lamports,
|
||
)?;
|
||
transfer_from_vault(
|
||
&ctx.accounts.inflow_vault_pda,
|
||
&ctx.accounts.dao_wallet,
|
||
dao_lamports,
|
||
)?;
|
||
transfer_from_vault(
|
||
&ctx.accounts.inflow_vault_pda,
|
||
&ctx.accounts.signer,
|
||
coef_limit.call_reward_lamports,
|
||
)?;
|
||
|
||
ticket.is_paid = true;
|
||
write_state(&ctx.accounts.next_ticket_pda, &ticket)?;
|
||
|
||
if target_queue == 1 {
|
||
queues.q1_tickets_paid = queues
|
||
.q1_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
queues.q1_sum_paid_usd_cents = queues
|
||
.q1_sum_paid_usd_cents
|
||
.checked_add(ticket.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
} else {
|
||
queues.q2_tickets_paid = queues
|
||
.q2_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
queues.q2_sum_paid_usd_cents = queues
|
||
.q2_sum_paid_usd_cents
|
||
.checked_add(ticket.payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
}
|
||
write_state(&ctx.accounts.queues_pda, &queues)?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
pub fn change_ticket_recipient(
|
||
ctx: Context<ChangeTicketRecipient>,
|
||
args: ChangeTicketRecipientArgs,
|
||
) -> Result<()> {
|
||
let queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||
let mut ticket = read_state::<TicketState>(&ctx.accounts.ticket_pda)?;
|
||
|
||
require!(!ticket.is_paid, PaymentsError::TicketAlreadyPaid);
|
||
require_keys_eq!(
|
||
ctx.accounts.signer.key(),
|
||
ticket.recipient_wallet,
|
||
PaymentsError::UnauthorizedTicketOwner
|
||
);
|
||
|
||
let (expected_ticket_pda, _) = find_ticket_pda(ctx.program_id, ticket.queue_id, ticket.index);
|
||
require_keys_eq!(
|
||
expected_ticket_pda,
|
||
ctx.accounts.ticket_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
|
||
let q1_pending = queues
|
||
.q1_tickets_total
|
||
.checked_sub(queues.q1_tickets_paid)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let q2_pending = queues
|
||
.q2_tickets_total
|
||
.checked_sub(queues.q2_tickets_paid)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
|
||
if q1_pending > 0 || q2_pending > 0 {
|
||
let target_queue = if q1_pending > 0 { 1 } else { 2 };
|
||
let next_index = if target_queue == 1 {
|
||
queues
|
||
.q1_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
} else {
|
||
queues
|
||
.q2_tickets_paid
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
};
|
||
|
||
require!(
|
||
!(ticket.queue_id == target_queue && ticket.index == next_index),
|
||
PaymentsError::CannotChangeRecipientForNextPayoutTicket
|
||
);
|
||
}
|
||
|
||
ticket.recipient_wallet = args.new_recipient_wallet;
|
||
write_state(&ctx.accounts.ticket_pda, &ticket)?;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct Init<'info> {
|
||
/// CHECK: подписант и плательщик, проверяется атрибутом `signer`.
|
||
#[account(mut, signer)]
|
||
pub payer: AccountInfo<'info>,
|
||
/// CHECK: PDA конфига, адрес проверяется вручную в `ensure_expected_pdas`.
|
||
#[account(mut)]
|
||
pub config_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA коэффициента/лимита, адрес проверяется вручную в `ensure_expected_pdas`.
|
||
#[account(mut)]
|
||
pub coef_limit_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA состояния очередей, адрес проверяется вручную в `ensure_expected_pdas`.
|
||
#[account(mut)]
|
||
pub queues_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA inflow-вольта, адрес проверяется вручную в `ensure_expected_pdas`.
|
||
#[account(mut)]
|
||
pub inflow_vault_pda: AccountInfo<'info>,
|
||
pub system_program: Program<'info, System>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct UpdateCoefLimit<'info> {
|
||
/// CHECK: подписант-DAO, проверяется атрибутом `signer` и сверкой адреса в коде.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub config_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA коэффициента/лимита, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub coef_limit_pda: AccountInfo<'info>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct GrantManagerLimits<'info> {
|
||
/// CHECK: подписант DAO, проверяется атрибутом `signer` и сверкой с config.dao_wallet.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub config_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA лимитов менеджера, адрес проверяется вручную по manager pubkey.
|
||
#[account(mut)]
|
||
pub manager_allowance_pda: AccountInfo<'info>,
|
||
pub system_program: Program<'info, System>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct BuyTicket<'info> {
|
||
/// CHECK: подписант-покупатель, проверяется атрибутом `signer`.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub config_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA коэффициента/лимита, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub coef_limit_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA очередей, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub queues_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную.
|
||
#[account(mut)]
|
||
pub ticket_pda: AccountInfo<'info>,
|
||
/// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную.
|
||
#[account(mut)]
|
||
pub dao_wallet: AccountInfo<'info>,
|
||
pub sol_usd_price_update: Account<'info, PriceUpdateV2>,
|
||
pub system_program: Program<'info, System>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct ManagerAddTicket<'info> {
|
||
/// CHECK: подписант-менеджер, проверяется атрибутом `signer`.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA лимитов менеджера, адрес сверяется вручную по signer.
|
||
#[account(mut)]
|
||
pub manager_allowance_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA очередей, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub queues_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA тикета, адрес и состояние (должен быть пустым) проверяются вручную.
|
||
#[account(mut)]
|
||
pub ticket_pda: AccountInfo<'info>,
|
||
pub system_program: Program<'info, System>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct StepPayout<'info> {
|
||
/// CHECK: подписант-вызвавший шаг выплат, проверяется атрибутом `signer`.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA конфига, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub config_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA очередей, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub queues_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA коэффициента/лимита/награды, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub coef_limit_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA inflow-вольта, адрес сверяется с конфигом вручную.
|
||
#[account(mut)]
|
||
pub inflow_vault_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA следующего тикета, адрес и содержимое валидируются вручную.
|
||
#[account(mut)]
|
||
pub next_ticket_pda: AccountInfo<'info>,
|
||
/// CHECK: кошелек получателя тикета, адрес сверяется с полем тикета вручную.
|
||
#[account(mut)]
|
||
pub ticket_recipient_wallet: AccountInfo<'info>,
|
||
/// CHECK: DAO-кошелек, адрес сверяется с конфигом вручную.
|
||
#[account(mut)]
|
||
pub dao_wallet: AccountInfo<'info>,
|
||
pub sol_usd_price_update: Account<'info, PriceUpdateV2>,
|
||
}
|
||
|
||
#[derive(Accounts)]
|
||
pub struct ChangeTicketRecipient<'info> {
|
||
/// CHECK: подписант-владелец текущего recipient тикета.
|
||
#[account(mut, signer)]
|
||
pub signer: AccountInfo<'info>,
|
||
/// CHECK: PDA очередей, читается вручную.
|
||
#[account(mut)]
|
||
pub queues_pda: AccountInfo<'info>,
|
||
/// CHECK: PDA тикета, читается и валидируется вручную.
|
||
#[account(mut)]
|
||
pub ticket_pda: AccountInfo<'info>,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct UpdateCoefLimitArgs {
|
||
pub coef_ppm: u64,
|
||
pub limit_usd_cents: u64,
|
||
pub call_reward_lamports: u64,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct GrantManagerLimitsArgs {
|
||
pub manager_wallet: Pubkey,
|
||
pub add_q1_usd_cents: u64,
|
||
pub add_q2_usd_cents: u64,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct BuyTicketArgs {
|
||
pub amount_lamports: u64,
|
||
pub recipient_wallet: Pubkey,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct BuyTicketUsdArgs {
|
||
pub amount_usd_cents: u64,
|
||
pub max_pay_lamports: u64,
|
||
pub recipient_wallet: Pubkey,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct BuyTicketSolArgs {
|
||
pub amount_lamports: u64,
|
||
pub min_expected_usd_cents: u64,
|
||
pub recipient_wallet: Pubkey,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct ManagerAddTicketArgs {
|
||
pub queue_id: u8,
|
||
pub recipient_wallet: Pubkey,
|
||
pub payout_usd_cents: u64,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct ChangeTicketRecipientArgs {
|
||
pub new_recipient_wallet: Pubkey,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct ConfigState {
|
||
pub version: u8,
|
||
pub dao_wallet: Pubkey,
|
||
pub inflow_vault: Pubkey,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct CoefLimitState {
|
||
pub version: u8,
|
||
pub coef_ppm: u64,
|
||
pub limit_usd_cents: u64,
|
||
pub call_reward_lamports: u64,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, 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,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, 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(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct ManagerAllowanceState {
|
||
pub version: u8,
|
||
pub manager_wallet: Pubkey,
|
||
pub q1_available_usd_cents: u64,
|
||
pub q2_available_usd_cents: u64,
|
||
}
|
||
|
||
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
|
||
pub struct VaultState {
|
||
pub version: u8,
|
||
}
|
||
|
||
struct SolUsdPrice {
|
||
price_num: u128,
|
||
price_den: u128,
|
||
}
|
||
|
||
#[error_code]
|
||
pub enum PaymentsError {
|
||
#[msg("Ошибка в адресах кошельков из настроек программы")]
|
||
InvalidSettingsWallet,
|
||
#[msg("Недостаточно данных PDA")]
|
||
EmptyState,
|
||
#[msg("Неверный inflow vault")]
|
||
InvalidInflowVault,
|
||
#[msg("Неверный DAO кошелек")]
|
||
InvalidDaoWallet,
|
||
#[msg("Управляющий кошелек не авторизован")]
|
||
UnauthorizedManager,
|
||
#[msg("DAO кошелек не авторизован для этой операции")]
|
||
UnauthorizedDao,
|
||
#[msg("Некорректный коэффициент")]
|
||
InvalidCoefficient,
|
||
#[msg("Некорректный лимит")]
|
||
InvalidLimit,
|
||
#[msg("Некорректная награда за шаг выплаты")]
|
||
InvalidCallReward,
|
||
#[msg("Некорректная сумма")]
|
||
InvalidAmount,
|
||
#[msg("Очередь временно приостановлена: достигнут лимит")]
|
||
QueueTemporarilyPaused,
|
||
#[msg("Некорректная сумма выплаты")]
|
||
InvalidPayoutAmount,
|
||
#[msg("Недостаточно средств на inflow vault для шага выплаты")]
|
||
NotEnoughInflowForStep,
|
||
#[msg("Тикет уже выплачен")]
|
||
TicketAlreadyPaid,
|
||
#[msg("Неверный получатель тикета")]
|
||
InvalidTicketRecipient,
|
||
#[msg("Неверный номер тикета")]
|
||
InvalidTicketIndex,
|
||
#[msg("Неверный тип очереди у тикета")]
|
||
InvalidTicketQueue,
|
||
#[msg("Неверный кошелек менеджера")]
|
||
InvalidManagerWallet,
|
||
#[msg("Лимит менеджера по выбранной очереди превышен")]
|
||
ManagerLimitExceeded,
|
||
#[msg("Только текущий получатель тикета может изменить получателя")]
|
||
UnauthorizedTicketOwner,
|
||
#[msg("Нельзя менять получателя у следующего тикета на выплату")]
|
||
CannotChangeRecipientForNextPayoutTicket,
|
||
#[msg("Оракул передан неверный")]
|
||
InvalidOracleAccount,
|
||
#[msg("Некорректный feed id оракула")]
|
||
InvalidOracleFeed,
|
||
#[msg("Конфигурация оракула в settings некорректна")]
|
||
InvalidOracleFeedConfig,
|
||
#[msg("Цена оракула устарела")]
|
||
OraclePriceTooOld,
|
||
#[msg("Цена оракула некорректна")]
|
||
InvalidOraclePrice,
|
||
#[msg("Защита от проскальзывания: лимит пользователя не проходит")]
|
||
SlippageExceeded,
|
||
}
|
||
|
||
fn ensure_expected_pdas(program_id: &Pubkey, accounts: &Init) -> Result<()> {
|
||
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,
|
||
accounts.config_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
require_keys_eq!(
|
||
coef,
|
||
accounts.coef_limit_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
require_keys_eq!(
|
||
queues,
|
||
accounts.queues_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
require_keys_eq!(
|
||
inflow,
|
||
accounts.inflow_vault_pda.key(),
|
||
ErrCode::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) {
|
||
let idx = index.to_le_bytes();
|
||
let seed = if queue_id == 1 {
|
||
settings::Q1_TICKET_SEED
|
||
} else {
|
||
settings::Q2_TICKET_SEED
|
||
};
|
||
Pubkey::find_program_address(&[seed, &idx], 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 buy_ticket_by_purchase_usd(
|
||
ctx: &Context<BuyTicket>,
|
||
purchase_usd_cents: u64,
|
||
transfer_lamports: u64,
|
||
recipient_wallet: Pubkey,
|
||
) -> Result<()> {
|
||
let config = read_state::<ConfigState>(&ctx.accounts.config_pda)?;
|
||
let coef_limit = read_state::<CoefLimitState>(&ctx.accounts.coef_limit_pda)?;
|
||
let mut queues = read_state::<QueuesState>(&ctx.accounts.queues_pda)?;
|
||
|
||
require_keys_eq!(
|
||
ctx.accounts.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 = queues
|
||
.q1_tickets_total
|
||
.checked_add(1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let (expected_ticket_pda, ticket_bump) = find_ticket_pda(ctx.program_id, 1, ticket_index);
|
||
require_keys_eq!(
|
||
expected_ticket_pda,
|
||
ctx.accounts.ticket_pda.key(),
|
||
ErrCode::InvalidPdaAddress
|
||
);
|
||
require!(
|
||
ctx.accounts.ticket_pda.owner == &Pubkey::default(),
|
||
ErrCode::PdaAlreadyExists
|
||
);
|
||
|
||
let payout_usd_cents = purchase_usd_cents
|
||
.checked_mul(coef_limit.coef_ppm)
|
||
.ok_or(error!(ErrCode::MathOverflow))?
|
||
/ settings::COEF_SCALE_PPM;
|
||
require!(payout_usd_cents > 0, PaymentsError::InvalidPayoutAmount);
|
||
|
||
transfer_from_signer_to_target(
|
||
&ctx.accounts.signer,
|
||
&ctx.accounts.dao_wallet,
|
||
&ctx.accounts.system_program.to_account_info(),
|
||
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(
|
||
ctx.program_id,
|
||
&ctx.accounts.signer,
|
||
&ctx.accounts.system_program.to_account_info(),
|
||
&ctx.accounts.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 = queues
|
||
.q1_sum_total_usd_cents
|
||
.checked_add(payout_usd_cents)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
write_state(&ctx.accounts.queues_pda, &queues)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn transfer_from_signer_to_target<'info>(
|
||
signer: &AccountInfo<'info>,
|
||
target: &AccountInfo<'info>,
|
||
system_program: &AccountInfo<'info>,
|
||
amount: u64,
|
||
) -> Result<()> {
|
||
let ix = system_instruction::transfer(signer.key, target.key, amount);
|
||
invoke(
|
||
&ix,
|
||
&[signer.clone(), target.clone(), system_program.clone()],
|
||
)?;
|
||
Ok(())
|
||
}
|
||
|
||
fn read_sol_usd_price(price_update: &Account<PriceUpdateV2>, key: &Pubkey) -> Result<SolUsdPrice> {
|
||
let expected_oracle = Pubkey::from_str(settings::PYTH_SOL_USD_ACCOUNT)
|
||
.map_err(|_| error!(PaymentsError::InvalidOracleFeedConfig))?;
|
||
require_keys_eq!(expected_oracle, *key, PaymentsError::InvalidOracleAccount);
|
||
|
||
let feed_id = get_feed_id_from_hex(settings::PYTH_SOL_USD_FEED_ID)
|
||
.map_err(|_| error!(PaymentsError::InvalidOracleFeedConfig))?;
|
||
let clock = Clock::get()?;
|
||
let price = price_update
|
||
.get_price_no_older_than(&clock, settings::ORACLE_MAX_AGE_SECS, &feed_id)
|
||
.map_err(map_oracle_error)?;
|
||
price_to_ratio(price)
|
||
}
|
||
|
||
fn map_oracle_error(err: GetPriceError) -> anchor_lang::error::Error {
|
||
match err {
|
||
GetPriceError::PriceTooOld => error!(PaymentsError::OraclePriceTooOld),
|
||
GetPriceError::MismatchedFeedId => error!(PaymentsError::InvalidOracleFeed),
|
||
GetPriceError::InsufficientVerificationLevel => error!(PaymentsError::InvalidOraclePrice),
|
||
GetPriceError::FeedIdMustBe32Bytes | GetPriceError::FeedIdNonHexCharacter => {
|
||
error!(PaymentsError::InvalidOracleFeedConfig)
|
||
}
|
||
_ => error!(PaymentsError::InvalidOraclePrice),
|
||
}
|
||
}
|
||
|
||
fn price_to_ratio(price: Price) -> Result<SolUsdPrice> {
|
||
require!(price.price > 0, PaymentsError::InvalidOraclePrice);
|
||
let mut num = (price.price as u128)
|
||
.checked_mul(settings::USD_CENTS_SCALE as u128)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let mut den: u128 = 1;
|
||
|
||
if price.exponent >= 0 {
|
||
let pow = 10u128
|
||
.checked_pow(price.exponent as u32)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
num = num.checked_mul(pow).ok_or(error!(ErrCode::MathOverflow))?;
|
||
} else {
|
||
let pow = 10u128
|
||
.checked_pow((-price.exponent) as u32)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
den = den.checked_mul(pow).ok_or(error!(ErrCode::MathOverflow))?;
|
||
}
|
||
|
||
require!(num > 0 && den > 0, PaymentsError::InvalidOraclePrice);
|
||
Ok(SolUsdPrice {
|
||
price_num: num,
|
||
price_den: den,
|
||
})
|
||
}
|
||
|
||
fn lamports_to_usd_cents_floor(lamports: u64, price: &SolUsdPrice) -> Result<u64> {
|
||
let numerator = (lamports as u128)
|
||
.checked_mul(price.price_num)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let denominator = (settings::LAMPORTS_PER_SOL as u128)
|
||
.checked_mul(price.price_den)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
require!(denominator > 0, PaymentsError::InvalidOraclePrice);
|
||
let value = numerator / denominator;
|
||
u64::try_from(value).map_err(|_| error!(ErrCode::MathOverflow))
|
||
}
|
||
|
||
fn usd_cents_to_lamports_ceil(usd_cents: u64, price: &SolUsdPrice) -> Result<u64> {
|
||
require!(usd_cents > 0, PaymentsError::InvalidAmount);
|
||
require!(price.price_num > 0, PaymentsError::InvalidOraclePrice);
|
||
|
||
let numerator = (usd_cents as u128)
|
||
.checked_mul(settings::LAMPORTS_PER_SOL as u128)
|
||
.and_then(|v| v.checked_mul(price.price_den))
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let adjusted = numerator
|
||
.checked_add(price.price_num - 1)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
let value = adjusted / price.price_num;
|
||
u64::try_from(value).map_err(|_| error!(ErrCode::MathOverflow))
|
||
}
|
||
|
||
fn create_and_store_state<'info, T: AnchorSerialize>(
|
||
program_id: &Pubkey,
|
||
payer: &AccountInfo<'info>,
|
||
system_program: &AccountInfo<'info>,
|
||
pda: &AccountInfo<'info>,
|
||
seed: &[u8],
|
||
space: usize,
|
||
state: &T,
|
||
) -> Result<()> {
|
||
let (_, bump) = find_single_pda(program_id, seed);
|
||
create_state_with_seeds(
|
||
program_id,
|
||
payer,
|
||
system_program,
|
||
pda,
|
||
&[seed, &[bump]],
|
||
space,
|
||
state,
|
||
)
|
||
}
|
||
|
||
fn create_state_with_seeds<'info, T: AnchorSerialize>(
|
||
program_id: &Pubkey,
|
||
payer: &AccountInfo<'info>,
|
||
system_program: &AccountInfo<'info>,
|
||
pda: &AccountInfo<'info>,
|
||
seeds: &[&[u8]],
|
||
space: usize,
|
||
state: &T,
|
||
) -> Result<()> {
|
||
create_pda(pda, payer, system_program, program_id, seeds, space as u64)?;
|
||
write_state(pda, state)
|
||
}
|
||
|
||
fn write_state<T: AnchorSerialize>(pda: &AccountInfo, state: &T) -> Result<()> {
|
||
let bytes = state
|
||
.try_to_vec()
|
||
.map_err(|_| error!(ErrCode::DeserializationError))?;
|
||
write_to_pda(pda, &bytes)
|
||
}
|
||
|
||
fn read_state<T: AnchorDeserialize>(pda: &AccountInfo) -> Result<T> {
|
||
let raw = safe_read_pda(pda);
|
||
require!(!raw.is_empty(), PaymentsError::EmptyState);
|
||
let mut slice: &[u8] = &raw;
|
||
T::deserialize(&mut slice).map_err(|_| error!(ErrCode::DeserializationError))
|
||
}
|
||
|
||
fn available_vault_lamports(vault: &AccountInfo) -> Result<u64> {
|
||
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) -> Result<()> {
|
||
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 = vault_lamports
|
||
.checked_sub(amount)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
**recipient_lamports = recipient_lamports
|
||
.checked_add(amount)
|
||
.ok_or(error!(ErrCode::MathOverflow))?;
|
||
Ok(())
|
||
}
|
||
|
||
fn transfer_all_available_to_dao(vault: &AccountInfo, dao_wallet: &AccountInfo) -> Result<()> {
|
||
let available = available_vault_lamports(vault)?;
|
||
transfer_from_vault(vault, dao_wallet, available)
|
||
}
|