Liq/Client: Various improvements

- Abstract away account fetching, so it can be done via RPC or from a
  websocket stream (or a geyser plugin) that populates a ChainData
  instance.

- Separate out information about tokens, markets into MangoGroupContext.

- Separate all gPA calls into functions in a new file

- The liquidator re-fetches critical accounts via RPC before
  liquidation. Unfortunately the websocket stream seems slower :/

- Don't re-implement health account derivation in the liquidator.
  Instead reuse the existing code from the client.

- More smaller stuff.
This commit is contained in:
Christian Kamm 2022-07-16 14:37:15 +02:00
parent eee7ed097b
commit 348d8cfcd8
13 changed files with 840 additions and 662 deletions

View File

@ -0,0 +1,72 @@
use std::collections::HashMap;
use std::sync::Mutex;
use anchor_client::ClientError;
use anchor_lang::AccountDeserialize;
use solana_client::rpc_client::RpcClient;
use anyhow::Context;
use solana_sdk::account::Account;
use solana_sdk::pubkey::Pubkey;
pub trait AccountFetcher: Sync + Send {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account>;
}
// Can't be in the trait, since then it would no longer be object-safe...
pub fn account_fetcher_fetch_anchor_account<T: AccountDeserialize>(
fetcher: &dyn AccountFetcher,
address: Pubkey,
) -> anyhow::Result<T> {
let account = fetcher.fetch_raw_account(address)?;
let mut data: &[u8] = &account.data;
T::try_deserialize(&mut data).with_context(|| format!("deserializing account {}", address))
}
pub struct RpcAccountFetcher {
pub rpc: RpcClient,
}
impl AccountFetcher for RpcAccountFetcher {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account> {
self.rpc
.get_account_with_commitment(&address, self.rpc.commitment())
.with_context(|| format!("fetch account {}", address))?
.value
.ok_or(ClientError::AccountNotFound)
.with_context(|| format!("fetch account {}", address))
}
}
pub struct CachedAccountFetcher<T: AccountFetcher> {
fetcher: T,
cache: Mutex<HashMap<Pubkey, Account>>,
}
impl<T: AccountFetcher> CachedAccountFetcher<T> {
pub fn new(fetcher: T) -> Self {
Self {
fetcher,
cache: Mutex::new(HashMap::new()),
}
}
pub fn clear_cache(&self) {
let mut cache = self.cache.lock().unwrap();
cache.clear();
}
}
impl<T: AccountFetcher> AccountFetcher for CachedAccountFetcher<T> {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<Account> {
let mut cache = self.cache.lock().unwrap();
if let Some(account) = cache.get(&address) {
return Ok(account.clone());
}
let account = self.fetcher.fetch_raw_account(address)?;
cache.insert(address, account.clone());
Ok(account)
}
}

View File

@ -1,25 +1,26 @@
use std::collections::HashMap;
use std::sync::Arc;
use anchor_client::{Client, Cluster, Program};
use anchor_client::{Client, ClientError, Cluster, Program};
use anchor_lang::__private::bytemuck;
use anchor_lang::prelude::System;
use anchor_lang::{AccountDeserialize, Id};
use anchor_lang::Id;
use anchor_spl::associated_token::get_associated_token_address;
use anchor_spl::token::{Mint, Token};
use anchor_spl::token::Token;
use fixed::types::I80F48;
use itertools::Itertools;
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use mango_v4::state::{
Bank, Group, MangoAccount, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, TokenIndex,
};
use mango_v4::state::{Bank, Group, MangoAccount, Serum3MarketIndex, TokenIndex};
use solana_client::rpc_client::RpcClient;
use crate::account_fetcher::*;
use crate::context::{MangoGroupContext, Serum3MarketContext, TokenContext};
use crate::gpa::fetch_mango_accounts;
use crate::util::MyClone;
use anyhow::Context;
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::signature::{Keypair, Signature};
use solana_sdk::sysvar;
@ -30,20 +31,15 @@ pub struct MangoClient {
pub rpc: RpcClient,
pub cluster: Cluster,
pub commitment: CommitmentConfig,
// todo: possibly this object should have cache-functions, so there can be one getMultipleAccounts
// call to refresh banks etc -- if it's backed by websockets, these could just do nothing
pub account_fetcher: Arc<dyn AccountFetcher>,
pub payer: Keypair,
pub mango_account_cache: (Pubkey, MangoAccount),
pub group: Pubkey,
pub group_cache: Group,
// TODO: future: this may not scale if there's thousands of mints, probably some function
// wrapping getMultipleAccounts is needed (or bettew: we provide this data as a service)
pub banks_cache: HashMap<String, Vec<(Pubkey, Bank)>>,
pub banks_cache_by_token_index: HashMap<TokenIndex, Vec<(Pubkey, Bank)>>,
pub mint_infos_cache: HashMap<Pubkey, (Pubkey, MintInfo, Mint)>,
pub mint_infos_cache_by_token_index: HashMap<TokenIndex, (Pubkey, MintInfo, Mint)>,
pub serum3_markets_cache: HashMap<String, (Pubkey, Serum3Market)>,
pub serum3_external_markets_cache: HashMap<String, (Pubkey, Vec<u8>)>,
pub perp_markets_cache: HashMap<String, (Pubkey, PerpMarket)>,
pub perp_markets_cache_by_perp_market_index: HashMap<PerpMarketIndex, (Pubkey, PerpMarket)>,
pub mango_account_address: Pubkey,
pub context: MangoGroupContext,
}
// TODO: add retry framework for sending tx and rpc calls
@ -60,34 +56,48 @@ impl MangoClient {
.0
}
/// Conveniently creates a RPC based client
pub fn new(
cluster: Cluster,
commitment: CommitmentConfig,
payer: Keypair,
group: Pubkey,
payer: Keypair,
mango_account_name: &str,
) -> anyhow::Result<Self> {
let group_context = MangoGroupContext::new_from_rpc(group, cluster.clone(), commitment)?;
let rpc = RpcClient::new_with_commitment(cluster.url().to_string(), commitment);
let account_fetcher = Arc::new(CachedAccountFetcher::new(RpcAccountFetcher { rpc }));
Self::new_detail(
cluster,
commitment,
payer,
mango_account_name,
group_context,
account_fetcher,
)
}
/// Allows control of AccountFetcher and externally created MangoGroupContext
pub fn new_detail(
cluster: Cluster,
commitment: CommitmentConfig,
payer: Keypair,
mango_account_name: &str,
// future: maybe pass Arc<MangoGroupContext>, so it can be extenally updated?
group_context: MangoGroupContext,
account_fetcher: Arc<dyn AccountFetcher>,
) -> anyhow::Result<Self> {
let program =
Client::new_with_options(cluster.clone(), std::rc::Rc::new(payer.clone()), commitment)
.program(mango_v4::ID);
let rpc = program.rpc();
let group_data = program.account::<Group>(group)?;
let group = group_context.group;
// Mango Account
let mut mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
])?;
let mut mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?;
let mango_account_opt = mango_account_tuples
.iter()
.find(|tuple| tuple.1.name() == mango_account_name);
@ -133,129 +143,24 @@ impl MangoClient {
.send()
.context("Failed to create account...")?;
}
let mango_account_tuples = program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(payer.pubkey().to_string()),
encoding: None,
}),
])?;
let mango_account_tuples = fetch_mango_accounts(&program, group, payer.pubkey())?;
let index = mango_account_tuples
.iter()
.position(|tuple| tuple.1.name() == mango_account_name)
.unwrap();
let mango_account_cache = mango_account_tuples[index];
// banks cache
let mut banks_cache = HashMap::new();
let mut banks_cache_by_token_index = HashMap::new();
let bank_tuples = program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in bank_tuples {
banks_cache
.entry(v.name().to_owned())
.or_insert_with(|| Vec::new())
.push((k, v));
banks_cache_by_token_index
.entry(v.token_index)
.or_insert_with(|| Vec::new())
.push((k, v));
}
// mintinfo cache
let mut mint_infos_cache = HashMap::new();
let mut mint_infos_cache_by_token_index = HashMap::new();
let mint_info_tuples =
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in mint_info_tuples {
let data = program
.rpc()
.get_account_with_commitment(&v.mint, commitment)?
.value
.unwrap()
.data;
let mint = Mint::try_deserialize(&mut &data[..])?;
mint_infos_cache.insert(v.mint, (k, v, mint.clone()));
mint_infos_cache_by_token_index.insert(v.token_index, (k, v, mint));
}
// serum3 markets cache
let mut serum3_markets_cache = HashMap::new();
let mut serum3_external_markets_cache = HashMap::new();
let serum3_market_tuples =
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in serum3_market_tuples {
serum3_markets_cache.insert(v.name().to_owned(), (k, v));
let market_external_bytes = program
.rpc()
.get_account_with_commitment(&v.serum_market_external, commitment)?
.value
.unwrap()
.data;
serum3_external_markets_cache.insert(
v.name().to_owned(),
(v.serum_market_external, market_external_bytes),
);
}
// perp markets cache
let mut perp_markets_cache = HashMap::new();
let mut perp_markets_cache_by_perp_market_index = HashMap::new();
let perp_market_tuples =
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])?;
for (k, v) in perp_market_tuples {
perp_markets_cache.insert(v.name().to_owned(), (k, v));
perp_markets_cache_by_perp_market_index.insert(v.perp_market_index, (k, v));
}
Ok(Self {
rpc,
cluster,
cluster: cluster.clone(),
commitment,
account_fetcher,
payer,
mango_account_cache,
group,
group_cache: group_data,
banks_cache,
banks_cache_by_token_index,
mint_infos_cache,
mint_infos_cache_by_token_index,
serum3_markets_cache,
serum3_external_markets_cache,
perp_markets_cache,
perp_markets_cache_by_perp_market_index,
mango_account_address: mango_account_cache.0,
context: group_context,
})
}
pub fn get_mint_info(&self, token_index: &TokenIndex) -> Pubkey {
self.mint_infos_cache_by_token_index
.get(token_index)
.unwrap()
.0
}
pub fn client(&self) -> Client {
Client::new_with_options(
self.cluster.clone(),
@ -273,98 +178,29 @@ impl MangoClient {
}
pub fn group(&self) -> Pubkey {
self.group
self.context.group
}
pub fn get_account(&self) -> Result<(Pubkey, MangoAccount), anchor_client::ClientError> {
let mango_accounts = self.program().accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(self.group().to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(self.payer().to_string()),
encoding: None,
}),
])?;
Ok(mango_accounts[0])
pub fn mango_account(&self) -> anyhow::Result<MangoAccount> {
account_fetcher_fetch_anchor_account(&*self.account_fetcher, self.mango_account_address)
}
pub fn first_bank(&self, token_index: TokenIndex) -> anyhow::Result<Bank> {
let bank_address = self.context.mint_info(token_index).first_bank();
account_fetcher_fetch_anchor_account(&*self.account_fetcher, bank_address)
}
pub fn derive_health_check_remaining_account_metas(
&self,
affected_bank: Option<(Pubkey, Bank)>,
affected_token: Option<TokenIndex>,
writable_banks: bool,
) -> Result<Vec<AccountMeta>, anchor_client::ClientError> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let account = self.get_account()?;
for position in account.1.tokens.iter_active() {
let mint_info = self
.mint_infos_cache_by_token_index
.get(&position.token_index)
.unwrap()
.1;
// TODO: ALTs are unavailable
// let lookup_table = account_loader
// .load_bytes(&mint_info.address_lookup_table)
// .await
// .unwrap();
// let addresses = mango_v4::address_lookup_table::addresses(&lookup_table);
// banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]);
// oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
banks.push(mint_info.first_bank());
oracles.push(mint_info.oracle);
}
if let Some(affected_bank) = affected_bank {
if !banks.iter().any(|&v| v == affected_bank.0) {
// If there is not yet an active position for the token, we need to pass
// the bank/oracle for health check anyway.
let new_position = account
.1
.tokens
.values
.iter()
.position(|p| !p.is_active())
.unwrap();
banks.insert(new_position, affected_bank.0);
oracles.insert(new_position, affected_bank.1.oracle);
}
}
let serum_oos = account.1.serum3.iter_active().map(|&s| s.open_orders);
let perp_markets = account.1.perps.iter_active_accounts().map(|&pa| {
self.perp_markets_cache_by_perp_market_index
.get(&pa.market_index)
.unwrap()
.0
});
Ok(banks
.iter()
.map(|&pubkey| AccountMeta {
pubkey,
is_writable: writable_banks,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(perp_markets.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.collect())
) -> anyhow::Result<Vec<AccountMeta>> {
let account = self.mango_account()?;
self.context.derive_health_check_remaining_account_metas(
&account,
affected_token,
writable_banks,
)
}
pub fn derive_liquidation_health_check_remaining_account_metas(
@ -372,34 +208,22 @@ impl MangoClient {
liqee: &MangoAccount,
asset_token_index: TokenIndex,
liab_token_index: TokenIndex,
) -> Result<Vec<AccountMeta>, anchor_client::ClientError> {
) -> anyhow::Result<Vec<AccountMeta>> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
let account = self.get_account()?;
let account = self.mango_account()?;
let token_indexes = liqee
.tokens
.iter_active()
.chain(account.1.tokens.iter_active())
.chain(account.tokens.iter_active())
.map(|ta| ta.token_index)
.unique();
for token_index in token_indexes {
let mint_info = self
.mint_infos_cache_by_token_index
.get(&token_index)
.unwrap()
.1;
let mint_info = self.context.mint_info(token_index);
let writable_bank = token_index == asset_token_index || token_index == liab_token_index;
// TODO: ALTs are unavailable
// let lookup_table = account_loader
// .load_bytes(&mint_info.address_lookup_table)
// .await
// .unwrap();
// let addresses = mango_v4::address_lookup_table::addresses(&lookup_table);
// banks.push(addresses[mint_info.address_lookup_table_bank_index as usize]);
// oracles.push(addresses[mint_info.address_lookup_table_oracle_index as usize]);
banks.push((mint_info.first_bank(), writable_bank));
oracles.push(mint_info.oracle);
}
@ -407,18 +231,13 @@ impl MangoClient {
let serum_oos = liqee
.serum3
.iter_active()
.chain(account.1.serum3.iter_active())
.chain(account.serum3.iter_active())
.map(|&s| s.open_orders);
let perp_markets = liqee
.perps
.iter_active_accounts()
.chain(account.1.perps.iter_active_accounts())
.map(|&pa| {
self.perp_markets_cache_by_perp_market_index
.get(&pa.market_index)
.unwrap()
.0
});
.chain(account.perps.iter_active_accounts())
.map(|&pa| self.context.perp_market_address(pa.market_index));
let to_account_meta = |pubkey| AccountMeta {
pubkey,
@ -434,17 +253,17 @@ impl MangoClient {
is_signer: false,
})
.chain(oracles.into_iter().map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.chain(perp_markets.map(to_account_meta))
.chain(serum_oos.map(to_account_meta))
.collect())
}
pub fn token_deposit(&self, token_name: &str, amount: u64) -> anyhow::Result<Signature> {
let bank = self.banks_cache.get(token_name).unwrap().get(0).unwrap();
let mint_info: MintInfo = self.mint_infos_cache.get(&bank.1.mint).unwrap().1;
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
let mint_info = self.context.mint_info(token_index);
let health_check_metas =
self.derive_health_check_remaining_account_metas(Some(*bank), false)?;
self.derive_health_check_remaining_account_metas(Some(token_index), false)?;
self.program()
.request()
@ -454,9 +273,9 @@ impl MangoClient {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenDeposit {
group: self.group(),
account: self.mango_account_cache.0,
bank: bank.0,
vault: bank.1.vault,
account: self.mango_account_address,
bank: mint_info.first_bank(),
vault: mint_info.first_vault(),
token_account: get_associated_token_address(
&self.payer(),
&mint_info.mint,
@ -481,17 +300,10 @@ impl MangoClient {
&self,
token_name: &str,
) -> Result<pyth_sdk_solana::Price, anyhow::Error> {
let bank = self.banks_cache.get(token_name).unwrap().get(0).unwrap().1;
let data = self
.program()
.rpc()
.get_account_with_commitment(&bank.oracle, self.commitment)?
.value
.unwrap()
.data;
Ok(pyth_sdk_solana::load_price(&data).unwrap())
let token_index = *self.context.token_indexes_by_name.get(token_name).unwrap();
let mint_info = self.context.mint_info(token_index);
let oracle_account = self.account_fetcher.fetch_raw_account(mint_info.oracle)?;
Ok(pyth_sdk_solana::load_price(&oracle_account.data).unwrap())
}
//
@ -499,15 +311,20 @@ impl MangoClient {
//
pub fn serum3_create_open_orders(&self, name: &str) -> anyhow::Result<Signature> {
let (account_pubkey, _) = self.mango_account_cache;
let account_pubkey = self.mango_account_address;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(name)
.unwrap();
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
let open_orders = Pubkey::find_program_address(
&[
account_pubkey.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
serum3_info.address.as_ref(),
],
&self.program().id(),
)
@ -522,9 +339,9 @@ impl MangoClient {
group: self.group(),
account: account_pubkey,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
serum_market: serum3_info.address,
serum_program: serum3_info.market.serum_program,
serum_market_external: serum3_info.market.serum_market_external,
open_orders,
owner: self.payer(),
payer: self.payer(),
@ -541,6 +358,25 @@ impl MangoClient {
.map_err(prettify_client_error)
}
fn serum3_data<'a>(&'a self, name: &str) -> Result<Serum3Data<'a>, ClientError> {
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(name)
.unwrap();
let serum3_info = self.context.serum3_markets.get(&market_index).unwrap();
let quote_info = self.context.token(serum3_info.market.quote_token_index);
let base_info = self.context.token(serum3_info.market.base_token_index);
Ok(Serum3Data {
market_index,
market: serum3_info,
quote: quote_info,
base: base_info,
})
}
#[allow(clippy::too_many_arguments)]
pub fn serum3_place_order(
&self,
@ -553,54 +389,22 @@ impl MangoClient {
client_order_id: u64,
limit: u16,
) -> anyhow::Result<Signature> {
let (_, account) = self.get_account()?;
let s3 = self.serum3_data(name)?;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let open_orders = account
.serum3
.find(serum3_market.1.market_index)
.unwrap()
.open_orders;
let (_, quote_info, quote_mint) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.quote_token_index)
.unwrap();
let (_, base_info, base_mint) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.base_token_index)
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self.serum3_external_markets_cache.get(name).unwrap().1)
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let bids = market_external.bids;
let asks = market_external.asks;
let event_q = market_external.event_q;
let req_q = market_external.req_q;
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum3_market.1.serum_market_external,
&serum3_market.1.serum_program,
)
.unwrap();
let account = self.mango_account()?;
let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders;
let health_check_metas = self.derive_health_check_remaining_account_metas(None, false)?;
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1306
let limit_price = {
(price
* ((10u64.pow(quote_mint.decimals as u32) * market_external.coin_lot_size) as f64))
(price * ((10u64.pow(s3.quote.decimals as u32) * s3.market.coin_lot_size) as f64))
as u64
/ (10u64.pow(base_mint.decimals as u32) * market_external.pc_lot_size)
/ (10u64.pow(s3.base.decimals as u32) * s3.market.pc_lot_size)
};
// https://github.com/project-serum/serum-ts/blob/master/packages/serum/src/market.ts#L1333
let max_base_qty = {
(size * 10u64.pow(base_mint.decimals as u32) as f64) as u64
/ market_external.coin_lot_size
};
let max_base_qty =
{ (size * 10u64.pow(s3.base.decimals as u32) as f64) as u64 / s3.market.coin_lot_size };
let max_native_quote_qty_including_fees = {
fn get_fee_tier(msrm_balance: u64, srm_balance: u64) -> u64 {
if msrm_balance >= 1 {
@ -646,8 +450,7 @@ impl MangoClient {
let fee_tier = get_fee_tier(0, 0);
let rates = get_fee_rates(fee_tier);
(market_external.pc_lot_size as f64 * (1f64 + rates.0)) as u64
* (limit_price * max_base_qty)
(s3.market.pc_lot_size as f64 * (1f64 + rates.0)) as u64 * (limit_price * max_base_qty)
};
self.program()
@ -658,22 +461,22 @@ impl MangoClient {
let mut ams = anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3PlaceOrder {
group: self.group(),
account: self.mango_account_cache.0,
account: self.mango_account_address,
open_orders,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
market_bids: from_serum_style_pubkey(&bids),
market_asks: from_serum_style_pubkey(&asks),
market_event_queue: from_serum_style_pubkey(&event_q),
market_request_queue: from_serum_style_pubkey(&req_q),
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
quote_bank: s3.quote.mint_info.first_bank(),
quote_vault: s3.quote.mint_info.first_vault(),
base_bank: s3.base.mint_info.first_bank(),
base_vault: s3.base.mint_info.first_vault(),
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
market_bids: s3.market.bids,
market_asks: s3.market.asks,
market_event_queue: s3.market.event_q,
market_request_queue: s3.market.req_q,
market_base_vault: s3.market.coin_vault,
market_quote_vault: s3.market.pc_vault,
market_vault_signer: s3.market.vault_signer,
owner: self.payer(),
token_program: Token::id(),
},
@ -700,35 +503,10 @@ impl MangoClient {
}
pub fn serum3_settle_funds(&self, name: &str) -> anyhow::Result<Signature> {
let (_, account) = self.get_account()?;
let s3 = self.serum3_data(name)?;
let serum3_market = self.serum3_markets_cache.get(name).unwrap();
let open_orders = account
.serum3
.find(serum3_market.1.market_index)
.unwrap()
.open_orders;
let (_, quote_info, _) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.quote_token_index)
.unwrap();
let (_, base_info, _) = self
.mint_infos_cache_by_token_index
.get(&serum3_market.1.base_token_index)
.unwrap();
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self.serum3_external_markets_cache.get(name).unwrap().1)
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let coin_vault = market_external.coin_vault;
let pc_vault = market_external.pc_vault;
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&serum3_market.1.serum_market_external,
&serum3_market.1.serum_program,
)
.unwrap();
let account = self.mango_account()?;
let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders;
self.program()
.request()
@ -737,18 +515,18 @@ impl MangoClient {
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3SettleFunds {
group: self.group(),
account: self.mango_account_cache.0,
account: self.mango_account_address,
open_orders,
quote_bank: quote_info.first_bank(),
quote_vault: quote_info.first_vault(),
base_bank: base_info.first_bank(),
base_vault: base_info.first_vault(),
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
market_base_vault: from_serum_style_pubkey(&coin_vault),
market_quote_vault: from_serum_style_pubkey(&pc_vault),
market_vault_signer: vault_signer,
quote_bank: s3.quote.mint_info.first_bank(),
quote_vault: s3.quote.mint_info.first_vault(),
base_bank: s3.base.mint_info.first_bank(),
base_vault: s3.base.mint_info.first_vault(),
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
market_base_vault: s3.market.coin_vault,
market_quote_vault: s3.market.pc_vault,
market_vault_signer: s3.market.vault_signer,
owner: self.payer(),
token_program: Token::id(),
},
@ -763,25 +541,15 @@ impl MangoClient {
}
pub fn serum3_cancel_all_orders(&self, market_name: &str) -> Result<Vec<u128>, anyhow::Error> {
let serum3_market = self.serum3_markets_cache.get(market_name).unwrap();
let market_index = *self
.context
.serum3_market_indexes_by_name
.get(market_name)
.unwrap();
let account = self.mango_account()?;
let open_orders = account.serum3.find(market_index).unwrap().open_orders;
let open_orders = Pubkey::find_program_address(
&[
self.mango_account_cache.0.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
],
&self.program().id(),
)
.0;
let open_orders_bytes = self
.program()
.rpc()
.get_account_with_commitment(&open_orders, self.commitment)?
.value
.unwrap()
.data;
let open_orders_bytes = self.account_fetcher.fetch_raw_account(open_orders)?.data;
let open_orders_data: &serum_dex::state::OpenOrders = bytemuck::from_bytes(
&open_orders_bytes[5..5 + std::mem::size_of::<serum_dex::state::OpenOrders>()],
);
@ -808,30 +576,10 @@ impl MangoClient {
side: Serum3Side,
order_id: u128,
) -> anyhow::Result<()> {
let (account_pubkey, _account) = self.get_account()?;
let s3 = self.serum3_data(market_name)?;
let serum3_market = self.serum3_markets_cache.get(market_name).unwrap();
let open_orders = Pubkey::find_program_address(
&[
account_pubkey.as_ref(),
b"Serum3OO".as_ref(),
serum3_market.0.as_ref(),
],
&self.program().id(),
)
.0;
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&(self
.serum3_external_markets_cache
.get(market_name)
.unwrap()
.1)[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let bids = market_external.bids;
let asks = market_external.asks;
let event_q = market_external.event_q;
let account = self.mango_account()?;
let open_orders = account.serum3.find(s3.market_index).unwrap().open_orders;
self.program()
.request()
@ -841,14 +589,14 @@ impl MangoClient {
anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::Serum3CancelOrder {
group: self.group(),
account: account_pubkey,
serum_market: serum3_market.0,
serum_program: serum3_market.1.serum_program,
serum_market_external: serum3_market.1.serum_market_external,
account: self.mango_account_address,
serum_market: s3.market.address,
serum_program: s3.market.market.serum_program,
serum_market_external: s3.market.market.serum_market_external,
open_orders,
market_bids: from_serum_style_pubkey(&bids),
market_asks: from_serum_style_pubkey(&asks),
market_event_queue: from_serum_style_pubkey(&event_q),
market_bids: s3.market.bids,
market_asks: s3.market.asks,
market_event_queue: s3.market.event_q,
owner: self.payer(),
},
None,
@ -896,7 +644,7 @@ impl MangoClient {
&mango_v4::accounts::LiqTokenWithToken {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_cache.0,
liqor: self.mango_account_address,
liqor_owner: self.payer.pubkey(),
},
None,
@ -924,16 +672,11 @@ impl MangoClient {
) -> anyhow::Result<Signature> {
let quote_token_index = 0;
let (_, quote_mint_info, _) = self
.mint_infos_cache_by_token_index
.get(&quote_token_index)
.unwrap();
let (liab_mint_info_key, liab_mint_info, _) = self
.mint_infos_cache_by_token_index
.get(&liab_token_index)
.unwrap();
let quote_info = self.context.token(quote_token_index);
let liab_info = self.context.token(liab_token_index);
let bank_remaining_ams = liab_mint_info
let bank_remaining_ams = liab_info
.mint_info
.banks()
.iter()
.map(|bank_pubkey| AccountMeta {
@ -951,6 +694,11 @@ impl MangoClient {
)
.unwrap();
let group = account_fetcher_fetch_anchor_account::<Group>(
&*self.account_fetcher,
self.context.group,
)?;
self.program()
.request()
.instruction(Instruction {
@ -960,11 +708,11 @@ impl MangoClient {
&mango_v4::accounts::LiqTokenBankruptcy {
group: self.group(),
liqee: *liqee.0,
liqor: self.mango_account_cache.0,
liqor: self.mango_account_address,
liqor_owner: self.payer.pubkey(),
liab_mint_info: *liab_mint_info_key,
quote_vault: quote_mint_info.first_vault(),
insurance_vault: self.group_cache.insurance_vault,
liab_mint_info: liab_info.mint_info_address,
quote_vault: quote_info.mint_info.first_vault(),
insurance_vault: group.insurance_vault,
token_program: Token::id(),
},
None,
@ -985,8 +733,11 @@ impl MangoClient {
}
}
fn from_serum_style_pubkey(d: &[u64; 4]) -> Pubkey {
Pubkey::new(bytemuck::cast_slice(d as &[_]))
struct Serum3Data<'a> {
market_index: Serum3MarketIndex,
market: &'a Serum3MarketContext,
quote: &'a TokenContext,
base: &'a TokenContext,
}
/// Do some manual unpacking on some ClientErrors

263
client/src/context.rs Normal file
View File

@ -0,0 +1,263 @@
use std::collections::HashMap;
use anchor_client::{Client, ClientError, Cluster, Program};
use anchor_lang::__private::bytemuck;
use mango_v4::state::{
MangoAccount, MintInfo, PerpMarket, PerpMarketIndex, Serum3Market, Serum3MarketIndex,
TokenIndex,
};
use crate::gpa::*;
use solana_sdk::account::Account;
use solana_sdk::instruction::AccountMeta;
use solana_sdk::signature::Keypair;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
pub struct TokenContext {
pub name: String,
pub mint_info: MintInfo,
pub mint_info_address: Pubkey,
pub decimals: u8,
}
pub struct Serum3MarketContext {
pub address: Pubkey,
pub market: Serum3Market,
pub bids: Pubkey,
pub asks: Pubkey,
pub event_q: Pubkey,
pub req_q: Pubkey,
pub coin_vault: Pubkey,
pub pc_vault: Pubkey,
pub vault_signer: Pubkey,
pub coin_lot_size: u64,
pub pc_lot_size: u64,
}
pub struct PerpMarketContext {
pub address: Pubkey,
pub market: PerpMarket,
}
pub struct MangoGroupContext {
pub group: Pubkey,
pub tokens: HashMap<TokenIndex, TokenContext>,
pub token_indexes_by_name: HashMap<String, TokenIndex>,
pub serum3_markets: HashMap<Serum3MarketIndex, Serum3MarketContext>,
pub serum3_market_indexes_by_name: HashMap<String, Serum3MarketIndex>,
pub perp_markets: HashMap<PerpMarketIndex, PerpMarketContext>,
pub perp_market_indexes_by_name: HashMap<String, PerpMarketIndex>,
}
impl MangoGroupContext {
pub fn mint_info_address(&self, token_index: TokenIndex) -> Pubkey {
self.token(token_index).mint_info_address
}
pub fn mint_info(&self, token_index: TokenIndex) -> MintInfo {
self.token(token_index).mint_info
}
pub fn token(&self, token_index: TokenIndex) -> &TokenContext {
self.tokens.get(&token_index).unwrap()
}
pub fn perp_market_address(&self, perp_market_index: PerpMarketIndex) -> Pubkey {
self.perp_markets.get(&perp_market_index).unwrap().address
}
pub fn new_from_rpc(
group: Pubkey,
cluster: Cluster,
commitment: CommitmentConfig,
) -> Result<Self, ClientError> {
let program =
Client::new_with_options(cluster, std::rc::Rc::new(Keypair::new()), commitment)
.program(mango_v4::ID);
// tokens
let mint_info_tuples = fetch_mint_infos(&program, group)?;
let mut tokens = mint_info_tuples
.iter()
.map(|(pk, mi)| {
(
mi.token_index,
TokenContext {
name: String::new(),
mint_info: *mi,
mint_info_address: *pk,
decimals: u8::MAX,
},
)
})
.collect::<HashMap<_, _>>();
// reading the banks is only needed for the token names and decimals
// 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(&program, group)?;
for (_, bank) in bank_tuples {
let token = tokens.get_mut(&bank.token_index).unwrap();
token.name = bank.name().into();
token.decimals = bank.mint_decimals;
}
assert!(tokens.values().all(|t| t.decimals != u8::MAX));
// serum3 markets
let serum3_market_tuples = fetch_serum3_markets(&program, group)?;
let serum3_markets = serum3_market_tuples
.iter()
.map(|(pk, s)| {
let market_external_account = fetch_raw_account(&program, s.serum_market_external)?;
let market_external: &serum_dex::state::MarketState = bytemuck::from_bytes(
&market_external_account.data
[5..5 + std::mem::size_of::<serum_dex::state::MarketState>()],
);
let vault_signer = serum_dex::state::gen_vault_signer_key(
market_external.vault_signer_nonce,
&s.serum_market_external,
&s.serum_program,
)
.unwrap();
Ok((
s.market_index,
Serum3MarketContext {
address: *pk,
market: *s,
bids: from_serum_style_pubkey(market_external.bids),
asks: from_serum_style_pubkey(market_external.asks),
event_q: from_serum_style_pubkey(market_external.event_q),
req_q: from_serum_style_pubkey(market_external.req_q),
coin_vault: from_serum_style_pubkey(market_external.coin_vault),
pc_vault: from_serum_style_pubkey(market_external.pc_vault),
vault_signer,
coin_lot_size: market_external.coin_lot_size,
pc_lot_size: market_external.pc_lot_size,
},
))
})
.collect::<Result<HashMap<_, _>, ClientError>>()?;
// perp markets
let perp_market_tuples = fetch_perp_markets(&program, group)?;
let perp_markets = perp_market_tuples
.iter()
.map(|(pk, pm)| {
(
pm.perp_market_index,
PerpMarketContext {
address: *pk,
market: *pm,
},
)
})
.collect::<HashMap<_, _>>();
// Name lookup tables
let token_indexes_by_name = tokens
.iter()
.map(|(i, t)| (t.name.clone(), *i))
.collect::<HashMap<_, _>>();
let serum3_market_indexes_by_name = serum3_markets
.iter()
.map(|(i, s)| (s.market.name().to_string(), *i))
.collect::<HashMap<_, _>>();
let perp_market_indexes_by_name = perp_markets
.iter()
.map(|(i, p)| (p.market.name().to_string(), *i))
.collect::<HashMap<_, _>>();
Ok(MangoGroupContext {
group,
tokens,
token_indexes_by_name,
serum3_markets,
serum3_market_indexes_by_name,
perp_markets,
perp_market_indexes_by_name,
})
}
pub fn derive_health_check_remaining_account_metas(
&self,
account: &MangoAccount,
affected_token: Option<TokenIndex>,
writable_banks: bool,
) -> anyhow::Result<Vec<AccountMeta>> {
// figure out all the banks/oracles that need to be passed for the health check
let mut banks = vec![];
let mut oracles = vec![];
for position in account.tokens.iter_active() {
let mint_info = self.mint_info(position.token_index);
banks.push(mint_info.first_bank());
oracles.push(mint_info.oracle);
}
if let Some(affected_token_index) = affected_token {
if account
.tokens
.iter_active()
.find(|p| p.token_index == affected_token_index)
.is_none()
{
// If there is not yet an active position for the token, we need to pass
// the bank/oracle for health check anyway.
let new_position = account
.tokens
.values
.iter()
.position(|p| !p.is_active())
.unwrap();
let mint_info = self.mint_info(affected_token_index);
banks.insert(new_position, mint_info.first_bank());
oracles.insert(new_position, mint_info.oracle);
}
}
let serum_oos = account.serum3.iter_active().map(|&s| s.open_orders);
let perp_markets = account
.perps
.iter_active_accounts()
.map(|&pa| self.perp_market_address(pa.market_index));
Ok(banks
.iter()
.map(|&pubkey| AccountMeta {
pubkey,
is_writable: writable_banks,
is_signer: false,
})
.chain(oracles.iter().map(|&pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(perp_markets.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.chain(serum_oos.map(|pubkey| AccountMeta {
pubkey,
is_writable: false,
is_signer: false,
}))
.collect())
}
}
fn from_serum_style_pubkey(d: [u64; 4]) -> Pubkey {
Pubkey::new(bytemuck::cast_slice(&d as &[_]))
}
fn fetch_raw_account(program: &Program, address: Pubkey) -> Result<Account, ClientError> {
let rpc = program.rpc();
rpc.get_account_with_commitment(&address, rpc.commitment())?
.value
.ok_or(ClientError::AccountNotFound)
}

66
client/src/gpa.rs Normal file
View File

@ -0,0 +1,66 @@
use anchor_client::{ClientError, Program};
use mango_v4::state::{Bank, MangoAccount, MintInfo, PerpMarket, Serum3Market};
use solana_client::rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType};
use solana_sdk::pubkey::Pubkey;
pub fn fetch_mango_accounts(
program: &Program,
group: Pubkey,
owner: Pubkey,
) -> Result<Vec<(Pubkey, MangoAccount)>, ClientError> {
program.accounts::<MangoAccount>(vec![
RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
}),
RpcFilterType::Memcmp(Memcmp {
offset: 40,
bytes: MemcmpEncodedBytes::Base58(owner.to_string()),
encoding: None,
}),
])
}
pub fn fetch_banks(program: &Program, group: Pubkey) -> Result<Vec<(Pubkey, Bank)>, ClientError> {
program.accounts::<Bank>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
}
pub fn fetch_mint_infos(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, MintInfo)>, ClientError> {
program.accounts::<MintInfo>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
}
pub fn fetch_serum3_markets(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, Serum3Market)>, ClientError> {
program.accounts::<Serum3Market>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
}
pub fn fetch_perp_markets(
program: &Program,
group: Pubkey,
) -> Result<Vec<(Pubkey, PerpMarket)>, ClientError> {
program.accounts::<PerpMarket>(vec![RpcFilterType::Memcmp(Memcmp {
offset: 8,
bytes: MemcmpEncodedBytes::Base58(group.to_string()),
encoding: None,
})])
}

View File

@ -1,5 +1,10 @@
pub use account_fetcher::*;
pub use client::*;
pub use context::*;
pub use util::*;
pub mod client;
mod account_fetcher;
mod client;
mod context;
mod gpa;
mod util;

View File

@ -16,26 +16,24 @@ pub async fn runner(
debugging_handle: impl Future,
) -> Result<(), anyhow::Error> {
let handles1 = mango_client
.banks_cache
.values()
.map(|banks_for_a_token| {
loop_update_index_and_rate(
mango_client.clone(),
banks_for_a_token.get(0).unwrap().1.token_index,
)
})
.context
.tokens
.keys()
.map(|&token_index| loop_update_index_and_rate(mango_client.clone(), token_index))
.collect::<Vec<_>>();
let handles2 = mango_client
.perp_markets_cache
.context
.perp_markets
.values()
.map(|(pk, perp_market)| loop_consume_events(mango_client.clone(), *pk, *perp_market))
.map(|perp| loop_consume_events(mango_client.clone(), perp.address, perp.market))
.collect::<Vec<_>>();
let handles3 = mango_client
.perp_markets_cache
.context
.perp_markets
.values()
.map(|(pk, perp_market)| loop_update_funding(mango_client.clone(), *pk, *perp_market))
.map(|perp| loop_update_funding(mango_client.clone(), perp.address, perp.market))
.collect::<Vec<_>>();
futures::join!(
@ -56,16 +54,10 @@ pub async fn loop_update_index_and_rate(mango_client: Arc<MangoClient>, token_in
let client = mango_client.clone();
let res = tokio::task::spawn_blocking(move || -> anyhow::Result<()> {
let mint_info = client.get_mint_info(&token_index);
let banks_for_a_token = client.banks_cache_by_token_index.get(&token_index).unwrap();
let some_bank = banks_for_a_token.get(0).unwrap().1;
let token_name = some_bank.name();
let oracle = some_bank.oracle;
let bank_pubkeys_for_a_token = banks_for_a_token
.into_iter()
.map(|bank| bank.0)
.collect::<Vec<Pubkey>>();
let token = client.context.token(token_index);
let banks_for_a_token = token.mint_info.banks();
let token_name = &token.name;
let oracle = token.mint_info.oracle;
let sig_result = client
.program()
@ -75,7 +67,7 @@ pub async fn loop_update_index_and_rate(mango_client: Arc<MangoClient>, token_in
program_id: mango_v4::id(),
accounts: anchor_lang::ToAccountMetas::to_account_metas(
&mango_v4::accounts::TokenUpdateIndexAndRate {
mint_info,
mint_info: token.mint_info_address,
oracle,
instructions: solana_program::sysvar::instructions::id(),
},
@ -85,7 +77,7 @@ pub async fn loop_update_index_and_rate(mango_client: Arc<MangoClient>, token_in
&mango_v4::instruction::TokenUpdateIndexAndRate {},
),
};
let mut banks = bank_pubkeys_for_a_token
let mut banks = banks_for_a_token
.iter()
.map(|bank_pubkey| AccountMeta {
pubkey: *bank_pubkey,

View File

@ -101,8 +101,8 @@ fn main() -> Result<(), anyhow::Error> {
let mango_client = Arc::new(MangoClient::new(
cluster,
commitment,
payer,
group,
payer,
&cli.mango_account_name,
)?);

View File

@ -6,10 +6,7 @@ use std::{
use fixed::types::I80F48;
use futures::Future;
use mango_v4::{
instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side},
state::Bank,
};
use mango_v4::instructions::{Serum3OrderType, Serum3SelfTradeBehavior, Serum3Side};
use tokio::time;
@ -24,7 +21,7 @@ pub async fn runner(
ensure_oo(&mango_client)?;
let mut price_arcs = HashMap::new();
for market_name in mango_client.serum3_markets_cache.keys() {
for market_name in mango_client.context.serum3_market_indexes_by_name.keys() {
let price = mango_client
.get_oracle_price(
market_name
@ -43,7 +40,8 @@ pub async fn runner(
}
let handles1 = mango_client
.serum3_markets_cache
.context
.serum3_market_indexes_by_name
.keys()
.map(|market_name| {
loop_blocking_price_update(
@ -55,7 +53,8 @@ pub async fn runner(
.collect::<Vec<_>>();
let handles2 = mango_client
.serum3_markets_cache
.context
.serum3_market_indexes_by_name
.keys()
.map(|market_name| {
loop_blocking_orders(
@ -75,11 +74,11 @@ pub async fn runner(
}
fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let account = mango_client.get_account()?.1;
let account = mango_client.mango_account()?;
for (_, serum3_market) in mango_client.serum3_markets_cache.values() {
if account.serum3.find(serum3_market.market_index).is_none() {
mango_client.serum3_create_open_orders(serum3_market.name())?;
for (market_index, serum3_market) in mango_client.context.serum3_markets.iter() {
if account.serum3.find(*market_index).is_none() {
mango_client.serum3_create_open_orders(serum3_market.market.name())?;
}
}
@ -87,25 +86,18 @@ fn ensure_oo(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
}
fn ensure_deposit(mango_client: &Arc<MangoClient>) -> Result<(), anyhow::Error> {
let mango_account = mango_client.get_account()?.1;
let mango_account = mango_client.mango_account()?;
let banks: Vec<Bank> = mango_client
.banks_cache
.values()
.map(|vec| vec.get(0).unwrap().1)
.collect::<Vec<Bank>>();
for &token_index in mango_client.context.tokens.keys() {
let bank = mango_client.first_bank(token_index)?;
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(bank.mint_decimals as u32));
for bank in banks {
let mint = &mango_client.mint_infos_cache.get(&bank.mint).unwrap().2;
let desired_balance = I80F48::from_num(10_000 * 10u64.pow(mint.decimals as u32));
let token_account_opt = mango_account.tokens.find(bank.token_index);
let token_account_opt = mango_account.tokens.find(token_index);
let deposit_native = match token_account_opt {
Some(token_account) => {
let native = token_account.native(&bank);
let ui = token_account.ui(&bank, mint);
let ui = token_account.ui(&bank);
log::info!("Current balance {} {}", ui, bank.name());
if native < I80F48::ZERO {

View File

@ -244,6 +244,22 @@ impl ChainData {
}
}
pub fn update_from_rpc(&mut self, pubkey: &Pubkey, account: AccountData) {
// Add a stub slot if the rpc has information about the future.
// If it's in the past, either the slot already exists (and maybe we have
// the data already), or it was a skipped slot and adding it now makes no difference.
if account.slot > self.newest_processed_slot {
self.update_slot(SlotData {
slot: account.slot,
parent: Some(self.newest_processed_slot),
status: SlotStatus::Processed,
chain: 0,
});
}
self.update_account(*pubkey, account)
}
fn is_account_write_live(&self, write: &AccountData) -> bool {
self.slots
.get(&write.slot)

View File

@ -0,0 +1,81 @@
use std::sync::{Arc, RwLock};
use crate::chain_data::*;
use client::AccountFetcher;
use mango_v4::accounts_zerocopy::LoadZeroCopy;
use anyhow::Context;
use solana_client::rpc_client::RpcClient;
use solana_sdk::account::AccountSharedData;
use solana_sdk::pubkey::Pubkey;
pub struct ChainDataAccountFetcher {
pub chain_data: Arc<RwLock<ChainData>>,
pub rpc: RpcClient,
}
impl ChainDataAccountFetcher {
// 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))?
.clone())
}
// fetches via RPC, stores in ChainData, returns new version
pub fn fetch_fresh<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
&self,
address: &Pubkey,
) -> anyhow::Result<T> {
self.refresh_account_via_rpc(address)?;
self.fetch(address)
}
pub fn fetch_raw(&self, address: &Pubkey) -> anyhow::Result<AccountSharedData> {
let chain_data = self.chain_data.read().unwrap();
Ok(chain_data
.account(address)
.with_context(|| format!("fetch account {} via chain_data", address))?
.clone())
}
pub fn refresh_account_via_rpc(&self, address: &Pubkey) -> anyhow::Result<()> {
let response = self
.rpc
.get_account_with_commitment(&address, self.rpc.commitment())
.with_context(|| format!("refresh account {} via rpc", address))?;
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();
chain_data.update_from_rpc(
address,
AccountData {
slot: response.context.slot,
account: account.into(),
},
);
log::trace!(
"refreshed data of account {} via rpc, got context slot {}",
address,
response.context.slot
);
Ok(())
}
}
impl AccountFetcher for ChainDataAccountFetcher {
fn fetch_raw_account(&self, address: Pubkey) -> anyhow::Result<solana_sdk::account::Account> {
self.fetch_raw(&address).map(|a| a.clone().into())
}
}

View File

@ -1,132 +1,72 @@
use std::collections::HashMap;
use crate::account_shared_data::KeyedAccountSharedData;
use crate::ChainDataAccountFetcher;
use client::MangoClient;
use mango_v4::accounts_zerocopy::LoadZeroCopy;
use client::{AccountFetcher, MangoClient, MangoGroupContext};
use mango_v4::state::{
new_health_cache, oracle_price, Bank, FixedOrderAccountRetriever, HealthCache, HealthType,
MangoAccount, MintInfo, PerpMarketIndex, TokenIndex,
MangoAccount, TokenIndex,
};
use {
crate::chain_data::ChainData, anyhow::Context, fixed::types::I80F48,
solana_sdk::account::AccountSharedData, solana_sdk::pubkey::Pubkey,
};
pub fn load_mango_account<T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
account: &AccountSharedData,
) -> anyhow::Result<&T> {
account.load::<T>().map_err(|e| e.into())
}
fn load_mango_account_from_chain<'a, T: anchor_lang::ZeroCopy + anchor_lang::Owner>(
chain_data: &'a ChainData,
pubkey: &Pubkey,
) -> anyhow::Result<&'a T> {
load_mango_account::<T>(
chain_data
.account(pubkey)
.context("retrieving account from chain")?,
)
}
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
pub fn new_health_cache_(
chain_data: &ChainData,
mint_infos: &HashMap<TokenIndex, Pubkey>,
perp_markets: &HashMap<PerpMarketIndex, Pubkey>,
context: &MangoGroupContext,
account_fetcher: &ChainDataAccountFetcher,
account: &MangoAccount,
) -> anchor_lang::Result<HealthCache> {
let mut health_accounts = vec![];
let mut banks = vec![];
let mut oracles = vec![];
) -> anyhow::Result<HealthCache> {
let active_token_len = account.tokens.iter_active().count();
let active_perp_len = account.perps.iter_active_accounts().count();
// collect banks and oracles for active token positions
for position in account.tokens.iter_active() {
let mint_info = load_mango_account_from_chain::<MintInfo>(
chain_data,
mint_infos
.get(&position.token_index)
.expect("mint_infos cache missing entry"),
)
.unwrap();
banks.push((
mint_info.first_bank(),
chain_data
.account(&mint_info.first_bank())
.expect("chain data is missing bank"),
));
oracles.push((
mint_info.oracle,
chain_data
.account(&mint_info.oracle)
.expect("chain data is missing oracle"),
));
}
// collect active perp markets
let mut perp_markets = account
.perps
.iter_active_accounts()
.map(|&s| {
(
*perp_markets
.get(&s.market_index)
.expect("perp markets cache is missing entry"),
chain_data
.account(
perp_markets
.get(&s.market_index)
.expect("perp markets cache is missing entry"),
)
.expect("chain data is missing perp market"),
)
let metas = context.derive_health_check_remaining_account_metas(account, None, false)?;
let accounts = metas
.iter()
.map(|meta| {
Ok(KeyedAccountSharedData::new(
meta.pubkey,
account_fetcher.fetch_raw(&meta.pubkey)?,
))
})
.collect::<Vec<(Pubkey, &AccountSharedData)>>();
let active_perp_len = perp_markets.len();
// collect OO for active serum markets
let mut serum_oos = account
.serum3
.iter_active()
.map(|&s| (s.open_orders, chain_data.account(&s.open_orders).unwrap()))
.collect::<Vec<(Pubkey, &AccountSharedData)>>();
let active_token_len = banks.len();
health_accounts.append(&mut banks);
health_accounts.append(&mut oracles);
health_accounts.append(&mut perp_markets);
health_accounts.append(&mut serum_oos);
.collect::<anyhow::Result<Vec<_>>>()?;
let retriever = FixedOrderAccountRetriever {
ais: health_accounts
.into_iter()
.map(|asd| KeyedAccountSharedData::new(asd.0, asd.1.clone()))
.collect::<Vec<_>>(),
ais: accounts,
n_banks: active_token_len,
begin_perp: active_token_len * 2,
begin_serum3: active_token_len * 2 + active_perp_len,
};
new_health_cache(account, &retriever)
new_health_cache(account, &retriever).context("make health cache")
}
#[allow(clippy::too_many_arguments)]
pub fn process_account(
mango_client: &MangoClient,
chain_data: &ChainData,
mint_infos: &HashMap<TokenIndex, Pubkey>,
perp_markets: &HashMap<PerpMarketIndex, Pubkey>,
account_fetcher: &ChainDataAccountFetcher,
pubkey: &Pubkey,
) -> anyhow::Result<()> {
// TODO: configurable
let min_health_ratio = I80F48::from_num(50.0f64);
let quote_token_index = 0;
let account = load_mango_account_from_chain::<MangoAccount>(chain_data, pubkey)?;
let account = account_fetcher.fetch::<MangoAccount>(pubkey)?;
let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account)
.expect("always ok")
.health(HealthType::Maint);
// compute maint health for account
let maint_health = new_health_cache_(chain_data, mint_infos, perp_markets, account)
if maint_health >= 0 && !account.is_bankrupt() {
return Ok(());
}
log::trace!(
"possible candidate: {}, with owner: {}, maint health: {}, bankrupt: {}",
pubkey,
account.owner,
maint_health,
account.is_bankrupt(),
);
// Fetch a fresh account and re-compute
let account = account_fetcher.fetch_fresh::<MangoAccount>(pubkey)?;
let maint_health = new_health_cache_(&mango_client.context, account_fetcher, &account)
.expect("always ok")
.health(HealthType::Maint);
@ -134,28 +74,29 @@ pub fn process_account(
let mut tokens = account
.tokens
.iter_active()
.map(|token| {
let mint_info_pk = mint_infos.get(&token.token_index).expect("always Ok");
let mint_info = load_mango_account_from_chain::<MintInfo>(chain_data, mint_info_pk)?;
let bank = load_mango_account_from_chain::<Bank>(chain_data, &mint_info.first_bank())?;
let oracle = chain_data.account(&mint_info.oracle)?;
.map(|token_position| {
let token = mango_client.context.token(token_position.token_index);
let bank = account_fetcher.fetch::<Bank>(&token.mint_info.first_bank())?;
let oracle = account_fetcher.fetch_raw_account(token.mint_info.oracle)?;
let price = oracle_price(
&KeyedAccountSharedData::new(mint_info.oracle, oracle.clone()),
&KeyedAccountSharedData::new(token.mint_info.oracle, oracle.into()),
bank.oracle_config.conf_filter,
bank.mint_decimals,
)?;
Ok((token.token_index, bank, token.native(bank) * price))
Ok((
token_position.token_index,
bank,
token_position.native(&bank) * price,
))
})
.collect::<anyhow::Result<Vec<(TokenIndex, &Bank, I80F48)>>>()?;
.collect::<anyhow::Result<Vec<(TokenIndex, Bank, I80F48)>>>()?;
tokens.sort_by(|a, b| a.2.cmp(&b.2));
let get_max_liab_transfer = |source, target| -> anyhow::Result<I80F48> {
let mut liqor = load_mango_account_from_chain::<MangoAccount>(
chain_data,
&mango_client.mango_account_cache.0,
)
.context("getting liquidator account")?
.clone();
let mut liqor = account_fetcher
.fetch_fresh::<MangoAccount>(&mango_client.mango_account_address)
.context("getting liquidator account")?
.clone();
// Ensure the tokens are activated, so they appear in the health cache and
// max_swap_source() will work.
@ -163,20 +104,13 @@ pub fn process_account(
liqor.tokens.get_mut_or_create(target)?;
let health_cache =
new_health_cache_(chain_data, mint_infos, perp_markets, &liqor).expect("always ok");
new_health_cache_(&mango_client.context, account_fetcher, &liqor).expect("always ok");
let amount = health_cache
.max_swap_source_for_health_ratio(source, target, min_health_ratio)
.context("getting max_swap_source")?;
Ok(amount)
};
log::trace!(
"checking account {} with owner {}: maint health: {}",
pubkey,
account.owner,
maint_health
);
// try liquidating
if account.is_bankrupt() {
if tokens.is_empty() {
@ -187,7 +121,7 @@ pub fn process_account(
let max_liab_transfer = get_max_liab_transfer(*liab_token_index, quote_token_index)?;
let sig = mango_client
.liq_token_bankruptcy((pubkey, account), *liab_token_index, max_liab_transfer)
.liq_token_bankruptcy((pubkey, &account), *liab_token_index, max_liab_transfer)
.context("sending liq_token_bankruptcy")?;
log::info!(
"Liquidated bankruptcy for {}..., maint_health was {}, tx sig {:?}",
@ -209,12 +143,10 @@ pub fn process_account(
// TODO: log liqor's assets in UI form
// TODO: log liquee's liab_needed, need to refactor program code to be able to be accessed from client side
// TODO: swap inherited liabs to desired asset for liqor
// TODO: hook ChainData into MangoClient
// TODO: liq_token_with_token() re-gets the liqor account via rpc unnecessarily
//
let sig = mango_client
.liq_token_with_token(
(pubkey, account),
(pubkey, &account),
*asset_token_index,
*liab_token_index,
max_liab_transfer,
@ -233,13 +165,11 @@ pub fn process_account(
#[allow(clippy::too_many_arguments)]
pub fn process_accounts<'a>(
mango_client: &MangoClient,
chain_data: &ChainData,
account_fetcher: &ChainDataAccountFetcher,
accounts: impl Iterator<Item = &'a Pubkey>,
mint_infos: &HashMap<TokenIndex, Pubkey>,
perp_markets: &HashMap<PerpMarketIndex, Pubkey>,
) -> anyhow::Result<()> {
for pubkey in accounts {
match process_account(mango_client, chain_data, mint_infos, perp_markets, pubkey) {
match process_account(mango_client, account_fetcher, pubkey) {
Err(err) => log::error!("error liquidating account {}: {:?}", pubkey, err),
_ => {}
};

View File

@ -1,17 +1,16 @@
use std::collections::HashMap;
use std::sync::Arc;
use crate::chain_data::*;
use crate::util::{is_mango_account, is_mango_bank, is_mint_info, is_perp_market};
use std::sync::{Arc, RwLock};
use std::time::Duration;
use anchor_client::Cluster;
use client::MangoClient;
use client::{MangoClient, MangoGroupContext};
use log::*;
use mango_v4::state::{PerpMarketIndex, TokenIndex};
use once_cell::sync::OnceCell;
use serde_derive::Deserialize;
use solana_client::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signer::keypair;
@ -22,12 +21,17 @@ use std::str::FromStr;
pub mod account_shared_data;
pub mod chain_data;
pub mod chain_data_fetcher;
pub mod liquidate;
pub mod metrics;
pub mod snapshot_source;
pub mod util;
pub mod websocket_source;
use crate::chain_data::*;
use crate::chain_data_fetcher::*;
use crate::util::{is_mango_account, is_mango_bank, is_mint_info, is_perp_market};
// jemalloc seems to be better at keeping the memory footprint reasonable over
// longer periods of time
#[global_allocator]
@ -90,31 +94,19 @@ async fn main() -> anyhow::Result<()> {
let mango_group_id = Pubkey::from_str(&config.mango_group_id)?;
//
// mango client setup
//
let mango_client = {
let payer = keypair::read_keypair_file(&config.payer).unwrap();
let rpc_url = config.rpc_http_url.to_owned();
let ws_url = rpc_url.replace("https", "wss");
let rpc_timeout = Duration::from_secs(1);
let cluster = Cluster::Custom(rpc_url, ws_url);
let commitment = CommitmentConfig::processed();
let group_context =
MangoGroupContext::new_from_rpc(mango_group_id, cluster.clone(), commitment)?;
let rpc_url = config.rpc_http_url.to_owned();
let ws_url = rpc_url.replace("https", "wss");
let cluster = Cluster::Custom(rpc_url, ws_url);
let commitment = CommitmentConfig::confirmed();
Arc::new(MangoClient::new(
cluster,
commitment,
payer,
mango_group_id,
&config.mango_account_name,
)?)
};
let mango_pyth_oracles = mango_client
.mint_infos_cache
// TODO: this is all oracles, not just pyth!
let mango_pyth_oracles = group_context
.tokens
.values()
.map(|value| value.1.oracle)
.map(|value| value.mint_info.oracle)
.collect::<Vec<Pubkey>>();
//
@ -142,7 +134,16 @@ async fn main() -> anyhow::Result<()> {
snapshot_source::start(config.clone(), mango_pyth_oracles, snapshot_sender);
// The representation of current on-chain account data
let mut chain_data = ChainData::new(&metrics);
let chain_data = Arc::new(RwLock::new(ChainData::new(&metrics)));
// Reading accounts from chain_data
let account_fetcher = Arc::new(ChainDataAccountFetcher {
chain_data: chain_data.clone(),
rpc: RpcClient::new_with_timeout_and_commitment(
cluster.url().to_string(),
rpc_timeout,
commitment,
),
});
// Addresses of the MangoAccounts belonging to the mango program.
// Needed to check health of them all when the cache updates.
@ -167,6 +168,22 @@ async fn main() -> anyhow::Result<()> {
let mut metric_snapshot_queue_len = metrics.register_u64("snapshot_queue_length".into());
let mut metric_mango_accounts = metrics.register_u64("mango_accouns".into());
//
// mango client setup
//
let mango_client = {
let payer = keypair::read_keypair_file(&config.payer).unwrap();
Arc::new(MangoClient::new_detail(
cluster,
commitment,
payer,
&config.mango_account_name,
group_context,
account_fetcher.clone(),
)?)
};
info!("main loop");
loop {
tokio::select! {
@ -177,7 +194,7 @@ async fn main() -> anyhow::Result<()> {
// build a model of slots and accounts in `chain_data`
// this code should be generic so it can be reused in future projects
chain_data.update_from_websocket(message.clone());
chain_data.write().unwrap().update_from_websocket(message.clone());
// specific program logic using the mirrored data
if let websocket_source::Message::Account(account_write) = message {
@ -197,10 +214,8 @@ async fn main() -> anyhow::Result<()> {
if let Err(err) = liquidate::process_accounts(
&mango_client,
&chain_data,
&account_fetcher,
std::iter::once(&account_write.pubkey),
&mint_infos,
&perp_markets,
) {
warn!("could not process account {}: {:?}", account_write.pubkey, err);
@ -230,10 +245,8 @@ async fn main() -> anyhow::Result<()> {
// so optimizing much seems unnecessary.
if let Err(err) = liquidate::process_accounts(
&mango_client,
&chain_data,
&account_fetcher,
mango_accounts.iter(),
&mint_infos,
&perp_markets,
) {
warn!("could not process accounts: {:?}", err);
}
@ -260,16 +273,14 @@ async fn main() -> anyhow::Result<()> {
}
metric_mango_accounts.set(mango_accounts.len() as u64);
chain_data.update_from_snapshot(message);
chain_data.write().unwrap().update_from_snapshot(message);
one_snapshot_done = true;
// trigger a full health check
if let Err(err) = liquidate::process_accounts(
&mango_client,
&chain_data,
&account_fetcher,
mango_accounts.iter(),
&mint_infos,
&perp_markets,
) {
warn!("could not process accounts: {:?}", err);
}

View File

@ -1,5 +1,4 @@
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use checked_math as cm;
use fixed::types::I80F48;
use static_assertions::const_assert_eq;
@ -63,13 +62,13 @@ impl TokenPosition {
}
}
pub fn ui(&self, bank: &Bank, mint: &Mint) -> I80F48 {
pub fn ui(&self, bank: &Bank) -> I80F48 {
if self.indexed_position.is_positive() {
(self.indexed_position * bank.deposit_index)
/ I80F48::from_num(10u64.pow(mint.decimals as u32))
/ I80F48::from_num(10u64.pow(bank.mint_decimals as u32))
} else {
(self.indexed_position * bank.borrow_index)
/ I80F48::from_num(10u64.pow(mint.decimals as u32))
/ I80F48::from_num(10u64.pow(bank.mint_decimals as u32))
}
}