use bytemuck::{Pod, Zeroable};
use jito_bytemuck::{types::PodU64, AccountDeserialize, Discriminator};
use jito_jsm_core::get_epoch;
use jito_vault_sdk::error::VaultError;
use shank::ShankAccount;
use solana_program::{account_info::AccountInfo, msg, program_error::ProgramError, pubkey::Pubkey};
const RESERVED_SPACE_LEN: usize = 263;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Pod, Zeroable, AccountDeserialize, ShankAccount)]
#[repr(C)]
pub struct VaultStakerWithdrawalTicket {
pub vault: Pubkey,
pub staker: Pubkey,
pub base: Pubkey,
vrt_amount: PodU64,
slot_unstaked: PodU64,
pub bump: u8,
reserved: [u8; 263],
}
impl VaultStakerWithdrawalTicket {
pub fn new(
vault: Pubkey,
staker: Pubkey,
base: Pubkey,
vrt_amount: u64,
slot_unstaked: u64,
bump: u8,
) -> Self {
Self {
vault,
staker,
base,
vrt_amount: PodU64::from(vrt_amount),
slot_unstaked: PodU64::from(slot_unstaked),
bump,
reserved: [0; RESERVED_SPACE_LEN],
}
}
pub fn vrt_amount(&self) -> u64 {
self.vrt_amount.into()
}
pub fn slot_unstaked(&self) -> u64 {
self.slot_unstaked.into()
}
pub fn check_staker(&self, staker: &Pubkey) -> Result<(), VaultError> {
if self.staker.ne(staker) {
msg!("Staker is not the owner of the withdrawal ticket");
return Err(VaultError::VaultStakerWithdrawalTicketInvalidStaker);
}
Ok(())
}
pub fn is_withdrawable(&self, slot: u64, epoch_length: u64) -> Result<bool, ProgramError> {
let current_epoch = get_epoch(slot, epoch_length)?;
let epoch_unstaked = get_epoch(self.slot_unstaked(), epoch_length)?;
if current_epoch
<= epoch_unstaked
.checked_add(1)
.ok_or(VaultError::ArithmeticOverflow)?
{
Ok(false)
} else {
Ok(true)
}
}
pub fn seeds(vault: &Pubkey, base: &Pubkey) -> Vec<Vec<u8>> {
Vec::from_iter([
b"vault_staker_withdrawal_ticket".to_vec(),
vault.to_bytes().to_vec(),
base.to_bytes().to_vec(),
])
}
pub fn signing_seeds(&self, vault: &Pubkey) -> Vec<Vec<u8>> {
let mut vault_seeds = Self::seeds(vault, &self.base);
vault_seeds.push(vec![self.bump]);
vault_seeds
}
pub fn find_program_address(
program_id: &Pubkey,
vault: &Pubkey,
base: &Pubkey,
) -> (Pubkey, u8, Vec<Vec<u8>>) {
let seeds = Self::seeds(vault, base);
let seeds_iter: Vec<_> = seeds.iter().map(|s| s.as_slice()).collect();
let (pda, bump) = Pubkey::find_program_address(&seeds_iter, program_id);
(pda, bump, seeds)
}
pub fn load(
program_id: &Pubkey,
vault_staker_withdrawal_ticket: &AccountInfo,
vault: &AccountInfo,
expect_writable: bool,
) -> Result<(), ProgramError> {
if vault_staker_withdrawal_ticket.owner.ne(program_id) {
msg!("Vault staker withdrawal ticket has an invalid owner");
return Err(ProgramError::InvalidAccountOwner);
}
if vault_staker_withdrawal_ticket.data_is_empty() {
msg!("Vault staker withdrawal ticket data is empty");
return Err(ProgramError::InvalidAccountData);
}
if expect_writable && !vault_staker_withdrawal_ticket.is_writable {
msg!("Vault staker withdrawal ticket is not writable");
return Err(ProgramError::InvalidAccountData);
}
if vault_staker_withdrawal_ticket.data.borrow()[0].ne(&Self::DISCRIMINATOR) {
msg!("Vault staker withdrawal ticket discriminator is invalid");
return Err(ProgramError::InvalidAccountData);
}
let vault_staker_withdrawal_ticket_data = vault_staker_withdrawal_ticket.data.borrow();
let ticket = Self::try_from_slice_unchecked(&vault_staker_withdrawal_ticket_data)?;
if ticket.vault.ne(vault.key) {
msg!("Vault must match the vault in the withdrawal ticket");
return Err(ProgramError::InvalidAccountData);
}
let seeds = ticket.signing_seeds(vault.key);
let seed_slices: Vec<&[u8]> = seeds.iter().map(|seed| seed.as_slice()).collect();
let expected_pubkey = Pubkey::create_program_address(&seed_slices, program_id)?;
if vault_staker_withdrawal_ticket.key.ne(&expected_pubkey) {
msg!("Vault staker withdrawal ticket is not at the correct PDA");
return Err(ProgramError::InvalidAccountData);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vault_staker_withdrawal_ticket_no_padding() {
let vault_staker_withdrawal_ticket_size =
std::mem::size_of::<VaultStakerWithdrawalTicket>();
let sum_of_fields = size_of::<Pubkey>() + size_of::<Pubkey>() + size_of::<Pubkey>() + size_of::<PodU64>() + size_of::<PodU64>() + size_of::<u8>() + RESERVED_SPACE_LEN; assert_eq!(vault_staker_withdrawal_ticket_size, sum_of_fields);
}
}