mango-v4/lib/client/src/chain_data_fetcher.rs

262 lines
8.6 KiB
Rust

use std::sync::{Arc, RwLock};
use std::thread;
use std::time::{Duration, Instant};
use crate::chain_data::*;
use anchor_lang::Discriminator;
use fixed::types::I80F48;
use mango_v4::accounts_zerocopy::{KeyedAccountSharedData, LoadZeroCopy};
use mango_v4::state::{
pyth_mainnet_sol_oracle, pyth_mainnet_usdc_oracle, Bank, MangoAccount, MangoAccountValue,
OracleAccountInfos,
};
use anyhow::Context;
use solana_client::nonblocking::rpc_client::RpcClient as RpcClientAsync;
use solana_sdk::account::{AccountSharedData, ReadableAccount};
use solana_sdk::clock::Slot;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
/// A complex account fetcher that mostly depends on an external job keeping
/// the chain_data up to date.
///
/// In addition to the usual async fetching interface, it also has synchronous
/// functions to access some kinds of data with less overhead.
///
/// Also, there's functions for fetching up to date data via rpc.
pub struct AccountFetcher {
pub chain_data: Arc<RwLock<ChainData>>,
pub rpc: RpcClientAsync,
}
impl AccountFetcher {
// loads from ChainData
pub fn fetch<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
&self,
address: &Pubkey,
) -> anyhow::Result<T> {
Ok(*self
.fetch_raw(address)?
.load::<T>()
.with_context(|| format!("loading account {}", address))?)
}
pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result<MangoAccountValue> {
let acc = self.fetch_raw(address)?;
let data = acc.data();
if data.len() < 8 {
anyhow::bail!(
"account at {} has only {} bytes of data",
address,
data.len()
);
}
let disc_bytes = &data[0..8];
if disc_bytes != MangoAccount::discriminator() {
anyhow::bail!("not a mango account at {}", address);
}
MangoAccountValue::from_bytes(&data[8..])
.with_context(|| format!("loading mango account {}", address))
}
pub fn fetch_bank_and_price(&self, bank: &Pubkey) -> anyhow::Result<(Bank, I80F48)> {
let bank: Bank = self.fetch(bank)?;
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)
}
// fetches via RPC, stores in ChainData, returns new version
pub async fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
&self,
address: &Pubkey,
) -> anyhow::Result<T> {
self.refresh_account_via_rpc(address).await?;
self.fetch(address)
}
pub async fn fetch_fresh_mango_account(
&self,
address: &Pubkey,
) -> anyhow::Result<MangoAccountValue> {
self.refresh_account_via_rpc(address).await?;
self.fetch_mango_account(address)
}
pub fn fetch_raw(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data
.account(address)
.map(|d| d.account.clone())
.with_context(|| format!("fetch account {} via chain_data", address))?)
}
pub async fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<Slot> {
let response = self
.rpc
.get_account_with_commitment(address, self.rpc.commitment())
.await
.with_context(|| format!("refresh account {} via rpc", address))?;
let slot = response.context.slot;
let account = response
.value
.ok_or(anchor_client::ClientError::AccountNotFound)
.with_context(|| format!("refresh account {} via rpc", address))?;
let mut chain_data = self.chain_data.write().unwrap();
let best_chain_slot = chain_data.best_chain_slot();
// The RPC can get information for slots that haven't been seen yet on chaindata. That means
// that the rpc thinks that slot is valid. Make it so by telling chain data about it.
if best_chain_slot < slot {
chain_data.update_slot(SlotData {
slot,
parent: Some(best_chain_slot),
status: SlotStatus::Processed,
chain: 0,
});
}
chain_data.update_account(
*address,
AccountData {
slot,
account: account.into(),
write_version: 1,
},
);
Ok(slot)
}
/// Return the maximum slot reported for the processing of the signatures
pub async fn transaction_max_slot(&self, signatures: &[Signature]) -> anyhow::Result<Slot> {
let statuses = self.rpc.get_signature_statuses(signatures).await?.value;
Ok(statuses
.iter()
.map(|status_opt| status_opt.as_ref().map(|status| status.slot).unwrap_or(0))
.max()
.unwrap_or(0))
}
/// Return success once all addresses have data >= min_slot
pub async fn refresh_accounts_via_rpc_until_slot(
&self,
addresses: &[Pubkey],
min_slot: Slot,
timeout: Duration,
) -> anyhow::Result<()> {
let start = Instant::now();
for address in addresses {
loop {
if start.elapsed() > timeout {
anyhow::bail!(
"timeout while waiting for data for {} that's newer than slot {}",
address,
min_slot
);
}
let data_slot = self.refresh_account_via_rpc(address).await?;
if data_slot >= min_slot {
break;
}
thread::sleep(Duration::from_millis(500));
}
}
Ok(())
}
}
#[async_trait::async_trait]
impl crate::AccountFetcher for AccountFetcher {
async fn fetch_raw_account(
&self,
address: &Pubkey,
) -> anyhow::Result<solana_sdk::account::AccountSharedData> {
self.fetch_raw(address)
}
async fn fetch_raw_account_lookup_table(
&self,
address: &Pubkey,
) -> anyhow::Result<AccountSharedData> {
// Fetch data via RPC if missing: the chain data updater doesn't know about all the
// lookup talbes we may need.
if let Ok(alt) = self.fetch_raw(address) {
return Ok(alt);
}
self.refresh_account_via_rpc(address).await?;
self.fetch_raw(address)
}
async fn fetch_program_accounts(
&self,
program: &Pubkey,
discriminator: [u8; 8],
) -> anyhow::Result<Vec<(Pubkey, AccountSharedData)>> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data
.iter_accounts()
.filter_map(|(pk, data)| {
if data.account.owner() != program {
return None;
}
let acc_data = data.account.data();
if acc_data.len() < 8 || acc_data[..8] != discriminator {
return None;
}
Some((*pk, data.account.clone()))
})
.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())
}
}