From db98ba5edf48b06adce485c97a8b12cd0bd8c9d5 Mon Sep 17 00:00:00 2001 From: Lou-Kamades <128186011+Lou-Kamades@users.noreply.github.com> Date: Tue, 23 Jan 2024 10:26:31 -0600 Subject: [PATCH] Use fallback oracles in Rust client (#838) * rename usd_opt to usdc_opt in OracleAccountInfos * use fallbacks when fetching bank+ price in AccountFetcher struct * feat: add derive_fallback_oracle_keys to MangoGroupContext * test: properly assert failure CU in test_health_compute_tokens_fallback_oracles * provide fallback oracle accounts in the rust client * liquidator: update for fallback oracles * set fallback config in mango services * support fallback oracles in rust settler + keeper * fix send error related to fetching fallbacks dynamically in tcs_start * drop derive_fallback_oracle_keys_sync * add fetch_multiple_accounts to AccountFetcher trait * revert client::new() api * deriving oracle keys uses account_fetcher * use client helpers for deriving health_check account_metas * add health_cache helper to mango client * add get_slot to account_fetcher * lint * cached account fetcher only fetches uncached accounts * ensure keeper client does not use cached oracles for staleness checks * address minor review comments * create unique job keys for CachedAccountFetcher.fetch_multiple_accounts * fmt * improve hashing in CachedAccountFetcher.fetch_multiple_accounts --------- Co-authored-by: Christian Kamm --- bin/keeper/src/main.rs | 24 +- bin/liquidator/src/liquidate.rs | 13 +- bin/liquidator/src/rebalance.rs | 1 + bin/liquidator/src/trigger_tcs.rs | 11 +- bin/service-mango-pnl/src/main.rs | 11 +- bin/settler/src/settle.rs | 20 +- bin/settler/src/tcs_start.rs | 18 +- lib/client/src/account_fetcher.rs | 82 ++++++ lib/client/src/chain_data_fetcher.rs | 49 +++- lib/client/src/client.rs | 239 ++++++++++++------ lib/client/src/context.rs | 169 ++++++++++++- lib/client/src/gpa.rs | 21 +- lib/client/src/health_cache.rs | 43 +++- lib/client/src/jupiter/v4.rs | 2 + lib/client/src/jupiter/v6.rs | 2 + lib/client/src/perp_pnl.rs | 8 +- .../mango-v4/src/health/account_retriever.rs | 10 +- programs/mango-v4/src/state/oracle.rs | 20 +- programs/mango-v4/src/state/orca_cpi.rs | 28 ++ .../tests/cases/test_health_compute.rs | 2 +- 20 files changed, 610 insertions(+), 163 deletions(-) diff --git a/bin/keeper/src/main.rs b/bin/keeper/src/main.rs index 4e006363d..02f374f19 100644 --- a/bin/keeper/src/main.rs +++ b/bin/keeper/src/main.rs @@ -7,7 +7,9 @@ use std::time::Duration; use anchor_client::Cluster; use clap::{Parser, Subcommand}; -use mango_v4_client::{keypair_from_cli, Client, MangoClient, TransactionBuilderConfig}; +use mango_v4_client::{ + keypair_from_cli, Client, FallbackOracleConfig, MangoClient, TransactionBuilderConfig, +}; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::pubkey::Pubkey; use tokio::time; @@ -98,19 +100,21 @@ async fn main() -> Result<(), anyhow::Error> { let mango_client = Arc::new( MangoClient::new_for_existing_account( - Client::new( - cluster, - commitment, - owner.clone(), - Some(Duration::from_secs(cli.timeout)), - TransactionBuilderConfig { + Client::builder() + .cluster(cluster) + .commitment(commitment) + .fee_payer(Some(owner.clone())) + .timeout(Duration::from_secs(cli.timeout)) + .transaction_builder_config(TransactionBuilderConfig { prioritization_micro_lamports: (cli.prioritization_micro_lamports > 0) .then_some(cli.prioritization_micro_lamports), compute_budget_per_instruction: None, - }, - ), + }) + .fallback_oracle_config(FallbackOracleConfig::Never) + .build() + .unwrap(), cli.mango_account, - owner.clone(), + owner, ) .await?, ); diff --git a/bin/liquidator/src/liquidate.rs b/bin/liquidator/src/liquidate.rs index 2ddd43606..b0d280207 100644 --- a/bin/liquidator/src/liquidate.rs +++ b/bin/liquidator/src/liquidate.rs @@ -4,7 +4,7 @@ use std::time::Duration; use itertools::Itertools; use mango_v4::health::{HealthCache, HealthType}; use mango_v4::state::{MangoAccountValue, PerpMarketIndex, Side, TokenIndex, QUOTE_TOKEN_INDEX}; -use mango_v4_client::{chain_data, health_cache, MangoClient}; +use mango_v4_client::{chain_data, MangoClient}; use solana_sdk::signature::Signature; use futures::{stream, StreamExt, TryStreamExt}; @@ -155,10 +155,7 @@ impl<'a> LiquidateHelper<'a> { .await .context("getting liquidator account")?; liqor.ensure_perp_position(*perp_market_index, QUOTE_TOKEN_INDEX)?; - let mut health_cache = - health_cache::new(&self.client.context, self.account_fetcher, &liqor) - .await - .context("health cache")?; + let mut health_cache = self.client.health_cache(&liqor).await.expect("always ok"); let quote_bank = self .client .first_bank(QUOTE_TOKEN_INDEX) @@ -589,7 +586,8 @@ pub async fn maybe_liquidate_account( let liqor_min_health_ratio = I80F48::from_num(config.min_health_ratio); let account = account_fetcher.fetch_mango_account(pubkey)?; - let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account) + let health_cache = mango_client + .health_cache(&account) .await .context("creating health cache 1")?; let maint_health = health_cache.health(HealthType::Maint); @@ -607,7 +605,8 @@ pub async fn maybe_liquidate_account( // This is -- unfortunately -- needed because the websocket streams seem to not // be great at providing timely updates to the account data. let account = account_fetcher.fetch_fresh_mango_account(pubkey).await?; - let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account) + let health_cache = mango_client + .health_cache(&account) .await .context("creating health cache 2")?; if !health_cache.is_liquidatable() { diff --git a/bin/liquidator/src/rebalance.rs b/bin/liquidator/src/rebalance.rs index 01c750e84..23757976a 100644 --- a/bin/liquidator/src/rebalance.rs +++ b/bin/liquidator/src/rebalance.rs @@ -520,6 +520,7 @@ impl Rebalancer { }; let counters = perp_pnl::fetch_top( &self.mango_client.context, + &self.mango_client.client.config().fallback_oracle_config, self.account_fetcher.as_ref(), perp_position.market_index, direction, diff --git a/bin/liquidator/src/trigger_tcs.rs b/bin/liquidator/src/trigger_tcs.rs index 5f1a1caa6..c8b914698 100644 --- a/bin/liquidator/src/trigger_tcs.rs +++ b/bin/liquidator/src/trigger_tcs.rs @@ -11,7 +11,7 @@ use mango_v4::{ i80f48::ClampToInt, state::{Bank, MangoAccountValue, TokenConditionalSwap, TokenIndex}, }; -use mango_v4_client::{chain_data, health_cache, jupiter, MangoClient, TransactionBuilder}; +use mango_v4_client::{chain_data, jupiter, MangoClient, TransactionBuilder}; use anyhow::Context as AnyhowContext; use solana_sdk::{signature::Signature, signer::Signer}; @@ -665,8 +665,9 @@ impl Context { liqee_old: &MangoAccountValue, tcs_id: u64, ) -> anyhow::Result> { - let fetcher = self.account_fetcher.as_ref(); - let health_cache = health_cache::new(&self.mango_client.context, fetcher, liqee_old) + let health_cache = self + .mango_client + .health_cache(liqee_old) .await .context("creating health cache 1")?; if health_cache.is_liquidatable() { @@ -685,7 +686,9 @@ impl Context { return Ok(None); } - let health_cache = health_cache::new(&self.mango_client.context, fetcher, &liqee) + let health_cache = self + .mango_client + .health_cache(&liqee) .await .context("creating health cache 2")?; if health_cache.is_liquidatable() { diff --git a/bin/service-mango-pnl/src/main.rs b/bin/service-mango-pnl/src/main.rs index c2f2f385c..49869f379 100644 --- a/bin/service-mango-pnl/src/main.rs +++ b/bin/service-mango-pnl/src/main.rs @@ -21,7 +21,8 @@ use fixed::types::I80F48; use mango_feeds_connector::metrics::*; use mango_v4::state::{MangoAccount, MangoAccountValue, PerpMarketIndex}; use mango_v4_client::{ - chain_data, health_cache, AccountFetcher, Client, MangoGroupContext, TransactionBuilderConfig, + chain_data, health_cache, AccountFetcher, Client, FallbackOracleConfig, MangoGroupContext, + TransactionBuilderConfig, }; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::{account::ReadableAccount, signature::Keypair}; @@ -52,7 +53,13 @@ async fn compute_pnl( account_fetcher: Arc, account: &MangoAccountValue, ) -> anyhow::Result> { - let health_cache = health_cache::new(&context, account_fetcher.as_ref(), account).await?; + let health_cache = health_cache::new( + &context, + &FallbackOracleConfig::Dynamic, + account_fetcher.as_ref(), + account, + ) + .await?; let pnls = account .active_perp_positions() diff --git a/bin/settler/src/settle.rs b/bin/settler/src/settle.rs index 51b91d5fd..3534091d7 100644 --- a/bin/settler/src/settle.rs +++ b/bin/settler/src/settle.rs @@ -5,9 +5,7 @@ use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::HealthType; use mango_v4::state::{OracleAccountInfos, PerpMarket, PerpMarketIndex}; -use mango_v4_client::{ - chain_data, health_cache, MangoClient, PreparedInstructions, TransactionBuilder, -}; +use mango_v4_client::{chain_data, MangoClient, PreparedInstructions, TransactionBuilder}; use solana_sdk::address_lookup_table_account::AddressLookupTableAccount; use solana_sdk::commitment_config::CommitmentConfig; use solana_sdk::signature::Signature; @@ -113,7 +111,8 @@ impl SettlementState { continue; } - let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account) + let health_cache = mango_client + .health_cache(&account) .await .context("creating health cache")?; let liq_end_health = health_cache.health(HealthType::LiquidationEnd); @@ -304,11 +303,14 @@ impl<'a> SettleBatchProcessor<'a> { ) -> anyhow::Result> { let a_value = self.account_fetcher.fetch_mango_account(&account_a)?; let b_value = self.account_fetcher.fetch_mango_account(&account_b)?; - let new_ixs = self.mango_client.perp_settle_pnl_instruction( - self.perp_market_index, - (&account_a, &a_value), - (&account_b, &b_value), - )?; + let new_ixs = self + .mango_client + .perp_settle_pnl_instruction( + self.perp_market_index, + (&account_a, &a_value), + (&account_b, &b_value), + ) + .await?; let previous = self.instructions.clone(); self.instructions.append(new_ixs.clone()); diff --git a/bin/settler/src/tcs_start.rs b/bin/settler/src/tcs_start.rs index d481e3946..b10a2ac12 100644 --- a/bin/settler/src/tcs_start.rs +++ b/bin/settler/src/tcs_start.rs @@ -113,14 +113,18 @@ impl State { } // Clear newly created token positions, so the liqor account is mostly empty - for token_index in startable_chunk.iter().map(|(_, _, ti)| *ti).unique() { + let new_token_pos_indices = startable_chunk + .iter() + .map(|(_, _, ti)| *ti) + .unique() + .collect_vec(); + for token_index in new_token_pos_indices { let mint = mango_client.context.token(token_index).mint; - instructions.append(mango_client.token_withdraw_instructions( - &liqor_account, - mint, - u64::MAX, - false, - )?); + let ix = mango_client + .token_withdraw_instructions(&liqor_account, mint, u64::MAX, false) + .await?; + + instructions.append(ix) } let txsig = match mango_client diff --git a/lib/client/src/account_fetcher.rs b/lib/client/src/account_fetcher.rs index 6c1273a0f..9b769a56c 100644 --- a/lib/client/src/account_fetcher.rs +++ b/lib/client/src/account_fetcher.rs @@ -11,10 +11,14 @@ use anchor_lang::AccountDeserialize; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_sdk::account::{AccountSharedData, ReadableAccount}; +use solana_sdk::hash::Hash; +use solana_sdk::hash::Hasher; use solana_sdk::pubkey::Pubkey; use mango_v4::state::MangoAccountValue; +use crate::gpa; + #[async_trait::async_trait] pub trait AccountFetcher: Sync + Send { async fn fetch_raw_account(&self, address: &Pubkey) -> anyhow::Result; @@ -29,6 +33,13 @@ pub trait AccountFetcher: Sync + Send { program: &Pubkey, discriminator: [u8; 8], ) -> anyhow::Result>; + + async fn fetch_multiple_accounts( + &self, + keys: &[Pubkey], + ) -> anyhow::Result>; + + async fn get_slot(&self) -> anyhow::Result; } // Can't be in the trait, since then it would no longer be object-safe... @@ -100,6 +111,17 @@ impl AccountFetcher for RpcAccountFetcher { .map(|(pk, acc)| (pk, acc.into())) .collect::>()) } + + async fn fetch_multiple_accounts( + &self, + keys: &[Pubkey], + ) -> anyhow::Result> { + gpa::fetch_multiple_accounts(&self.rpc, keys).await + } + + async fn get_slot(&self) -> anyhow::Result { + Ok(self.rpc.get_slot().await?) + } } struct CoalescedAsyncJob { @@ -138,6 +160,8 @@ struct AccountCache { keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec>, account_jobs: CoalescedAsyncJob>, + multiple_accounts_jobs: + CoalescedAsyncJob>>, program_accounts_jobs: CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result>>, } @@ -261,4 +285,62 @@ impl AccountFetcher for CachedAccountFetcher { )), } } + + async fn fetch_multiple_accounts( + &self, + keys: &[Pubkey], + ) -> anyhow::Result> { + let fetch_job = { + let mut cache = self.cache.lock().unwrap(); + let mut missing_keys: Vec = keys + .iter() + .filter(|k| !cache.accounts.contains_key(k)) + .cloned() + .collect(); + if missing_keys.len() == 0 { + return Ok(keys + .iter() + .map(|pk| (*pk, cache.accounts.get(&pk).unwrap().clone())) + .collect::>()); + } + + let self_copy = self.clone(); + missing_keys.sort(); + let mut hasher = Hasher::default(); + for key in missing_keys.iter() { + hasher.hash(key.as_ref()); + } + let job_key = hasher.result(); + cache + .multiple_accounts_jobs + .run_coalesced(job_key.clone(), async move { + let result = self_copy + .fetcher + .fetch_multiple_accounts(&missing_keys) + .await; + let mut cache = self_copy.cache.lock().unwrap(); + cache.multiple_accounts_jobs.remove(&job_key); + + if let Ok(results) = result.as_ref() { + for (key, account) in results { + cache.accounts.insert(*key, account.clone()); + } + } + result + }) + }; + + match fetch_job.get().await { + Ok(v) => Ok(v.clone()), + // Can't clone the stored error, so need to stringize it + Err(err) => Err(anyhow::format_err!( + "fetch error in CachedAccountFetcher: {:?}", + err + )), + } + } + + async fn get_slot(&self) -> anyhow::Result { + self.fetcher.get_slot().await + } } diff --git a/lib/client/src/chain_data_fetcher.rs b/lib/client/src/chain_data_fetcher.rs index 856453619..2fe9313c9 100644 --- a/lib/client/src/chain_data_fetcher.rs +++ b/lib/client/src/chain_data_fetcher.rs @@ -8,7 +8,10 @@ use anchor_lang::Discriminator; use fixed::types::I80F48; use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy}; -use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, OracleAccountInfos}; +use mango_v4::state::{ + pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, Bank, MangoAccount, MangoAccountValue, + OracleAccountInfos, +}; use anyhow::Context; @@ -64,12 +67,34 @@ impl AccountFetcher { pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> { let bank: Bank = self.fetch(bank)?; - let oracle = self.fetch_raw(&bank.oracle)?; - let oracle_acc = &KeyedAccountSharedData::new(bank.oracle, oracle.into()); - let price = bank.oracle_price(&OracleAccountInfos::from_reader(oracle_acc), None)?; + let oracle_data = self.fetch_raw(&bank.oracle)?; + let oracle = &KeyedAccountSharedData::new(bank.oracle, oracle_data.into()); + + let fallback_opt = self.fetch_keyed_account_data(bank.fallback_oracle)?; + let sol_opt = self.fetch_keyed_account_data(pyth_mainnet_sol_oracle::ID)?; + let usdc_opt = self.fetch_keyed_account_data(pyth_mainnet_usdc_oracle::ID)?; + + let oracle_acc_infos = OracleAccountInfos { + oracle, + fallback_opt: fallback_opt.as_ref(), + usdc_opt: usdc_opt.as_ref(), + sol_opt: sol_opt.as_ref(), + }; + let price = bank.oracle_price(&oracle_acc_infos, None)?; Ok((bank, price)) } + #[inline(always)] + fn fetch_keyed_account_data( + &self, + key: Pubkey, + ) -> anyhow::Result> { + Ok(self + .fetch_raw(&key) + .ok() + .map(|data| KeyedAccountSharedData::new(key, data))) + } + pub fn fetch_bank_price(&self, bank: &Pubkey) -> anyhow::Result { self.fetch_bank_and_price(bank).map(|(_, p)| p) } @@ -217,4 +242,20 @@ impl crate::AccountFetcher for AccountFetcher { }) .collect::>()) } + + async fn fetch_multiple_accounts( + &self, + keys: &[Pubkey], + ) -> anyhow::Result> { + let chain_data = self.chain_data.read().unwrap(); + Ok(keys + .iter() + .map(|pk| (*pk, chain_data.account(pk).unwrap().account.clone())) + .collect::>()) + } + + async fn get_slot(&self) -> anyhow::Result { + let chain_data = self.chain_data.read().unwrap(); + Ok(chain_data.newest_processed_slot()) + } } diff --git a/lib/client/src/client.rs b/lib/client/src/client.rs index fd051d26e..2caa5fb19 100644 --- a/lib/client/src/client.rs +++ b/lib/client/src/client.rs @@ -18,11 +18,19 @@ use tracing::*; use mango_v4::accounts_ix::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; +use mango_v4::health::HealthCache; use mango_v4::state::{ Bank, Group, MangoAccountValue, OracleAccountInfos, PerpMarket, PerpMarketIndex, PlaceOrderType, SelfTradeBehavior, Serum3MarketIndex, Side, TokenIndex, INSURANCE_TOKEN_INDEX, }; +use crate::account_fetcher::*; +use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig}; +use crate::context::MangoGroupContext; +use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; +use crate::health_cache; +use crate::util::PreparedInstructions; +use crate::{jupiter, util}; use solana_address_lookup_table_program::state::AddressLookupTable; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_client::SerializableTransaction; @@ -35,13 +43,6 @@ use solana_sdk::hash::Hash; use solana_sdk::signer::keypair; use solana_sdk::transaction::TransactionError; -use crate::account_fetcher::*; -use crate::confirm_transaction::{wait_for_transaction_confirmation, RpcConfirmTransactionConfig}; -use crate::context::MangoGroupContext; -use crate::gpa::{fetch_anchor_account, fetch_mango_accounts}; -use crate::util::PreparedInstructions; -use crate::{jupiter, util}; - use anyhow::Context; use solana_sdk::account::ReadableAccount; use solana_sdk::instruction::{AccountMeta, Instruction}; @@ -96,6 +97,10 @@ pub struct ClientConfig { #[builder(default = "\"\".into()")] pub jupiter_token: String, + /// Determines how fallback oracle accounts are provided to instructions. Defaults to Dynamic. + #[builder(default = "FallbackOracleConfig::Dynamic")] + pub fallback_oracle_config: FallbackOracleConfig, + /// If set, don't use `cluster` for sending transactions and send to all /// addresses configured here instead. #[builder(default = "None")] @@ -445,35 +450,65 @@ impl MangoClient { pub async fn derive_health_check_remaining_account_metas( &self, + account: &MangoAccountValue, affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, ) -> anyhow::Result<(Vec, u32)> { - let account = self.mango_account().await?; + let fallback_contexts = self + .context + .derive_fallback_oracle_keys( + &self.client.config.fallback_oracle_config, + &*self.account_fetcher, + ) + .await?; self.context.derive_health_check_remaining_account_metas( &account, affected_tokens, writable_banks, affected_perp_markets, + fallback_contexts, ) } - pub async fn derive_liquidation_health_check_remaining_account_metas( + pub async fn derive_health_check_remaining_account_metas_two_accounts( &self, - liqee: &MangoAccountValue, + account_1: &MangoAccountValue, + account_2: &MangoAccountValue, affected_tokens: &[TokenIndex], writable_banks: &[TokenIndex], ) -> anyhow::Result<(Vec, u32)> { - let account = self.mango_account().await?; + let fallback_contexts = self + .context + .derive_fallback_oracle_keys( + &self.client.config.fallback_oracle_config, + &*self.account_fetcher, + ) + .await?; + self.context .derive_health_check_remaining_account_metas_two_accounts( - &account, - liqee, + account_1, + account_2, affected_tokens, writable_banks, + fallback_contexts, ) } + pub async fn health_cache( + &self, + mango_account: &MangoAccountValue, + ) -> anyhow::Result { + health_cache::new( + &self.context, + &self.client.config.fallback_oracle_config, + &*self.account_fetcher, + mango_account, + ) + .await + } + pub async fn token_deposit( &self, mint: Pubkey, @@ -482,9 +517,15 @@ impl MangoClient { ) -> anyhow::Result { let token = self.context.token_by_mint(&mint)?; let token_index = token.token_index; + let mango_account = &self.mango_account().await?; let (health_check_metas, health_cu) = self - .derive_health_check_remaining_account_metas(vec![token_index], vec![], vec![]) + .derive_health_check_remaining_account_metas( + mango_account, + vec![token_index], + vec![], + vec![], + ) .await?; let ixs = PreparedInstructions::from_single( @@ -521,7 +562,7 @@ impl MangoClient { /// Creates token withdraw instructions for the MangoClient's account/owner. /// The `account` state is passed in separately so changes during the tx can be /// accounted for when deriving health accounts. - pub fn token_withdraw_instructions( + pub async fn token_withdraw_instructions( &self, account: &MangoAccountValue, mint: Pubkey, @@ -531,13 +572,9 @@ impl MangoClient { let token = self.context.token_by_mint(&mint)?; let token_index = token.token_index; - let (health_check_metas, health_cu) = - self.context.derive_health_check_remaining_account_metas( - account, - vec![token_index], - vec![], - vec![], - )?; + let (health_check_metas, health_cu) = self + .derive_health_check_remaining_account_metas(account, vec![token_index], vec![], vec![]) + .await?; let ixs = PreparedInstructions::from_vec( vec![ @@ -587,7 +624,9 @@ impl MangoClient { allow_borrow: bool, ) -> anyhow::Result { let account = self.mango_account().await?; - let ixs = self.token_withdraw_instructions(&account, mint, amount, allow_borrow)?; + let ixs = self + .token_withdraw_instructions(&account, mint, amount, allow_borrow) + .await?; self.send_and_confirm_owner_tx(ixs.to_instructions()).await } @@ -667,7 +706,7 @@ impl MangoClient { } #[allow(clippy::too_many_arguments)] - pub fn serum3_place_order_instruction( + pub async fn serum3_place_order_instruction( &self, account: &MangoAccountValue, market_index: Serum3MarketIndex, @@ -689,8 +728,8 @@ impl MangoClient { .open_orders; let (health_check_metas, health_cu) = self - .context - .derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; + .derive_health_check_remaining_account_metas(account, vec![], vec![], vec![]) + .await?; let payer_token = match side { Serum3Side::Bid => "e, @@ -762,18 +801,20 @@ impl MangoClient { ) -> anyhow::Result { let account = self.mango_account().await?; let market_index = self.context.serum3_market_index(name); - let ixs = self.serum3_place_order_instruction( - &account, - market_index, - side, - limit_price, - max_base_qty, - max_native_quote_qty_including_fees, - self_trade_behavior, - order_type, - client_order_id, - limit, - )?; + let ixs = self + .serum3_place_order_instruction( + &account, + market_index, + side, + limit_price, + max_base_qty, + max_native_quote_qty_including_fees, + self_trade_behavior, + order_type, + client_order_id, + limit, + ) + .await?; self.send_and_confirm_owner_tx(ixs.to_instructions()).await } @@ -899,10 +940,9 @@ impl MangoClient { let s3 = self.context.serum3(market_index); let base = self.context.serum3_base_token(market_index); let quote = self.context.serum3_quote_token(market_index); - let (health_remaining_ams, health_cu) = self - .context .derive_health_check_remaining_account_metas(liqee.1, vec![], vec![], vec![]) + .await .unwrap(); let limit = 5; @@ -990,7 +1030,7 @@ impl MangoClient { // #[allow(clippy::too_many_arguments)] - pub fn perp_place_order_instruction( + pub async fn perp_place_order_instruction( &self, account: &MangoAccountValue, market_index: PerpMarketIndex, @@ -1006,13 +1046,14 @@ impl MangoClient { self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { let perp = self.context.perp(market_index); - let (health_remaining_metas, health_cu) = - self.context.derive_health_check_remaining_account_metas( + let (health_remaining_metas, health_cu) = self + .derive_health_check_remaining_account_metas( account, vec![], vec![], vec![market_index], - )?; + ) + .await?; let ixs = PreparedInstructions::from_single( Instruction { @@ -1072,20 +1113,22 @@ impl MangoClient { self_trade_behavior: SelfTradeBehavior, ) -> anyhow::Result { let account = self.mango_account().await?; - let ixs = self.perp_place_order_instruction( - &account, - market_index, - side, - price_lots, - max_base_lots, - max_quote_lots, - client_order_id, - order_type, - reduce_only, - expiry_timestamp, - limit, - self_trade_behavior, - )?; + let ixs = self + .perp_place_order_instruction( + &account, + market_index, + side, + price_lots, + max_base_lots, + max_quote_lots, + client_order_id, + order_type, + reduce_only, + expiry_timestamp, + limit, + self_trade_behavior, + ) + .await?; self.send_and_confirm_owner_tx(ixs.to_instructions()).await } @@ -1127,9 +1170,10 @@ impl MangoClient { market_index: PerpMarketIndex, ) -> anyhow::Result { let perp = self.context.perp(market_index); + let mango_account = &self.mango_account().await?; let (health_check_metas, health_cu) = self - .derive_health_check_remaining_account_metas(vec![], vec![], vec![]) + .derive_health_check_remaining_account_metas(mango_account, vec![], vec![], vec![]) .await?; let ixs = PreparedInstructions::from_single( @@ -1157,7 +1201,7 @@ impl MangoClient { self.send_and_confirm_owner_tx(ixs.to_instructions()).await } - pub fn perp_settle_pnl_instruction( + pub async fn perp_settle_pnl_instruction( &self, market_index: PerpMarketIndex, account_a: (&Pubkey, &MangoAccountValue), @@ -1167,13 +1211,13 @@ impl MangoClient { let settlement_token = self.context.token(perp.settle_token_index); let (health_remaining_ams, health_cu) = self - .context .derive_health_check_remaining_account_metas_two_accounts( account_a.1, account_b.1, &[], &[], ) + .await .unwrap(); let ixs = PreparedInstructions::from_single( @@ -1210,7 +1254,9 @@ impl MangoClient { account_a: (&Pubkey, &MangoAccountValue), account_b: (&Pubkey, &MangoAccountValue), ) -> anyhow::Result { - let ixs = self.perp_settle_pnl_instruction(market_index, account_a, account_b)?; + let ixs = self + .perp_settle_pnl_instruction(market_index, account_a, account_b) + .await?; self.send_and_confirm_permissionless_tx(ixs.to_instructions()) .await } @@ -1223,8 +1269,8 @@ impl MangoClient { let perp = self.context.perp(market_index); let (health_remaining_ams, health_cu) = self - .context .derive_health_check_remaining_account_metas(liqee.1, vec![], vec![], vec![]) + .await .unwrap(); let limit = 5; @@ -1265,9 +1311,15 @@ impl MangoClient { ) -> anyhow::Result { let perp = self.context.perp(market_index); let settle_token_info = self.context.token(perp.settle_token_index); + let mango_account = &self.mango_account().await?; let (health_remaining_ams, health_cu) = self - .derive_liquidation_health_check_remaining_account_metas(liqee.1, &[], &[]) + .derive_health_check_remaining_account_metas_two_accounts( + mango_account, + liqee.1, + &[], + &[], + ) .await .unwrap(); @@ -1316,12 +1368,14 @@ impl MangoClient { ) .await?; + let mango_account = &self.mango_account().await?; let perp = self.context.perp(market_index); let settle_token_info = self.context.token(perp.settle_token_index); let insurance_token_info = self.context.token(INSURANCE_TOKEN_INDEX); let (health_remaining_ams, health_cu) = self - .derive_liquidation_health_check_remaining_account_metas( + .derive_health_check_remaining_account_metas_two_accounts( + mango_account, liqee.1, &[INSURANCE_TOKEN_INDEX], &[], @@ -1375,8 +1429,10 @@ impl MangoClient { liab_token_index: TokenIndex, max_liab_transfer: I80F48, ) -> anyhow::Result { + let mango_account = &self.mango_account().await?; let (health_remaining_ams, health_cu) = self - .derive_liquidation_health_check_remaining_account_metas( + .derive_health_check_remaining_account_metas_two_accounts( + mango_account, liqee.1, &[], &[asset_token_index, liab_token_index], @@ -1417,6 +1473,7 @@ impl MangoClient { liab_token_index: TokenIndex, max_liab_transfer: I80F48, ) -> anyhow::Result { + let mango_account = &self.mango_account().await?; let quote_token_index = 0; let quote_info = self.context.token(quote_token_index); @@ -1429,7 +1486,8 @@ impl MangoClient { .collect::>(); let (health_remaining_ams, health_cu) = self - .derive_liquidation_health_check_remaining_account_metas( + .derive_health_check_remaining_account_metas_two_accounts( + mango_account, liqee.1, &[INSURANCE_TOKEN_INDEX], &[quote_token_index, liab_token_index], @@ -1483,6 +1541,7 @@ impl MangoClient { min_taker_price: f32, extra_affected_tokens: &[TokenIndex], ) -> anyhow::Result { + let mango_account = &self.mango_account().await?; let (tcs_index, tcs) = liqee .1 .token_conditional_swap_by_id(token_conditional_swap_id)?; @@ -1493,7 +1552,8 @@ impl MangoClient { .copied() .collect_vec(); let (health_remaining_ams, health_cu) = self - .derive_liquidation_health_check_remaining_account_metas( + .derive_health_check_remaining_account_metas_two_accounts( + mango_account, liqee.1, &affected_tokens, &[tcs.buy_token_index, tcs.sell_token_index], @@ -1538,13 +1598,19 @@ impl MangoClient { account: (&Pubkey, &MangoAccountValue), token_conditional_swap_id: u64, ) -> anyhow::Result { + let mango_account = &self.mango_account().await?; let (tcs_index, tcs) = account .1 .token_conditional_swap_by_id(token_conditional_swap_id)?; let affected_tokens = vec![tcs.buy_token_index, tcs.sell_token_index]; let (health_remaining_ams, health_cu) = self - .derive_health_check_remaining_account_metas(vec![], affected_tokens, vec![]) + .derive_health_check_remaining_account_metas( + mango_account, + vec![], + affected_tokens, + vec![], + ) .await .unwrap(); @@ -1578,20 +1644,21 @@ impl MangoClient { // health region - pub fn health_region_begin_instruction( + pub async fn health_region_begin_instruction( &self, account: &MangoAccountValue, affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, ) -> anyhow::Result { - let (health_remaining_metas, _health_cu) = - self.context.derive_health_check_remaining_account_metas( + let (health_remaining_metas, _health_cu) = self + .derive_health_check_remaining_account_metas( account, affected_tokens, writable_banks, affected_perp_markets, - )?; + ) + .await?; let ix = Instruction { program_id: mango_v4::id(), @@ -1617,20 +1684,21 @@ impl MangoClient { )) } - pub fn health_region_end_instruction( + pub async fn health_region_end_instruction( &self, account: &MangoAccountValue, affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, ) -> anyhow::Result { - let (health_remaining_metas, health_cu) = - self.context.derive_health_check_remaining_account_metas( + let (health_remaining_metas, health_cu) = self + .derive_health_check_remaining_account_metas( account, affected_tokens, writable_banks, affected_perp_markets, - )?; + ) + .await?; let ix = Instruction { program_id: mango_v4::id(), @@ -1857,6 +1925,23 @@ impl TransactionSize { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum FallbackOracleConfig { + /// No fallback oracles + Never, + /// Only provided fallback oracles are used + Fixed(Vec), + /// The account_fetcher checks for stale oracles and uses fallbacks only for stale oracles + Dynamic, + /// Every possible fallback oracle (may cause serious issues with the 64 accounts-per-tx limit) + All, +} +impl Default for FallbackOracleConfig { + fn default() -> Self { + FallbackOracleConfig::Dynamic + } +} + #[derive(Copy, Clone, Debug, Default)] pub struct TransactionBuilderConfig { /// adds a SetComputeUnitPrice instruction in front if none exists diff --git a/lib/client/src/context.rs b/lib/client/src/context.rs index fca1afcf5..3b8267b67 100644 --- a/lib/client/src/context.rs +++ b/lib/client/src/context.rs @@ -4,15 +4,20 @@ use anchor_client::ClientError; use anchor_lang::__private::bytemuck; -use mango_v4::state::{ - Group, MangoAccountValue, PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS, +use mango_v4::{ + accounts_zerocopy::{KeyedAccountReader, KeyedAccountSharedData}, + state::{ + determine_oracle_type, load_whirlpool_state, oracle_state_unchecked, Group, + MangoAccountValue, OracleAccountInfos, OracleConfig, OracleConfigParams, OracleType, + PerpMarketIndex, Serum3MarketIndex, TokenIndex, MAX_BANKS, + }, }; use fixed::types::I80F48; use futures::{stream, StreamExt, TryStreamExt}; use itertools::Itertools; -use crate::gpa::*; +use crate::{gpa::*, AccountFetcher, FallbackOracleConfig}; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_sdk::account::Account; @@ -28,9 +33,10 @@ pub struct TokenContext { pub oracle: Pubkey, pub banks: [Pubkey; MAX_BANKS], pub vaults: [Pubkey; MAX_BANKS], - pub fallback_oracle: Pubkey, + pub fallback_context: FallbackOracleContext, pub mint_info_address: Pubkey, pub decimals: u8, + pub oracle_config: OracleConfig, } impl TokenContext { @@ -56,6 +62,18 @@ impl TokenContext { } } +#[derive(Clone, PartialEq, Eq)] +pub struct FallbackOracleContext { + pub key: Pubkey, + // only used for CLMM fallback oracles, otherwise Pubkey::default + pub quote_key: Pubkey, +} +impl FallbackOracleContext { + pub fn keys(&self) -> Vec { + vec![self.key, self.quote_key] + } +} + #[derive(Clone, PartialEq, Eq)] pub struct Serum3MarketContext { pub address: Pubkey, @@ -101,6 +119,7 @@ pub struct ComputeEstimates { pub cu_per_serum3_order_cancel: u32, pub cu_per_perp_order_match: u32, pub cu_per_perp_order_cancel: u32, + pub cu_per_oracle_fallback: u32, } impl Default for ComputeEstimates { @@ -118,25 +137,36 @@ impl Default for ComputeEstimates { cu_per_perp_order_match: 7_000, // measured around 3.5k, see test_perp_compute cu_per_perp_order_cancel: 7_000, + // measured around 2k, see test_health_compute_tokens_fallback_oracles + cu_per_oracle_fallback: 2000, } } } impl ComputeEstimates { - pub fn health_for_counts(&self, tokens: usize, perps: usize, serums: usize) -> u32 { + pub fn health_for_counts( + &self, + tokens: usize, + perps: usize, + serums: usize, + fallbacks: usize, + ) -> u32 { let tokens: u32 = tokens.try_into().unwrap(); let perps: u32 = perps.try_into().unwrap(); let serums: u32 = serums.try_into().unwrap(); + let fallbacks: u32 = fallbacks.try_into().unwrap(); tokens * self.health_cu_per_token + perps * self.health_cu_per_perp + serums * self.health_cu_per_serum + + fallbacks * self.cu_per_oracle_fallback } - pub fn health_for_account(&self, account: &MangoAccountValue) -> u32 { + pub fn health_for_account(&self, account: &MangoAccountValue, num_fallbacks: usize) -> u32 { self.health_for_counts( account.active_token_positions().count(), account.active_perp_positions().count(), account.active_serum3_orders().count(), + num_fallbacks, ) } } @@ -227,8 +257,12 @@ impl MangoGroupContext { decimals: u8::MAX, banks: mi.banks, vaults: mi.vaults, - fallback_oracle: mi.fallback_oracle, oracle: mi.oracle, + fallback_context: FallbackOracleContext { + key: mi.fallback_oracle, + quote_key: Pubkey::default(), + }, + oracle_config: OracleConfigParams::default().to_oracle_config(), group: mi.group, mint: mi.mint, }, @@ -236,14 +270,23 @@ impl MangoGroupContext { }) .collect::>(); - // reading the banks is only needed for the token names and decimals + // reading the banks is only needed for the token names, decimals and oracle configs // FUTURE: either store the names on MintInfo as well, or maybe don't store them at all // because they are in metaplex? let bank_tuples = fetch_banks(rpc, program, group).await?; - for (_, bank) in bank_tuples { + let fallback_keys: Vec = bank_tuples + .iter() + .map(|tup| tup.1.fallback_oracle) + .collect(); + let fallback_oracle_accounts = fetch_multiple_accounts(rpc, &fallback_keys[..]).await?; + for (index, (_, bank)) in bank_tuples.iter().enumerate() { let token = tokens.get_mut(&bank.token_index).unwrap(); token.name = bank.name().into(); token.decimals = bank.mint_decimals; + token.oracle_config = bank.oracle_config; + let (key, acc_info) = fallback_oracle_accounts[index].clone(); + token.fallback_context.quote_key = + get_fallback_quote_key(&KeyedAccountSharedData::new(key, acc_info)); } assert!(tokens.values().all(|t| t.decimals != u8::MAX)); @@ -357,6 +400,7 @@ impl MangoGroupContext { affected_tokens: Vec, writable_banks: Vec, affected_perp_markets: Vec, + fallback_contexts: HashMap, ) -> anyhow::Result<(Vec, u32)> { let mut account = account.clone(); for affected_token_index in affected_tokens.iter().chain(writable_banks.iter()) { @@ -370,6 +414,7 @@ impl MangoGroupContext { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; let mut oracles = vec![]; + let mut fallbacks = vec![]; for position in account.active_token_positions() { let token = self.token(position.token_index); banks.push(( @@ -377,6 +422,9 @@ impl MangoGroupContext { writable_banks.iter().any(|&ti| ti == position.token_index), )); oracles.push(token.oracle); + if let Some(fallback_context) = fallback_contexts.get(&token.oracle) { + fallbacks.extend(fallback_context.keys()); + } } let serum_oos = account.active_serum3_orders().map(|&s| s.open_orders); @@ -386,6 +434,14 @@ impl MangoGroupContext { let perp_oracles = account .active_perp_positions() .map(|&pa| self.perp(pa.market_index).oracle); + // FUTURE: implement fallback oracles for perps + + let fallback_oracles: Vec = fallbacks + .into_iter() + .unique() + .filter(|key| !oracles.contains(key) && key != &Pubkey::default()) + .collect(); + let fallbacks_len = fallback_oracles.len(); let to_account_meta = |pubkey| AccountMeta { pubkey, @@ -404,9 +460,12 @@ impl MangoGroupContext { .chain(perp_markets.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) + .chain(fallback_oracles.into_iter().map(to_account_meta)) .collect(); - let cu = self.compute_estimates.health_for_account(&account); + let cu = self + .compute_estimates + .health_for_account(&account, fallbacks_len); Ok((accounts, cu)) } @@ -417,10 +476,12 @@ impl MangoGroupContext { account2: &MangoAccountValue, affected_tokens: &[TokenIndex], writable_banks: &[TokenIndex], + fallback_contexts: HashMap, ) -> anyhow::Result<(Vec, u32)> { // figure out all the banks/oracles that need to be passed for the health check let mut banks = vec![]; let mut oracles = vec![]; + let mut fallbacks = vec![]; let token_indexes = account2 .active_token_positions() @@ -434,6 +495,9 @@ impl MangoGroupContext { let writable_bank = writable_banks.iter().contains(&token_index); banks.push((token.first_bank(), writable_bank)); oracles.push(token.oracle); + if let Some(fallback_context) = fallback_contexts.get(&token.oracle) { + fallbacks.extend(fallback_context.keys()); + } } let serum_oos = account2 @@ -452,6 +516,14 @@ impl MangoGroupContext { let perp_oracles = perp_market_indexes .iter() .map(|&index| self.perp(index).oracle); + // FUTURE: implement fallback oracles for perps + + let fallback_oracles: Vec = fallbacks + .into_iter() + .unique() + .filter(|key| !oracles.contains(key) && key != &Pubkey::default()) + .collect(); + let fallbacks_len = fallback_oracles.len(); let to_account_meta = |pubkey| AccountMeta { pubkey, @@ -470,6 +542,7 @@ impl MangoGroupContext { .chain(perp_markets.map(to_account_meta)) .chain(perp_oracles.map(to_account_meta)) .chain(serum_oos.map(to_account_meta)) + .chain(fallback_oracles.into_iter().map(to_account_meta)) .collect(); // Since health is likely to be computed separately for both accounts, we don't use the @@ -490,10 +563,12 @@ impl MangoGroupContext { account1_token_count, account1.active_perp_positions().count(), account1.active_serum3_orders().count(), + fallbacks_len, ) + self.compute_estimates.health_for_counts( account2_token_count, account2.active_perp_positions().count(), account2.active_serum3_orders().count(), + fallbacks_len, ); Ok((accounts, cu)) @@ -554,6 +629,61 @@ impl MangoGroupContext { let new_perp_markets = fetch_perp_markets(rpc, mango_v4::id(), self.group).await?; Ok(new_perp_markets.len() > self.perp_markets.len()) } + + /// Returns a map of oracle pubkey -> FallbackOracleContext + pub async fn derive_fallback_oracle_keys( + &self, + fallback_oracle_config: &FallbackOracleConfig, + account_fetcher: &dyn AccountFetcher, + ) -> anyhow::Result> { + // FUTURE: implement for perp oracles as well + let fallbacks_by_oracle = match fallback_oracle_config { + FallbackOracleConfig::Never => HashMap::new(), + FallbackOracleConfig::Fixed(keys) => self + .tokens + .iter() + .filter(|token| { + token.1.fallback_context.key != Pubkey::default() + && keys.contains(&token.1.fallback_context.key) + }) + .map(|t| (t.1.oracle, t.1.fallback_context.clone())) + .collect(), + FallbackOracleConfig::All => self + .tokens + .iter() + .filter(|token| token.1.fallback_context.key != Pubkey::default()) + .map(|t| (t.1.oracle, t.1.fallback_context.clone())) + .collect(), + FallbackOracleConfig::Dynamic => { + let tokens_by_oracle: HashMap = + self.tokens.iter().map(|t| (t.1.oracle, t.1)).collect(); + let oracle_keys: Vec = + tokens_by_oracle.values().map(|b| b.oracle).collect(); + let oracle_accounts = account_fetcher + .fetch_multiple_accounts(&oracle_keys) + .await?; + let now_slot = account_fetcher.get_slot().await?; + + let mut stale_oracles_with_fallbacks = vec![]; + for (key, acc) in oracle_accounts { + let token = tokens_by_oracle.get(&key).unwrap(); + let state = oracle_state_unchecked( + &OracleAccountInfos::from_reader(&KeyedAccountSharedData::new(key, acc)), + token.decimals, + )?; + let oracle_is_valid = state + .check_confidence_and_maybe_staleness(&token.oracle_config, Some(now_slot)); + if oracle_is_valid.is_err() && token.fallback_context.key != Pubkey::default() { + stale_oracles_with_fallbacks + .push((token.oracle, token.fallback_context.clone())); + } + } + stale_oracles_with_fallbacks.into_iter().collect() + } + }; + + Ok(fallbacks_by_oracle) + } } fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey { @@ -567,3 +697,22 @@ async fn fetch_raw_account(rpc: &RpcClientAsync, address: Pubkey) -> Result Pubkey { + let maybe_key = match determine_oracle_type(acc_info).ok() { + Some(oracle_type) => match oracle_type { + OracleType::OrcaCLMM => match load_whirlpool_state(acc_info).ok() { + Some(whirlpool) => whirlpool.get_quote_oracle().ok(), + None => None, + }, + _ => None, + }, + None => None, + }; + + maybe_key.unwrap_or_else(|| Pubkey::default()) +} diff --git a/lib/client/src/gpa.rs b/lib/client/src/gpa.rs index 5dbd1106c..e96aa5418 100644 --- a/lib/client/src/gpa.rs +++ b/lib/client/src/gpa.rs @@ -1,11 +1,11 @@ use anchor_lang::{AccountDeserialize, Discriminator}; - use mango_v4::state::{Bank, MangoAccount, MangoAccountValue, MintInfo, PerpMarket, Serum3Market}; use solana_account_decoder::UiAccountEncoding; use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync; use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig}; use solana_client::rpc_filter::{Memcmp, RpcFilterType}; +use solana_sdk::account::AccountSharedData; use solana_sdk::pubkey::Pubkey; pub async fn fetch_mango_accounts( @@ -129,3 +129,22 @@ pub async fn fetch_perp_markets( ) .await } + +pub async fn fetch_multiple_accounts( + rpc: &RpcClientAsync, + keys: &[Pubkey], +) -> anyhow::Result> { + let config = RpcAccountInfoConfig { + encoding: Some(UiAccountEncoding::Base64), + ..RpcAccountInfoConfig::default() + }; + Ok(rpc + .get_multiple_accounts_with_config(keys, config) + .await? + .value + .into_iter() + .zip(keys.iter()) + .filter(|(maybe_acc, _)| maybe_acc.is_some()) + .map(|(acc, key)| (*key, acc.unwrap().into())) + .collect()) +} diff --git a/lib/client/src/health_cache.rs b/lib/client/src/health_cache.rs index 14716fe51..47a176f54 100644 --- a/lib/client/src/health_cache.rs +++ b/lib/client/src/health_cache.rs @@ -1,22 +1,32 @@ -use crate::{AccountFetcher, MangoGroupContext}; +use crate::{AccountFetcher, FallbackOracleConfig, MangoGroupContext}; use anyhow::Context; use futures::{stream, StreamExt, TryStreamExt}; use mango_v4::accounts_zerocopy::KeyedAccountSharedData; use mango_v4::health::{FixedOrderAccountRetriever, HealthCache}; -use mango_v4::state::MangoAccountValue; +use mango_v4::state::{pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, MangoAccountValue}; +use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; pub async fn new( context: &MangoGroupContext, - account_fetcher: &impl AccountFetcher, + fallback_config: &FallbackOracleConfig, + account_fetcher: &dyn AccountFetcher, account: &MangoAccountValue, ) -> anyhow::Result { let active_token_len = account.active_token_positions().count(); let active_perp_len = account.active_perp_positions().count(); - let (metas, _health_cu) = - context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; + let fallback_keys = context + .derive_fallback_oracle_keys(fallback_config, account_fetcher) + .await?; + let (metas, _health_cu) = context.derive_health_check_remaining_account_metas( + account, + vec![], + vec![], + vec![], + fallback_keys, + )?; let accounts: anyhow::Result> = stream::iter(metas.iter()) .then(|meta| async { Ok(KeyedAccountSharedData::new( @@ -34,9 +44,13 @@ pub async fn new( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, - begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts - usd_oracle_index: None, - sol_oracle_index: None, + begin_fallback_oracles: metas.len(), + usdc_oracle_index: metas + .iter() + .position(|m| m.pubkey == pyth_mainnet_usdc_oracle::ID), + sol_oracle_index: metas + .iter() + .position(|m| m.pubkey == pyth_mainnet_sol_oracle::ID), }; let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); mango_v4::health::new_health_cache(&account.borrow(), &retriever, now_ts) @@ -51,8 +65,13 @@ pub fn new_sync( let active_token_len = account.active_token_positions().count(); let active_perp_len = account.active_perp_positions().count(); - let (metas, _health_cu) = - context.derive_health_check_remaining_account_metas(account, vec![], vec![], vec![])?; + let (metas, _health_cu) = context.derive_health_check_remaining_account_metas( + account, + vec![], + vec![], + vec![], + HashMap::new(), + )?; let accounts = metas .iter() .map(|meta| { @@ -70,8 +89,8 @@ pub fn new_sync( begin_perp: active_token_len * 2, begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: None, - begin_fallback_oracles: metas.len(), // TODO: add support for fallback oracle accounts - usd_oracle_index: None, + begin_fallback_oracles: metas.len(), + usdc_oracle_index: None, sol_oracle_index: None, }; let now_ts = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); diff --git a/lib/client/src/jupiter/v4.rs b/lib/client/src/jupiter/v4.rs index 770ddf3e9..85bbb6eea 100644 --- a/lib/client/src/jupiter/v4.rs +++ b/lib/client/src/jupiter/v4.rs @@ -228,6 +228,7 @@ impl<'a> JupiterV4<'a> { .collect::>(); let owner = self.mango_client.owner(); + let account = &self.mango_client.mango_account().await?; let token_ams = [source_token.mint, target_token.mint] .into_iter() @@ -252,6 +253,7 @@ impl<'a> JupiterV4<'a> { let (health_ams, _health_cu) = self .mango_client .derive_health_check_remaining_account_metas( + account, vec![source_token.token_index, target_token.token_index], vec![source_token.token_index, target_token.token_index], vec![], diff --git a/lib/client/src/jupiter/v6.rs b/lib/client/src/jupiter/v6.rs index 09ccd6cf1..1d79371d9 100644 --- a/lib/client/src/jupiter/v6.rs +++ b/lib/client/src/jupiter/v6.rs @@ -237,6 +237,7 @@ impl<'a> JupiterV6<'a> { .collect::>(); let owner = self.mango_client.owner(); + let account = &self.mango_client.mango_account().await?; let token_ams = [source_token.mint, target_token.mint] .into_iter() @@ -259,6 +260,7 @@ impl<'a> JupiterV6<'a> { let (health_ams, _health_cu) = self .mango_client .derive_health_check_remaining_account_metas( + account, vec![source_token.token_index, target_token.token_index], vec![source_token.token_index, target_token.token_index], vec![], diff --git a/lib/client/src/perp_pnl.rs b/lib/client/src/perp_pnl.rs index 86bd3de33..7d76f8918 100644 --- a/lib/client/src/perp_pnl.rs +++ b/lib/client/src/perp_pnl.rs @@ -17,6 +17,7 @@ pub enum Direction { /// Note: keep in sync with perp.ts:getSettlePnlCandidates pub async fn fetch_top( context: &crate::context::MangoGroupContext, + fallback_config: &FallbackOracleConfig, account_fetcher: &impl AccountFetcher, perp_market_index: PerpMarketIndex, direction: Direction, @@ -91,9 +92,10 @@ pub async fn fetch_top( } else { I80F48::ZERO }; - let perp_max_settle = crate::health_cache::new(context, account_fetcher, &acc) - .await? - .perp_max_settle(perp_market.settle_token_index)?; + let perp_max_settle = + crate::health_cache::new(context, fallback_config, account_fetcher, &acc) + .await? + .perp_max_settle(perp_market.settle_token_index)?; let settleable_pnl = if perp_max_settle > 0 { (*pnl).max(-perp_max_settle) } else { diff --git a/programs/mango-v4/src/health/account_retriever.rs b/programs/mango-v4/src/health/account_retriever.rs index 8296bc2af..27bc0f14d 100644 --- a/programs/mango-v4/src/health/account_retriever.rs +++ b/programs/mango-v4/src/health/account_retriever.rs @@ -59,7 +59,7 @@ pub struct FixedOrderAccountRetriever { pub begin_serum3: usize, pub staleness_slot: Option, pub begin_fallback_oracles: usize, - pub usd_oracle_index: Option, + pub usdc_oracle_index: Option, pub sol_oracle_index: Option, } @@ -78,7 +78,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( ais.len(), expected_ais, active_token_len, active_token_len, active_perp_len, active_perp_len, active_serum3_len ); - let usd_oracle_index = ais[..] + let usdc_oracle_index = ais[..] .iter() .position(|o| o.key == &pyth_mainnet_usdc_oracle::ID); let sol_oracle_index = ais[..] @@ -93,7 +93,7 @@ pub fn new_fixed_order_account_retriever<'a, 'info>( begin_serum3: active_token_len * 2 + active_perp_len * 2, staleness_slot: Some(Clock::get()?.slot), begin_fallback_oracles: expected_ais, - usd_oracle_index, + usdc_oracle_index, sol_oracle_index, }) } @@ -139,7 +139,7 @@ impl FixedOrderAccountRetriever { OracleAccountInfos { oracle, fallback_opt, - usd_opt: self.usd_oracle_index.map(|i| &self.ais[i]), + usdc_opt: self.usdc_oracle_index.map(|i| &self.ais[i]), sol_opt: self.sol_oracle_index.map(|i| &self.ais[i]), } } @@ -324,7 +324,7 @@ impl<'a, 'info> ScannedBanksAndOracles<'a, 'info> { OracleAccountInfos { oracle, fallback_opt, - usd_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]), + usdc_opt: self.usd_oracle_index.map(|i| &self.fallback_oracles[i]), sol_opt: self.sol_oracle_index.map(|i| &self.fallback_oracles[i]), } } diff --git a/programs/mango-v4/src/state/oracle.rs b/programs/mango-v4/src/state/oracle.rs index f69e2d9fc..fc4106941 100644 --- a/programs/mango-v4/src/state/oracle.rs +++ b/programs/mango-v4/src/state/oracle.rs @@ -82,7 +82,7 @@ pub mod sol_mint_mainnet { } #[zero_copy] -#[derive(AnchorDeserialize, AnchorSerialize, Derivative)] +#[derive(AnchorDeserialize, AnchorSerialize, Derivative, PartialEq, Eq)] #[derivative(Debug)] pub struct OracleConfig { pub conf_filter: I80F48, @@ -94,7 +94,7 @@ const_assert_eq!(size_of::(), 16 + 8 + 72); const_assert_eq!(size_of::(), 96); const_assert_eq!(size_of::() % 8, 0); -#[derive(AnchorDeserialize, AnchorSerialize, Debug)] +#[derive(AnchorDeserialize, AnchorSerialize, Debug, Default)] pub struct OracleConfigParams { pub conf_filter: f32, pub max_staleness_slots: Option, @@ -278,7 +278,7 @@ fn get_pyth_state( pub struct OracleAccountInfos<'a, T: KeyedAccountReader> { pub oracle: &'a T, pub fallback_opt: Option<&'a T>, - pub usd_opt: Option<&'a T>, + pub usdc_opt: Option<&'a T>, pub sol_opt: Option<&'a T>, } @@ -287,7 +287,7 @@ impl<'a, T: KeyedAccountReader> OracleAccountInfos<'a, T> { OracleAccountInfos { oracle: acc_reader, fallback_opt: None, - usd_opt: None, + usdc_opt: None, sol_opt: None, } } @@ -406,9 +406,7 @@ fn oracle_state_unchecked_inner( OracleType::OrcaCLMM => { let whirlpool = load_whirlpool_state(oracle_info)?; - let inverted = whirlpool.token_mint_a == usdc_mint_mainnet::ID - || (whirlpool.token_mint_a == sol_mint_mainnet::ID - && whirlpool.token_mint_b != usdc_mint_mainnet::ID); + let inverted = whirlpool.is_inverted(); let quote_state = if inverted { quote_state_unchecked(acc_infos, &whirlpool.token_mint_a)? } else { @@ -441,7 +439,7 @@ fn quote_state_unchecked( ) -> Result { if quote_mint == &usdc_mint_mainnet::ID { let usd_feed = acc_infos - .usd_opt + .usdc_opt .ok_or_else(|| error!(MangoError::MissingFeedForCLMMOracle))?; let usd_state = get_pyth_state(usd_feed, QUOTE_DECIMALS as u8)?; return Ok(usd_state); @@ -590,13 +588,13 @@ mod tests { let usdc_ais = OracleAccountInfos { oracle: usdc_ai, fallback_opt: None, - usd_opt: None, + usdc_opt: None, sol_opt: None, }; let orca_ais = OracleAccountInfos { oracle: ai, fallback_opt: None, - usd_opt: Some(usdc_ai), + usdc_opt: Some(usdc_ai), sol_opt: None, }; let usdc = oracle_state_unchecked(&usdc_ais, usdc_decimals).unwrap(); @@ -635,7 +633,7 @@ mod tests { let oracle_infos = OracleAccountInfos { oracle: ai, fallback_opt: None, - usd_opt: None, + usdc_opt: None, sol_opt: None, }; assert!(oracle_state_unchecked(&oracle_infos, base_decimals) diff --git a/programs/mango-v4/src/state/orca_cpi.rs b/programs/mango-v4/src/state/orca_cpi.rs index 19c14d870..f33f7ff0b 100644 --- a/programs/mango-v4/src/state/orca_cpi.rs +++ b/programs/mango-v4/src/state/orca_cpi.rs @@ -3,6 +3,10 @@ use solana_program::pubkey::Pubkey; use crate::{accounts_zerocopy::KeyedAccountReader, error::MangoError}; +use super::{ + pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, sol_mint_mainnet, usdc_mint_mainnet, +}; + pub mod orca_mainnet_whirlpool { use solana_program::declare_id; declare_id!("whirLbMiicVdio4qvUfM5KAg6Ct8VwpYzGff3uctyCc"); @@ -18,6 +22,30 @@ pub struct WhirlpoolState { pub token_mint_b: Pubkey, // 32 } +impl WhirlpoolState { + pub fn is_inverted(&self) -> bool { + self.token_mint_a == usdc_mint_mainnet::ID + || (self.token_mint_a == sol_mint_mainnet::ID + && self.token_mint_b != usdc_mint_mainnet::ID) + } + + pub fn get_quote_oracle(&self) -> Result { + let mint = if self.is_inverted() { + self.token_mint_a + } else { + self.token_mint_b + }; + + if mint == usdc_mint_mainnet::ID { + return Ok(pyth_mainnet_usdc_oracle::ID); + } else if mint == sol_mint_mainnet::ID { + return Ok(pyth_mainnet_sol_oracle::ID); + } else { + return Err(MangoError::MissingFeedForCLMMOracle.into()); + } + } +} + pub fn load_whirlpool_state(acc_info: &impl KeyedAccountReader) -> Result { let data = &acc_info.data(); require!( diff --git a/programs/mango-v4/tests/cases/test_health_compute.rs b/programs/mango-v4/tests/cases/test_health_compute.rs index 68e6c9d38..091205042 100644 --- a/programs/mango-v4/tests/cases/test_health_compute.rs +++ b/programs/mango-v4/tests/cases/test_health_compute.rs @@ -335,7 +335,7 @@ async fn test_health_compute_tokens_fallback_oracles() -> Result<(), TransportEr println!("average success increase: {avg_success_increase}"); println!("average failure increase: {avg_failure_increase}"); assert!(avg_success_increase < 2_050); - assert!(avg_success_increase < 18_500); + assert!(avg_failure_increase < 19_500); Ok(()) }