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>, pub rpc: RpcClientAsync, } impl AccountFetcher { // loads from ChainData pub fn fetch( &self, address: &Pubkey, ) -> anyhow::Result { Ok(*self .fetch_raw(address)? .load::() .with_context(|| format!("loading account {}", address))?) } pub fn fetch_mango_account(&self, address: &Pubkey) -> anyhow::Result { 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> { 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) } // fetches via RPC, stores in ChainData, returns new version pub async fn fetch_fresh( &self, address: &Pubkey, ) -> anyhow::Result { self.refresh_account_via_rpc(address).await?; self.fetch(address) } pub async fn fetch_fresh_mango_account( &self, address: &Pubkey, ) -> anyhow::Result { self.refresh_account_via_rpc(address).await?; self.fetch_mango_account(address) } pub fn fetch_raw(&self, address: &Pubkey) -> anyhow::Result { 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 { 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 { 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 { self.fetch_raw(address) } async fn fetch_raw_account_lookup_table( &self, address: &Pubkey, ) -> anyhow::Result { // 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> { 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::>()) } 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()) } }