331 lines
12 KiB
Rust
331 lines
12 KiB
Rust
|
use std::collections::HashMap;
|
||
|
use std::sync::Arc;
|
||
|
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||
|
|
||
|
use client::{
|
||
|
chain_data, health_cache, prettify_solana_client_error, MangoClient, TransactionBuilder,
|
||
|
};
|
||
|
use mango_v4::accounts_zerocopy::KeyedAccountSharedData;
|
||
|
use mango_v4::health::HealthType;
|
||
|
use mango_v4::state::{PerpMarket, PerpMarketIndex};
|
||
|
use solana_sdk::address_lookup_table_account::AddressLookupTableAccount;
|
||
|
use solana_sdk::commitment_config::CommitmentConfig;
|
||
|
use solana_sdk::instruction::Instruction;
|
||
|
use solana_sdk::signature::Signature;
|
||
|
|
||
|
use solana_sdk::signer::Signer;
|
||
|
use solana_sdk::transaction::VersionedTransaction;
|
||
|
use {anyhow::Context, fixed::types::I80F48, solana_sdk::pubkey::Pubkey};
|
||
|
|
||
|
pub struct Config {
|
||
|
/// Amount of time to wait before reusing a positive-pnl account
|
||
|
pub settle_cooldown: Duration,
|
||
|
}
|
||
|
|
||
|
fn perp_markets_and_prices(
|
||
|
mango_client: &MangoClient,
|
||
|
account_fetcher: &chain_data::AccountFetcher,
|
||
|
) -> HashMap<PerpMarketIndex, (PerpMarket, I80F48)> {
|
||
|
mango_client
|
||
|
.context
|
||
|
.perp_markets
|
||
|
.iter()
|
||
|
.map(|(market_index, perp)| {
|
||
|
let perp_market = account_fetcher.fetch::<PerpMarket>(&perp.address)?;
|
||
|
let oracle_acc = account_fetcher.fetch_raw(&perp_market.oracle)?;
|
||
|
let oracle_price = perp_market.oracle_price(
|
||
|
&KeyedAccountSharedData::new(perp_market.oracle, oracle_acc),
|
||
|
None,
|
||
|
)?;
|
||
|
|
||
|
Ok((*market_index, (perp_market, oracle_price)))
|
||
|
})
|
||
|
.filter_map(
|
||
|
|v: anyhow::Result<(PerpMarketIndex, (PerpMarket, I80F48))>| match v {
|
||
|
Ok(v) => Some(v),
|
||
|
Err(err) => {
|
||
|
log::error!("error while retriving perp market and price: {:?}", err);
|
||
|
None
|
||
|
}
|
||
|
},
|
||
|
)
|
||
|
.collect()
|
||
|
}
|
||
|
|
||
|
pub struct SettlementState {
|
||
|
pub mango_client: Arc<MangoClient>,
|
||
|
pub account_fetcher: Arc<chain_data::AccountFetcher>,
|
||
|
pub config: Config,
|
||
|
|
||
|
pub recently_settled: HashMap<Pubkey, Instant>,
|
||
|
}
|
||
|
|
||
|
impl SettlementState {
|
||
|
pub async fn settle(&mut self, mut accounts: Vec<Pubkey>) -> anyhow::Result<()> {
|
||
|
{
|
||
|
use rand::seq::SliceRandom;
|
||
|
let mut rng = rand::thread_rng();
|
||
|
accounts.shuffle(&mut rng);
|
||
|
}
|
||
|
|
||
|
self.expire_recently_settled();
|
||
|
|
||
|
self.run_settles(&accounts).await
|
||
|
}
|
||
|
|
||
|
fn expire_recently_settled(&mut self) {
|
||
|
let now = Instant::now();
|
||
|
self.recently_settled.retain(|_, last_settle| {
|
||
|
now.duration_since(*last_settle) < self.config.settle_cooldown
|
||
|
});
|
||
|
}
|
||
|
|
||
|
async fn run_settles(&mut self, accounts: &Vec<Pubkey>) -> anyhow::Result<()> {
|
||
|
let now_ts: u64 = SystemTime::now()
|
||
|
.duration_since(UNIX_EPOCH)?
|
||
|
.as_secs()
|
||
|
.try_into()?;
|
||
|
|
||
|
let mango_client = &*self.mango_client;
|
||
|
let account_fetcher = &*self.account_fetcher;
|
||
|
let perp_market_info = perp_markets_and_prices(mango_client, account_fetcher);
|
||
|
|
||
|
// Get settleable pnl for all accounts and markets
|
||
|
let mut all_positive_settleable =
|
||
|
HashMap::<PerpMarketIndex, Vec<(Pubkey, I80F48, I80F48)>>::new();
|
||
|
let mut all_negative_settleable =
|
||
|
HashMap::<PerpMarketIndex, priority_queue::PriorityQueue<Pubkey, I80F48>>::new();
|
||
|
for account_key in accounts.iter() {
|
||
|
let mut account = account_fetcher.fetch_mango_account(account_key)?;
|
||
|
if account.fixed.group != mango_client.group() {
|
||
|
continue;
|
||
|
}
|
||
|
if self.recently_settled.contains_key(account_key) {
|
||
|
continue;
|
||
|
}
|
||
|
let perp_indexes = account
|
||
|
.active_perp_positions()
|
||
|
.map(|pp| pp.market_index)
|
||
|
.collect::<Vec<_>>();
|
||
|
if perp_indexes.is_empty() {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
let health_cache = health_cache::new(&mango_client.context, account_fetcher, &account)
|
||
|
.await
|
||
|
.context("creating health cache")?;
|
||
|
let liq_end_health = health_cache.health(HealthType::LiquidationEnd);
|
||
|
let perp_settle_health = health_cache.perp_settle_health();
|
||
|
|
||
|
for perp_market_index in perp_indexes {
|
||
|
let (perp_market, price) = match perp_market_info.get(&perp_market_index) {
|
||
|
Some(v) => v,
|
||
|
None => continue, // skip accounts with perp positions where we couldn't get the price and market
|
||
|
};
|
||
|
|
||
|
let perp_position = account.perp_position_mut(perp_market_index).unwrap();
|
||
|
perp_position.settle_funding(perp_market);
|
||
|
perp_position.update_settle_limit(perp_market, now_ts);
|
||
|
|
||
|
let unsettled = perp_position.unsettled_pnl(perp_market, *price)?;
|
||
|
let limited = perp_position.apply_pnl_settle_limit(perp_market, unsettled);
|
||
|
let settleable = if limited >= 0 {
|
||
|
limited
|
||
|
} else {
|
||
|
limited.max(-perp_settle_health).min(I80F48::ZERO)
|
||
|
};
|
||
|
|
||
|
if settleable > 0 {
|
||
|
// compute maint health only when needed
|
||
|
let maint_health = if liq_end_health < 0 {
|
||
|
health_cache.health(HealthType::Maint)
|
||
|
} else {
|
||
|
liq_end_health
|
||
|
};
|
||
|
|
||
|
let fee = perp_market
|
||
|
.compute_settle_fee(settleable, liq_end_health, maint_health)
|
||
|
.unwrap();
|
||
|
if fee <= 0 {
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
all_positive_settleable
|
||
|
.entry(perp_market_index)
|
||
|
.or_default()
|
||
|
.push((*account_key, settleable, fee));
|
||
|
} else if settleable < 0 {
|
||
|
all_negative_settleable
|
||
|
.entry(perp_market_index)
|
||
|
.or_default()
|
||
|
.push(*account_key, -settleable);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
let address_lookup_tables = mango_client.mango_address_lookup_tables().await?;
|
||
|
|
||
|
for (perp_market_index, mut positive_settleable) in all_positive_settleable {
|
||
|
let (perp_market, _) = perp_market_info.get(&perp_market_index).unwrap();
|
||
|
let negative_settleable = match all_negative_settleable.get_mut(&perp_market_index) {
|
||
|
None => continue,
|
||
|
Some(v) => v,
|
||
|
};
|
||
|
// sort by fee, descending
|
||
|
positive_settleable.sort_by_key(|v| -v.2);
|
||
|
|
||
|
let mut batch_processor = SettleBatchProcessor {
|
||
|
mango_client,
|
||
|
account_fetcher,
|
||
|
perp_market_index,
|
||
|
instructions: Vec::new(),
|
||
|
max_batch_size: 8, // the 1.4M max CU limit if we assume settle ix can be up to around 150k
|
||
|
blockhash: mango_client
|
||
|
.client
|
||
|
.rpc_async()
|
||
|
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed())
|
||
|
.await?
|
||
|
.0,
|
||
|
address_lookup_tables: &address_lookup_tables,
|
||
|
};
|
||
|
|
||
|
for (account_a, mut settleable_a, fee) in positive_settleable {
|
||
|
// Settle account_a as much as we can while still getting a fee for it:
|
||
|
// Could be that all counterparties are small and we can settle multiple times
|
||
|
// until account_a is exhausted.
|
||
|
let mut settled_a_once = false;
|
||
|
while settleable_a > perp_market.settle_fee_amount_threshold
|
||
|
|| (settleable_a > 0 && fee != perp_market.settle_fee_flat)
|
||
|
{
|
||
|
// find the best remaining counterparty
|
||
|
let (&account_b, &settleable_b) = match negative_settleable.peek() {
|
||
|
None => break,
|
||
|
Some(v) => v,
|
||
|
};
|
||
|
|
||
|
let settleable = settleable_a.min(settleable_b);
|
||
|
if settleable <= 0
|
||
|
|| (settleable < perp_market.settle_fee_amount_threshold
|
||
|
&& fee == perp_market.settle_fee_flat)
|
||
|
{
|
||
|
// no more interesting pairs that would produce fees
|
||
|
break;
|
||
|
}
|
||
|
|
||
|
batch_processor
|
||
|
.add_and_maybe_send(account_a, account_b)
|
||
|
.await?;
|
||
|
|
||
|
settled_a_once = true;
|
||
|
settleable_a -= settleable;
|
||
|
negative_settleable.change_priority(&account_b, settleable_b - settleable);
|
||
|
}
|
||
|
if settled_a_once {
|
||
|
let now = Instant::now();
|
||
|
self.recently_settled.insert(account_a, now);
|
||
|
} else {
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// send final batch, if any
|
||
|
batch_processor.send().await?;
|
||
|
}
|
||
|
|
||
|
Ok(())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct SettleBatchProcessor<'a> {
|
||
|
mango_client: &'a MangoClient,
|
||
|
account_fetcher: &'a chain_data::AccountFetcher,
|
||
|
perp_market_index: PerpMarketIndex,
|
||
|
instructions: Vec<Instruction>,
|
||
|
max_batch_size: usize,
|
||
|
blockhash: solana_sdk::hash::Hash,
|
||
|
address_lookup_tables: &'a Vec<AddressLookupTableAccount>,
|
||
|
}
|
||
|
|
||
|
impl<'a> SettleBatchProcessor<'a> {
|
||
|
fn transaction(&self) -> anyhow::Result<VersionedTransaction> {
|
||
|
let client = &self.mango_client.client;
|
||
|
let fee_payer = &*client.fee_payer;
|
||
|
|
||
|
TransactionBuilder {
|
||
|
instructions: self.instructions.clone(),
|
||
|
address_lookup_tables: self.address_lookup_tables.clone(),
|
||
|
payer: fee_payer.pubkey(),
|
||
|
signers: vec![fee_payer],
|
||
|
config: client.transaction_builder_config,
|
||
|
}
|
||
|
.transaction_with_blockhash(self.blockhash)
|
||
|
}
|
||
|
|
||
|
async fn send(&mut self) -> anyhow::Result<Option<Signature>> {
|
||
|
if self.instructions.is_empty() {
|
||
|
return Ok(None);
|
||
|
}
|
||
|
|
||
|
let tx = self.transaction()?;
|
||
|
self.instructions.clear();
|
||
|
|
||
|
let send_result = self
|
||
|
.mango_client
|
||
|
.client
|
||
|
.rpc_async()
|
||
|
.send_transaction_with_config(&tx, self.mango_client.client.rpc_send_transaction_config)
|
||
|
.await
|
||
|
.map_err(|e| prettify_solana_client_error(e));
|
||
|
|
||
|
if let Err(err) = send_result {
|
||
|
log::info!("error while sending settle batch: {}", err);
|
||
|
return Ok(None);
|
||
|
}
|
||
|
|
||
|
let txsig = send_result.unwrap();
|
||
|
log::info!("sent settle tx: {txsig}");
|
||
|
Ok(Some(txsig))
|
||
|
}
|
||
|
|
||
|
async fn add_and_maybe_send(
|
||
|
&mut self,
|
||
|
account_a: Pubkey,
|
||
|
account_b: Pubkey,
|
||
|
) -> 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 ix = self.mango_client.perp_settle_pnl_instruction(
|
||
|
self.perp_market_index,
|
||
|
(&account_a, &a_value),
|
||
|
(&account_b, &b_value),
|
||
|
)?;
|
||
|
self.instructions.push(ix);
|
||
|
|
||
|
// if we exceed the batch limit or tx size limit, send a batch without the new ix
|
||
|
let needs_send = if self.instructions.len() > self.max_batch_size {
|
||
|
true
|
||
|
} else {
|
||
|
let tx = self.transaction()?;
|
||
|
let serialized = bincode::serialize(&tx)?;
|
||
|
let too_big = serialized.len() >= solana_sdk::packet::PACKET_DATA_SIZE;
|
||
|
if too_big && self.instructions.len() == 1 {
|
||
|
anyhow::bail!(
|
||
|
"settle instruction for accounts {} and {} does not fit tx size: {} bytes",
|
||
|
account_a,
|
||
|
account_b,
|
||
|
serialized.len()
|
||
|
);
|
||
|
}
|
||
|
too_big
|
||
|
};
|
||
|
if needs_send {
|
||
|
let ix = self.instructions.pop().unwrap();
|
||
|
let txsig = self.send().await?;
|
||
|
self.instructions.push(ix);
|
||
|
return Ok(txsig);
|
||
|
}
|
||
|
|
||
|
Ok(None)
|
||
|
}
|
||
|
}
|