use crate::generated::accounts::{fetch_all_maybe_pending_withdraw, fetch_state};
use crate::generated::shared;
use crate::{
find_orca_vault_address, find_pending_withdraw_pda, find_state_address, PendingWithdraw, State,
};
use anyhow::Context;
use solana_client::rpc_client::RpcClient;
use solana_program_error::ProgramError;
use solana_program_pack::Pack;
use solana_pubkey::Pubkey;
use spl_token_interface::state::{Account, Mint};
use std::str::FromStr;
pub const DEFAULT_MAX_WITHDRAWALS_TO_SEARCH: u8 = 15;
pub const ORCA_MINT_ADDRESS: &str = "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE";
pub const XORCA_MINT_ADDRESS: &str = "xorcaYqbXUNz3474ubUMJAdu2xgPsew3rUCe5ughT3N";
pub const TOKEN_PROGRAM_ADDRESS: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
#[derive(Debug, Clone)]
pub struct VaultState {
pub address: Pubkey,
pub owner: Pubkey,
pub mint: Pubkey,
pub amount: u64,
}
#[derive(Debug, Clone)]
pub struct StakingExchangeRate {
pub numerator: u64,
pub denominator: u64,
}
pub fn fetch_state_account_data(rpc: &RpcClient) -> Result<State, ProgramError> {
let (state_address, _) = find_state_address()?;
let decoded = fetch_state(rpc, &state_address)?;
Ok(decoded.data)
}
pub fn fetch_state_account_cool_down_period_s(rpc: &RpcClient) -> Result<i64, ProgramError> {
let state = fetch_state_account_data(rpc)?;
Ok(state.cool_down_period_s)
}
pub fn fetch_pending_withdraws_for_staker(
rpc: &RpcClient,
staker: &Pubkey,
max_withdrawals_to_search: Option<u8>,
) -> Result<Vec<PendingWithdraw>, ProgramError> {
let max_withdrawals_to_search =
max_withdrawals_to_search.unwrap_or(DEFAULT_MAX_WITHDRAWALS_TO_SEARCH);
let mut addresses = Vec::new();
for i in 0..max_withdrawals_to_search {
let (address, _) = find_pending_withdraw_pda(staker, &i)?;
addresses.push(address);
}
let maybe_accounts = fetch_all_maybe_pending_withdraw(rpc, &addresses)?;
let pending_withdraws: Vec<PendingWithdraw> = maybe_accounts
.into_iter()
.filter_map(|maybe_account| match maybe_account {
shared::MaybeAccount::Exists(decoded) => Some(decoded.data),
shared::MaybeAccount::NotFound(_) => None,
})
.collect();
Ok(pending_withdraws)
}
pub fn fetch_vault_state(rpc: &RpcClient) -> Result<VaultState, anyhow::Error> {
let (state_address, _) = find_state_address()?;
let token_program = Pubkey::from_str(TOKEN_PROGRAM_ADDRESS)?;
let orca_mint = Pubkey::from_str(ORCA_MINT_ADDRESS)?;
let (vault_address, _) = find_orca_vault_address(&state_address, &token_program, &orca_mint)?;
let account = rpc.get_account(&vault_address)?;
let token_account = Account::unpack(&account.data)?;
Ok(VaultState {
address: vault_address,
owner: token_account.owner,
mint: token_account.mint,
amount: token_account.amount,
})
}
pub fn fetch_xorca_mint_supply(rpc: &RpcClient) -> Result<u64, anyhow::Error> {
let xorca_mint = Pubkey::from_str(XORCA_MINT_ADDRESS)?;
let account = rpc.get_account(&xorca_mint)?;
let mint = Mint::unpack(&account.data)?;
Ok(mint.supply)
}
pub fn fetch_staking_exchange_rate(rpc: &RpcClient) -> Result<StakingExchangeRate, anyhow::Error> {
let state = fetch_state_account_data(rpc)?;
let vault = fetch_vault_state(rpc)?;
let numerator = vault
.amount
.checked_sub(state.escrowed_orca_amount)
.with_context(|| "Vault amount is less than escrowed amount")?;
let denominator = fetch_xorca_mint_supply(rpc)?;
Ok(StakingExchangeRate {
numerator,
denominator,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::make_mocked_client_from_fixtures;
use solana_pubkey::Pubkey;
use std::path::PathBuf;
use std::str::FromStr;
#[test]
fn test_mint_addresses_are_valid() {
assert!(Pubkey::from_str(ORCA_MINT_ADDRESS).is_ok());
assert!(Pubkey::from_str(XORCA_MINT_ADDRESS).is_ok());
assert!(Pubkey::from_str(TOKEN_PROGRAM_ADDRESS).is_ok());
}
fn fixtures_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("src")
.join("test_utils")
.join("fixtures")
.join("accounts.json")
}
#[test]
fn test_fetch_state_and_cooldown_unit() {
let rpc = make_mocked_client_from_fixtures(&fixtures_path()).expect("mock rpc");
let state = fetch_state_account_data(&rpc).expect("state");
assert_eq!(state.cool_down_period_s, 3600);
assert_eq!(state.escrowed_orca_amount, 5_000_000_000);
let cd = fetch_state_account_cool_down_period_s(&rpc).expect("cooldown");
assert_eq!(cd, 3600);
}
#[test]
fn test_fetch_vault_state_and_xorca_supply_unit() {
let rpc = make_mocked_client_from_fixtures(&fixtures_path()).expect("mock rpc");
let vs = fetch_vault_state(&rpc).expect("vault");
let orca_mint = Pubkey::from_str(ORCA_MINT_ADDRESS).unwrap();
assert_eq!(vs.mint, orca_mint);
assert_eq!(vs.amount, 8_000_000_000);
let supply = fetch_xorca_mint_supply(&rpc).expect("supply");
assert_eq!(supply, 10_000_000_000);
}
#[test]
fn test_fetch_pending_withdraws_some_missing_unit() {
let rpc = make_mocked_client_from_fixtures(&fixtures_path()).expect("mock rpc");
let staker =
Pubkey::from_str("9GJeoK3Qn2p8Rq6i7AbQm7x1SE7K75Eo3VdFUSf1xZ4i").expect("staker");
let pw = fetch_pending_withdraws_for_staker(&rpc, &staker, Some(6)).expect("pending");
assert_eq!(pw.len(), 3);
assert!(pw
.iter()
.any(|p| p.withdraw_index == 2 && p.unstaker == staker));
assert!(!pw.iter().any(|p| p.withdraw_index == 1));
}
#[test]
fn test_fetch_pending_withdraws_respects_max_limit_unit() {
let rpc = make_mocked_client_from_fixtures(&fixtures_path()).expect("mock rpc");
let staker = Pubkey::from_str("9GJeoK3Qn2p8Rq6i7AbQm7x1SE7K75Eo3VdFUSf1xZ4i").unwrap();
let pw = fetch_pending_withdraws_for_staker(&rpc, &staker, Some(2)).expect("pending");
assert_eq!(pw.len(), 1, "only index 0 exists within first two indices");
assert!(pw.iter().any(|p| p.withdraw_index == 0));
assert!(!pw.iter().any(|p| p.withdraw_index == 2));
}
#[test]
fn test_staking_exchange_rate_ok_unit() {
let rpc = make_mocked_client_from_fixtures(&fixtures_path()).expect("mock rpc");
let rate = fetch_staking_exchange_rate(&rpc).expect("rate");
assert_eq!(rate.numerator, 3_000_000_000);
assert_eq!(rate.denominator, 10_000_000_000);
}
}