Created
November 16, 2025 13:00
-
-
Save mgild/049abe83a2d0369aca9550e12bdc3365 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use std::{ | |
| collections::HashMap, | |
| str::FromStr, | |
| sync::{atomic::AtomicU64, Arc}, | |
| }; | |
| use anyhow::Context; | |
| use anyhow_ext::{anyhow, Result}; | |
| use async_recursion::async_recursion; | |
| use borsh::de::BorshDeserialize; | |
| use flat_fee_interface::ProgramState; | |
| use lazy_static::lazy_static; | |
| use rust_decimal::Decimal; | |
| use s_controller_interface::PoolState; | |
| use s_sol_val_calc_prog_aggregate::{ | |
| KnownLstSolValCalc, LidoLstSolValCalc, LstSolValCalc, MarinadeLstSolValCalc, | |
| MutableLstSolValCalc, SanctumSplLstSolValCalc, SanctumSplMultiLstSolValCalc, SplLstSolValCalc, | |
| SplLstSolValCalcInitKeys, WsolLstSolValCalc, | |
| }; | |
| use sanctum_lst_list::{ | |
| PoolInfo::{SPool, SanctumSpl, SanctumSplMulti, Spl}, | |
| SanctumLst, SanctumLstList, SplPoolAccounts, | |
| }; | |
| use solana_client::nonblocking::rpc_client::RpcClient; | |
| use solana_program::pubkey::Pubkey as ProgramPubkey; | |
| use solana_readonly_account::keyed; | |
| use solana_sdk::{ | |
| account::{Account, AccountSharedData}, | |
| native_token::LAMPORTS_PER_SOL, | |
| program_pack::Pack, | |
| pubkey::Pubkey, | |
| sysvar, | |
| sysvar::clock::Clock, | |
| }; | |
| use spl_token::state::Mint; | |
| use tracing::info; | |
| use crate::{ | |
| oracle_job::SanctumLstPriceTask, TaskInterface, TaskInterfaceAsync, TaskOutput, TaskResult, | |
| TaskRunnerContext, | |
| }; | |
| lazy_static! { | |
| static ref INF_MINT: Pubkey = | |
| Pubkey::from_str("5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm").unwrap(); | |
| static ref SOL_MINT: Pubkey = | |
| Pubkey::from_str("So11111111111111111111111111111111111111112").unwrap(); | |
| } | |
| pub async fn get_epoch(client: &RpcClient) -> Result<Arc<AtomicU64>> { | |
| let account_data = client.get_account(&sysvar::clock::ID).await; | |
| let account_data = account_data?; | |
| let clock = bincode::deserialize::<Clock>(&account_data.data)?; | |
| let epoch = Arc::new(AtomicU64::new(clock.epoch)); | |
| Ok(epoch) | |
| } | |
| fn overwrite_shared_current_epoch(calc: &mut KnownLstSolValCalc) -> Result<()> { | |
| match calc { | |
| KnownLstSolValCalc::Lido(LidoLstSolValCalc { | |
| calc: Some(calc), | |
| shared_current_epoch, | |
| }) => *shared_current_epoch = Arc::new(AtomicU64::new(calc.computed_in_epoch)), | |
| KnownLstSolValCalc::Spl(SplLstSolValCalc { | |
| calc: Some(calc), | |
| shared_current_epoch, | |
| .. | |
| }) | |
| | KnownLstSolValCalc::SanctumSpl(SanctumSplLstSolValCalc(SplLstSolValCalc { | |
| calc: Some(calc), | |
| shared_current_epoch, | |
| .. | |
| })) | |
| | KnownLstSolValCalc::SanctumSplMulti(SanctumSplMultiLstSolValCalc(SplLstSolValCalc { | |
| calc: Some(calc), | |
| shared_current_epoch, | |
| .. | |
| })) => *shared_current_epoch = Arc::new(AtomicU64::new(calc.last_update_epoch)), | |
| KnownLstSolValCalc::Marinade(_) | KnownLstSolValCalc::Wsol(_) => {} | |
| _ => return Err(anyhow!("Failed to overwrite shared current epoch")), | |
| }; | |
| Ok(()) | |
| } | |
| #[allow(dead_code)] | |
| async fn get_accounts(client: &RpcClient, keys: Vec<Pubkey>) -> Result<HashMap<Pubkey, Account>> { | |
| let mut account_datas = Vec::new(); | |
| for key_chunk in keys.chunks(100) { | |
| let chunk: Vec<Option<Account>> = client.get_multiple_accounts(key_chunk).await.unwrap(); | |
| account_datas.extend(chunk); | |
| } | |
| let mut data_map = HashMap::new(); | |
| for i in 0..keys.len() { | |
| let key = &keys[i]; | |
| let account = &account_datas[i]; | |
| if let Some(account) = account { | |
| data_map.insert(*key, account.clone()); | |
| } else { | |
| info!("Failed to get SPool required account {key}"); | |
| } | |
| } | |
| Ok(data_map) | |
| } | |
| pub async fn calc_from_calculator( | |
| client: &RpcClient, | |
| mut calc: KnownLstSolValCalc, | |
| skip_epoch_check: bool, | |
| ) -> Result<Decimal> { | |
| let accounts = calc.get_accounts_to_update(); | |
| let account_datas: Vec<Account> = client | |
| .get_multiple_accounts(&accounts.clone()) | |
| .await | |
| .map_err(|_| anyhow!("Failed to get accounts"))? | |
| .into_iter() | |
| .flatten() | |
| .collect::<Vec<_>>(); | |
| if accounts.len() != account_datas.len() { | |
| return Err(anyhow!( | |
| "Account data mismatch, failed to fetch all needed calc accounts" | |
| )); | |
| } | |
| let mut data_map = HashMap::new(); | |
| for i in 0..accounts.len() { | |
| let key = &accounts[i]; | |
| let account_data = AccountSharedData::from(account_datas[i].clone()); | |
| let keyed_account = keyed::Keyed { | |
| pubkey: ProgramPubkey::new_from_array(key.to_bytes()), | |
| account: account_data, | |
| }; | |
| data_map.insert(*key, keyed_account); | |
| } | |
| calc.update(&data_map) | |
| .map_err(|_| anyhow!("Failed to update calc"))?; | |
| if skip_epoch_check { | |
| // Overwrite `shared_current_epoch` to bypass validation | |
| overwrite_shared_current_epoch(&mut calc)?; | |
| } | |
| let range = calc | |
| .lst_to_sol(LAMPORTS_PER_SOL) | |
| .map_err(|_| anyhow!("Failed to calculate lst_to_sol"))?; | |
| // Allow up to 1 for rounding | |
| if range.get_max() - range.get_min() > 1 { | |
| return Err(anyhow!("SanctumCalcError: Unexpected range")); | |
| } | |
| Ok(Decimal::from(range.get_max()) / Decimal::from(LAMPORTS_PER_SOL)) | |
| } | |
| pub async fn calc_from_lst( | |
| client: &RpcClient, | |
| lst: Option<&SanctumLst>, | |
| skip_epoch_check: bool, | |
| ) -> Result<Decimal> { | |
| let lst = lst.ok_or(anyhow!("SanctumLstNotFound"))?; | |
| let calc; | |
| let epoch = get_epoch(client).await?; | |
| if let SanctumSplMulti(SplPoolAccounts { pool, .. }) = lst.pool { | |
| calc = KnownLstSolValCalc::SanctumSplMulti(SanctumSplMultiLstSolValCalc::from_keys( | |
| SplLstSolValCalcInitKeys { | |
| lst_mint: lst.mint, | |
| stake_pool_addr: pool, | |
| }, | |
| epoch, | |
| )); | |
| } else if let SanctumSpl(SplPoolAccounts { pool, .. }) = lst.pool { | |
| calc = KnownLstSolValCalc::SanctumSpl(SanctumSplLstSolValCalc::from_keys( | |
| SplLstSolValCalcInitKeys { | |
| lst_mint: lst.mint, | |
| stake_pool_addr: pool, | |
| }, | |
| epoch, | |
| )); | |
| } else if let Spl(SplPoolAccounts { pool, .. }) = lst.pool { | |
| calc = KnownLstSolValCalc::Spl(SplLstSolValCalc::from_keys( | |
| SplLstSolValCalcInitKeys { | |
| lst_mint: lst.mint, | |
| stake_pool_addr: pool, | |
| }, | |
| epoch, | |
| )); | |
| } else if let sanctum_lst_list::PoolInfo::Marinade = lst.pool { | |
| calc = MarinadeLstSolValCalc::default().into(); | |
| } else if let sanctum_lst_list::PoolInfo::Lido = lst.pool { | |
| calc = LidoLstSolValCalc { | |
| shared_current_epoch: epoch, | |
| calc: None, | |
| } | |
| .into(); | |
| } else if let sanctum_lst_list::PoolInfo::ReservePool = lst.pool { | |
| calc = WsolLstSolValCalc {}.into(); | |
| } else if let SPool(_) = lst.pool { | |
| if lst.mint != *INF_MINT { | |
| return Err(anyhow!("InvalidSpool")); | |
| } | |
| return inf_quote(client).await; | |
| } else { | |
| return Err(anyhow!("SanctumLstNotSpl")); | |
| } | |
| calc_from_calculator(client, calc, skip_epoch_check).await | |
| } | |
| pub async fn inf_quote(client: &RpcClient) -> Result<Decimal> { | |
| let sanctum_state_key = | |
| Pubkey::from_str("DpWzqkAVNjgdiLCDVy1M3XWGBPjJ11h177kNX8NywYNE").unwrap(); | |
| let inf_pool_key = Pubkey::from_str("AYhux5gJzCoeoc1PoJ1VxwPDe22RwcvpHviLDD1oCGvW").unwrap(); | |
| let sanctum_state_data = client | |
| .get_account_data(&sanctum_state_key) | |
| .await | |
| .context("Failed to get sanctum state data")?; | |
| let inf_pool_data = client | |
| .get_account_data(&inf_pool_key) | |
| .await | |
| .context("Failed to get inf pool data")?; | |
| let pool_state = PoolState::deserialize(&mut inf_pool_data.as_slice()) | |
| .context("Failed to deserialize pool state")?; | |
| let sanctum_state = ProgramState::deserialize(&mut sanctum_state_data.as_slice()) | |
| .context("Failed to deserialize sanctum state")?; | |
| let inf_mint_key = pool_state.lp_token_mint; | |
| let inf_mint_data = client | |
| .get_account_data(&inf_mint_key) | |
| .await | |
| .context("Failed to get inf mint data")?; | |
| let inf_mint = Mint::unpack(inf_mint_data.as_slice()).context("Failed to unpack inf mint")?; | |
| let ten_thousand = Decimal::from(10_000); | |
| let _lp_withdrawal_fee = ten_thousand | |
| .checked_sub(Decimal::from(sanctum_state.lp_withdrawal_fee_bps)) | |
| .context("Failed to calculate lp_withdrawal_fee")? | |
| .checked_div(ten_thousand) | |
| .context("Failed to calculate lp_withdrawal_fee")?; | |
| let total_sol_value = Decimal::from(pool_state.total_sol_value); | |
| let lp_total_supply = Decimal::from(inf_mint.supply); | |
| let ratio = total_sol_value | |
| .checked_div(lp_total_supply) | |
| .context("Failed to calculate ratio")?; | |
| // let rate_with_fee = ratio.checked_mul(lp_withdrawal_fee) | |
| // .context("Failed to calculate rate with fee")?; | |
| // Ok(rate_with_fee) | |
| Ok(ratio) | |
| } | |
| pub fn to_lst<'a>(lsts: &'a [SanctumLst], key: &'a Pubkey) -> Option<&'a SanctumLst> { | |
| lsts.iter().find(|x| x.mint == *key) | |
| } | |
| #[async_recursion] | |
| pub async fn sanctum_task(ctx: &TaskRunnerContext, task: &SanctumLstPriceTask) -> TaskResult { | |
| let lst_mint = Pubkey::from_str(&task.lst_mint.clone().unwrap_or_default()) | |
| .map_err(|_| anyhow!("SanctumPriceTask:InvalidMint"))?; | |
| let skip_epoch_check = task.skip_epoch_check.unwrap_or(false); | |
| let SanctumLstList { | |
| sanctum_lst_list: lsts, | |
| } = SanctumLstList::load(); | |
| let client = ctx.mainnet_rpc(); | |
| let price = calc_from_lst(&client, to_lst(&lsts, &lst_mint), skip_epoch_check).await?; | |
| Ok(TaskOutput::Num(price)) | |
| } | |
| impl TaskInterface for SanctumLstPriceTask { | |
| fn children(&self) -> Vec<crate::protos::OracleJob> { | |
| Vec::new() | |
| } | |
| fn uses_input(&self) -> bool { | |
| false | |
| } | |
| } | |
| #[async_trait::async_trait] | |
| impl TaskInterfaceAsync for SanctumLstPriceTask { | |
| async fn execute<'a>(&'a self, ctx: &'a mut TaskRunnerContext) -> TaskResult { | |
| sanctum_task(ctx, self).await | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use std::str::FromStr; | |
| use sanctum_lst_list::SanctumLstList; | |
| use super::*; | |
| use crate::{oracle_job::SanctumLstPriceTask, test_utils}; | |
| #[tokio::test] | |
| async fn sanctum_inf_base() { | |
| let ctx = test_utils::get_test_task_runner_context(true); | |
| let task = SanctumLstPriceTask { | |
| lst_mint: Some("5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm".to_string()), | |
| skip_epoch_check: Some(false), | |
| }; | |
| let output = sanctum_task(&ctx, &task).await.unwrap(); | |
| println!("output: {:#?}", output); | |
| } | |
| #[tokio::test] | |
| async fn sanctum_test_base() { | |
| let ctx = test_utils::get_test_task_runner_context(true); | |
| let task = SanctumLstPriceTask { | |
| lst_mint: Some("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn".to_string()), | |
| skip_epoch_check: Some(false), | |
| }; | |
| let output = sanctum_task(&ctx, &task).await.unwrap(); | |
| println!("output: {:#?}", output); | |
| } | |
| #[tokio::test] | |
| async fn sanctum_test_1() -> Result<()> { | |
| let client = test_utils::get_test_rpc_client(true); | |
| let jup_sol_mint = Pubkey::from_str("jupSoLaHXQiZZTSfEWMTRRgpnyFm8f6sZdosWBjx93v").unwrap(); | |
| let _path_mint = Pubkey::from_str("pathdXw4He1Xk3eX84pDdDZnGKEme3GivBamGCVPZ5a").unwrap(); | |
| let jito_sol_mint = | |
| Pubkey::from_str("J1toso1uCk3RLmjorhTtrVwY9HJ7X8V9yYac6Y7kGCPn").unwrap(); | |
| let inf_mint = Pubkey::from_str("5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm").unwrap(); | |
| let _lido_sol_mint = LidoLstSolValCalc { | |
| calc: None, | |
| shared_current_epoch: get_epoch(&client).await?, | |
| } | |
| .lst_mint(); | |
| let _msol_mint = MarinadeLstSolValCalc::default().lst_mint(); | |
| let wsol_mint = WsolLstSolValCalc {}.lst_mint(); | |
| let SanctumLstList { | |
| sanctum_lst_list: lsts, | |
| } = SanctumLstList::load(); | |
| let _skip_epoch_check = true; | |
| println!( | |
| "price: {:#?}", | |
| calc_from_lst(&client, to_lst(&lsts, &jup_sol_mint), false).await? | |
| ); | |
| println!( | |
| "price: {:#?}", | |
| calc_from_lst(&client, to_lst(&lsts, &wsol_mint), false).await? | |
| ); | |
| println!( | |
| "price: {:#?}", | |
| calc_from_lst(&client, to_lst(&lsts, &jito_sol_mint), false).await? | |
| ); | |
| println!( | |
| "price: {:#?}", | |
| calc_from_lst(&client, to_lst(&lsts, &inf_mint), false).await? | |
| ); | |
| Ok(()) | |
| } | |
| #[tokio::test] | |
| async fn sanctum_test_mango() { | |
| let ctx = test_utils::get_test_task_runner_context(true); | |
| let task = SanctumLstPriceTask { | |
| lst_mint: Some("StPsoHokZryePePFV8N7iXvfEmgUoJ87rivABX7gaW6".to_string()), | |
| skip_epoch_check: Some(false), | |
| }; | |
| let output = sanctum_task(&ctx, &task).await.unwrap(); | |
| println!("output: {:#?}", output); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment