use solana_rpc_client::nonblocking::rpc_client::RpcClient;
use std::collections::HashMap;
use tracing::warn;
use phoenix_eternal_types::quantities::{SignedBaseLots, SignedQuoteLots};
use phoenix_eternal_types::{program_ids, ActiveTraderBufferTree};
use super::super::config::SplineConfig;
use super::super::data::GtiCache;
use super::super::format::pubkey_trader_short;
use super::super::trading::{TopPositionEntry, TradingSide};
const ATB_HEADER_SIZE: usize =
std::mem::size_of::<phoenix_eternal_types::ActiveTraderBufferHeader>();
const ATB_NUM_ARENAS_OFFSET: usize = ATB_HEADER_SIZE + 4;
const QUOTE_DECIMALS: i32 = 6;
pub const TOP_N_POSITIONS: usize = 50;
async fn fetch_atb_buffers(client: &RpcClient) -> Result<Vec<Vec<u8>>, String> {
let (header_key, _) = program_ids::get_active_trader_buffer_address_default(0);
let header_account = client
.get_account(&header_key)
.await
.map_err(|e| format!("fetch ATB header: {e}"))?;
if header_account.data.len() < ATB_NUM_ARENAS_OFFSET + 2 {
return Err("ATB header account too small for superblock".to_string());
}
let num_arenas = u16::from_le_bytes([
header_account.data[ATB_NUM_ARENAS_OFFSET],
header_account.data[ATB_NUM_ARENAS_OFFSET + 1],
]);
let mut buffers: Vec<Vec<u8>> = Vec::with_capacity(num_arenas.max(1) as usize);
buffers.push(header_account.data);
for i in 1..num_arenas {
let (arena_key, _) = program_ids::get_active_trader_buffer_address_default(i);
match client.get_account(&arena_key).await {
Ok(acc) => buffers.push(acc.data),
Err(e) => {
warn!(arena = i, error = %e, "ATB arena fetch failed; truncating");
break;
}
}
}
Ok(buffers)
}
struct RawPosition {
trader_id: u32,
asset_id: u32,
base_lots: i64,
virtual_quote_lots: i64,
}
fn decode_positions(buffers: &[Vec<u8>]) -> Vec<RawPosition> {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let tree = ActiveTraderBufferTree::load_from_buffers(buffers.iter().map(|b| b.as_slice()));
let mut out: Vec<RawPosition> = Vec::with_capacity(tree.tree.len());
for (id, state) in tree.tree.iter() {
let base_lots: SignedBaseLots = state.base_lot_position;
if base_lots.as_inner() == 0 {
continue;
}
let virtual_quote_lots: SignedQuoteLots = state.virtual_quote_lot_position;
let Some(trader_id) = id.trader_id().as_u32_checked() else {
continue;
};
out.push(RawPosition {
trader_id,
asset_id: id.asset_id().as_inner(),
base_lots: base_lots.as_inner(),
virtual_quote_lots: virtual_quote_lots.as_inner(),
});
}
out
}))
.unwrap_or_default()
}
fn to_entry(
raw: &RawPosition,
configs_by_asset: &HashMap<u32, &SplineConfig>,
marks: &HashMap<String, f64>,
gti: Option<&GtiCache>,
) -> Option<TopPositionEntry> {
let cfg = configs_by_asset.get(&raw.asset_id).copied()?;
let base_units_per_unit = 10f64.powi(cfg.base_lot_decimals as i32);
if base_units_per_unit <= 0.0 {
return None;
}
let size_signed = raw.base_lots as f64 / base_units_per_unit;
let size = size_signed.abs();
if size < 1e-12 {
return None;
}
let virtual_quote_usd_abs = (raw.virtual_quote_lots as f64).abs() / 10f64.powi(QUOTE_DECIMALS);
let entry_price = if size > 0.0 {
virtual_quote_usd_abs / size
} else {
0.0
};
let side = if raw.base_lots > 0 {
TradingSide::Long
} else {
TradingSide::Short
};
let mark = marks.get(&cfg.symbol).copied().filter(|m| *m > 0.0);
let (notional, unrealized_pnl) = match mark {
Some(m) => {
let pnl = match side {
TradingSide::Long => size * (m - entry_price),
TradingSide::Short => size * (entry_price - m),
};
(size * m, pnl)
}
None => (size * entry_price, 0.0),
};
let (trader, trader_display) = resolve_trader(raw.trader_id, gti);
Some(TopPositionEntry {
symbol: cfg.symbol.clone(),
trader,
trader_display,
side,
size,
entry_price,
notional,
unrealized_pnl,
})
}
fn resolve_trader(trader_id: u32, gti: Option<&GtiCache>) -> (Option<String>, String) {
match gti.and_then(|g| g.resolve(trader_id)) {
Some(authority) => {
let full = authority.to_string();
let display = pubkey_trader_short(&authority);
(Some(full), display)
}
None => (None, format!("#{}", trader_id)),
}
}
pub async fn fetch_top_positions(
rpc_url: &str,
configs: &HashMap<String, SplineConfig>,
marks: &HashMap<String, f64>,
gti: Option<&GtiCache>,
) -> Result<Vec<TopPositionEntry>, String> {
use solana_commitment_config::CommitmentConfig;
let client = RpcClient::new_with_commitment(rpc_url.to_string(), CommitmentConfig::processed());
let buffers = fetch_atb_buffers(&client).await?;
let raw = decode_positions(&buffers);
let configs_by_asset: HashMap<u32, &SplineConfig> =
configs.values().map(|c| (c.asset_id, c)).collect();
Ok(raw
.iter()
.filter_map(|r| to_entry(r, &configs_by_asset, marks, gti))
.collect())
}