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 <mail@ckamm.de>
This commit is contained in:
Lou-Kamades 2024-01-23 10:26:31 -06:00 committed by GitHub
parent 40b6b49680
commit db98ba5edf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 610 additions and 163 deletions

View File

@ -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?,
);

View File

@ -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() {

View File

@ -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,

View File

@ -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<Option<PreparedExecution>> {
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() {

View File

@ -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<impl AccountFetcher>,
account: &MangoAccountValue,
) -> anyhow::Result<Vec<(PerpMarketIndex, I80F48)>> {
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()

View File

@ -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<Option<Signature>> {
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());

View File

@ -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

View File

@ -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<AccountSharedData>;
@ -29,6 +33,13 @@ pub trait AccountFetcher: Sync + Send {
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>>;
async fn get_slot(&self) -> anyhow::Result<u64>;
}
// 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::<Vec<_>>())
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
gpa::fetch_multiple_accounts(&self.rpc, keys).await
}
async fn get_slot(&self) -> anyhow::Result<u64> {
Ok(self.rpc.get_slot().await?)
}
}
struct CoalescedAsyncJob<Key, Output> {
@ -138,6 +160,8 @@ struct AccountCache {
keys_for_program_and_discriminator: HashMap<(Pubkey, [u8; 8]), Vec<Pubkey>>,
account_jobs: CoalescedAsyncJob<Pubkey, anyhow::Result<AccountSharedData>>,
multiple_accounts_jobs:
CoalescedAsyncJob<Hash, anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
program_accounts_jobs:
CoalescedAsyncJob<(Pubkey, [u8; 8]), anyhow::Result<Vec<(Pubkey, AccountSharedData)>>>,
}
@ -261,4 +285,62 @@ impl<T: AccountFetcher + 'static> AccountFetcher for CachedAccountFetcher<T> {
)),
}
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let fetch_job = {
let mut cache = self.cache.lock().unwrap();
let mut missing_keys: Vec<Pubkey> = 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::<Vec<_>>());
}
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<u64> {
self.fetcher.get_slot().await
}
}

View File

@ -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<Option<KeyedAccountSharedData>> {
Ok(self
.fetch_raw(&key)
.ok()
.map(|data| KeyedAccountSharedData::new(key, data)))
}
pub fn fetch_bank_price(&self, bank: &Pubkey) -> anyhow::Result<I80F48> {
self.fetch_bank_and_price(bank).map(|(_, p)| p)
}
@ -217,4 +242,20 @@ impl crate::AccountFetcher for AccountFetcher {
})
.collect::<Vec<_>>())
}
async fn fetch_multiple_accounts(
&self,
keys: &[Pubkey],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let chain_data = self.chain_data.read().unwrap();
Ok(keys
.iter()
.map(|pk| (*pk, chain_data.account(pk).unwrap().account.clone()))
.collect::<Vec<_>>())
}
async fn get_slot(&self) -> anyhow::Result<u64> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data.newest_processed_slot())
}
}

View File

@ -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<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
) -> anyhow::Result<(Vec<AccountMeta>, 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<AccountMeta>, 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<HealthCache> {
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<Signature> {
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<Signature> {
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 => &quote,
@ -762,18 +801,20 @@ impl MangoClient {
) -> anyhow::Result<Signature> {
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<PreparedInstructions> {
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<Signature> {
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<Signature> {
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<Signature> {
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<PreparedInstructions> {
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<PreparedInstructions> {
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<PreparedInstructions> {
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::<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,
&[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<PreparedInstructions> {
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<PreparedInstructions> {
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<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
) -> anyhow::Result<PreparedInstructions> {
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<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
) -> anyhow::Result<PreparedInstructions> {
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<Pubkey>),
/// 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

View File

@ -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<Pubkey> {
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::<HashMap<_, _>>();
// 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<Pubkey> = 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<TokenIndex>,
writable_banks: Vec<TokenIndex>,
affected_perp_markets: Vec<PerpMarketIndex>,
fallback_contexts: HashMap<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, 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<Pubkey> = 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<Pubkey, FallbackOracleContext>,
) -> anyhow::Result<(Vec<AccountMeta>, 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<Pubkey> = 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<HashMap<Pubkey, FallbackOracleContext>> {
// 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<Pubkey, &TokenContext> =
self.tokens.iter().map(|t| (t.1.oracle, t.1)).collect();
let oracle_keys: Vec<Pubkey> =
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<Acco
.value
.ok_or(ClientError::AccountNotFound)
}
/// Fetch the quote key for a fallback oracle account info.
/// Returns Pubkey::default if no quote key is found or there are any
/// errors occur when trying to fetch the quote oracle.
/// This function will only return a non-default key when a CLMM oracle is used
fn get_fallback_quote_key(acc_info: &impl KeyedAccountReader) -> 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())
}

View File

@ -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<Vec<(Pubkey, AccountSharedData)>> {
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())
}

View File

@ -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<HealthCache> {
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<Vec<KeyedAccountSharedData>> = 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();

View File

@ -228,6 +228,7 @@ impl<'a> JupiterV4<'a> {
.collect::<Vec<_>>();
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![],

View File

@ -237,6 +237,7 @@ impl<'a> JupiterV6<'a> {
.collect::<Vec<_>>();
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![],

View File

@ -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 {

View File

@ -59,7 +59,7 @@ pub struct FixedOrderAccountRetriever<T: KeyedAccountReader> {
pub begin_serum3: usize,
pub staleness_slot: Option<u64>,
pub begin_fallback_oracles: usize,
pub usd_oracle_index: Option<usize>,
pub usdc_oracle_index: Option<usize>,
pub sol_oracle_index: Option<usize>,
}
@ -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<T: KeyedAccountReader> FixedOrderAccountRetriever<T> {
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]),
}
}

View File

@ -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::<OracleConfig>(), 16 + 8 + 72);
const_assert_eq!(size_of::<OracleConfig>(), 96);
const_assert_eq!(size_of::<OracleConfig>() % 8, 0);
#[derive(AnchorDeserialize, AnchorSerialize, Debug)]
#[derive(AnchorDeserialize, AnchorSerialize, Debug, Default)]
pub struct OracleConfigParams {
pub conf_filter: f32,
pub max_staleness_slots: Option<u32>,
@ -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<T: KeyedAccountReader>(
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<T: KeyedAccountReader>(
) -> Result<OracleState> {
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)

View File

@ -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<Pubkey> {
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<WhirlpoolState> {
let data = &acc_info.data();
require!(

View File

@ -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(())
}