use anchor_lang::AccountDeserialize; use fixed::types::I80F48; use itertools::Itertools; use log::*; use mango_feeds_connector::metrics::MetricU64; use mango_feeds_connector::{ chain_data::{AccountData, ChainData, ChainDataMetrics, SlotData}, metrics::{MetricType, Metrics}, AccountWrite, SlotUpdate, }; use mango_feeds_lib::{ base_lots_to_ui, base_lots_to_ui_perp, price_lots_to_ui, price_lots_to_ui_perp, MarketConfig, OrderbookSide, }; use mango_v4::accounts_zerocopy::{AccountReader, KeyedAccountReader}; use mango_v4::state::oracle_state_unchecked; use mango_v4::state::OracleConfigParams; use mango_v4::{ serum3_cpi::OrderBookStateHeader, state::{BookSide, OrderTreeType}, }; use serum_dex::critbit::Slab; use service_mango_orderbook::{ BookCheckpoint, BookUpdate, LevelCheckpoint, LevelUpdate, Order, OrderbookFilterMessage, OrderbookLevel, }; use solana_sdk::account::AccountSharedData; use solana_sdk::{ account::{ReadableAccount, WritableAccount}, clock::Epoch, pubkey::Pubkey, }; use std::borrow::BorrowMut; use std::{ collections::{HashMap, HashSet}, mem::size_of, sync::{ atomic::{AtomicBool, Ordering}, Arc, }, time::{SystemTime, UNIX_EPOCH}, }; struct KeyedSharedDataAccountReader { pub key: Pubkey, pub shared: AccountSharedData, } impl AccountReader for KeyedSharedDataAccountReader { fn owner(&self) -> &Pubkey { ReadableAccount::owner(&self.shared) } fn data(&self) -> &[u8] { ReadableAccount::data(&self.shared) } } impl KeyedAccountReader for KeyedSharedDataAccountReader { fn key(&self) -> &Pubkey { &self.key } } #[allow(clippy::too_many_arguments)] #[allow(clippy::ptr_arg)] fn publish_changes( slot: u64, write_version: u64, mkt: &(Pubkey, MarketConfig), side: OrderbookSide, current_orders: &Vec, previous_orders: &Vec, maybe_other_orders: Option<&Vec>, orderbook_update_sender: &async_channel::Sender, metric_book_updates: &mut MetricU64, metric_level_updates: &mut MetricU64, ) { let mut level_update: Vec = vec![]; let mut book_additions: Vec = vec![]; let mut book_removals: Vec = vec![]; let current_bookside: Vec = current_orders .iter() .group_by(|order| order.price) .into_iter() .map(|(price, group)| [price, group.map(|o| o.quantity).sum()]) .collect(); let previous_bookside: Vec = previous_orders .iter() .group_by(|order| order.price) .into_iter() .map(|(price, group)| [price, group.map(|o| o.quantity).sum()]) .collect(); // push diff for levels that are no longer present if current_bookside.len() != previous_bookside.len() { debug!( "L {}", current_bookside.len() as i64 - previous_bookside.len() as i64 ) } for prev_order in previous_orders.iter() { let peer = current_orders.iter().find(|order| prev_order == *order); match peer { None => { debug!("R {:?}", prev_order); book_removals.push(prev_order.clone()); } _ => continue, } } for previous_level in previous_bookside.iter() { let peer = current_bookside .iter() .find(|level| previous_level[0] == level[0]); match peer { None => { debug!("R {} {}", previous_level[0], previous_level[1]); level_update.push([previous_level[0], 0f64]); } _ => continue, } } // push diff where there's a new level or size has changed for current_level in ¤t_bookside { let peer = previous_bookside .iter() .find(|item| item[0] == current_level[0]); match peer { Some(previous_level) => { if previous_level[1] == current_level[1] { continue; } debug!( "C {} {} -> {}", current_level[0], previous_level[1], current_level[1] ); level_update.push(*current_level); } None => { debug!("A {} {}", current_level[0], current_level[1]); level_update.push(*current_level) } } } for current_order in current_orders { let peer = previous_orders.iter().find(|order| current_order == *order); match peer { Some(_) => { continue; } None => { debug!("A {:?}", current_order); book_additions.push(current_order.clone()) } } } match maybe_other_orders { Some(other_orders) => { let (bids, asks) = match side { OrderbookSide::Bid => (current_orders, other_orders), OrderbookSide::Ask => (other_orders, current_orders), }; orderbook_update_sender .try_send(OrderbookFilterMessage::BookCheckpoint(BookCheckpoint { slot, write_version, bids: bids.clone(), asks: asks.clone(), market: mkt.0.to_string(), })) .unwrap(); let bid_levels = bids .iter() .group_by(|order| order.price) .into_iter() .map(|(price, group)| [price, group.map(|o| o.quantity).sum()]) .collect(); let ask_levels = asks .iter() .group_by(|order| order.price) .into_iter() .map(|(price, group)| [price, group.map(|o| o.quantity).sum()]) .collect(); orderbook_update_sender .try_send(OrderbookFilterMessage::LevelCheckpoint(LevelCheckpoint { slot, write_version, bids: bid_levels, asks: ask_levels, market: mkt.0.to_string(), })) .unwrap() } None => info!("other bookside not in cache"), } if !level_update.is_empty() { orderbook_update_sender .try_send(OrderbookFilterMessage::LevelUpdate(LevelUpdate { market: mkt.0.to_string(), side: side.clone(), update: level_update, slot, write_version, })) .unwrap(); // TODO: use anyhow to bubble up error metric_level_updates.increment(); } if !book_additions.is_empty() && !book_removals.is_empty() { orderbook_update_sender .try_send(OrderbookFilterMessage::BookUpdate(BookUpdate { market: mkt.0.to_string(), side, additions: book_additions, removals: book_removals, slot, write_version, })) .unwrap(); metric_book_updates.increment(); } } pub async fn init( market_configs: Vec<(Pubkey, MarketConfig)>, serum_market_configs: Vec<(Pubkey, MarketConfig)>, metrics_sender: Metrics, exit: Arc, ) -> anyhow::Result<( async_channel::Sender, async_channel::Sender, async_channel::Receiver, )> { let mut metric_book_events_new = metrics_sender.register_u64("orderbook_book_updates".into(), MetricType::Counter); let mut metric_level_events_new = metrics_sender.register_u64("orderbook_level_updates".into(), MetricType::Counter); // The actual message may want to also contain a retry count, if it self-reinserts on failure? let (account_write_queue_sender, account_write_queue_receiver) = async_channel::unbounded::(); // Slot updates flowing from the outside into the single processing thread. From // there they'll flow into the postgres sending thread. let (slot_queue_sender, slot_queue_receiver) = async_channel::unbounded::(); // Book updates can be consumed by client connections, they contain L2 and L3 updates for all markets let (book_update_sender, book_update_receiver) = async_channel::unbounded::(); let mut chain_cache = ChainData::new(); let mut chain_data_metrics = ChainDataMetrics::new(&metrics_sender); let mut bookside_cache: HashMap> = HashMap::new(); let mut serum_bookside_cache: HashMap> = HashMap::new(); let mut last_write_versions = HashMap::::new(); let mut relevant_pubkeys = [market_configs.clone(), serum_market_configs.clone()] .concat() .iter() .flat_map(|m| [m.1.bids, m.1.asks]) .collect::>(); relevant_pubkeys.extend(market_configs.iter().map(|(_, cfg)| cfg.oracle)); info!("relevant_pubkeys {:?}", relevant_pubkeys); // update handling thread, reads both slots and account updates tokio::spawn(async move { loop { if exit.load(Ordering::Relaxed) { warn!("shutting down orderbook_filter..."); break; } tokio::select! { Ok(account_write) = account_write_queue_receiver.recv() => { if !relevant_pubkeys.contains(&account_write.pubkey) { continue; } chain_cache.update_account( account_write.pubkey, AccountData { slot: account_write.slot, write_version: account_write.write_version, account: WritableAccount::create( account_write.lamports, account_write.data.clone(), account_write.owner, account_write.executable, account_write.rent_epoch as Epoch, ), }, ); } Ok(slot_update) = slot_queue_receiver.recv() => { chain_cache.update_slot(SlotData { slot: slot_update.slot, parent: slot_update.parent, status: slot_update.status, chain: 0, }); } } chain_data_metrics.report(&chain_cache); for mkt in market_configs.iter() { for side in 0..2 { let mkt_pk = mkt.0; let side_pk = if side == 0 { mkt.1.bids } else { mkt.1.asks }; let other_side_pk = if side == 0 { mkt.1.asks } else { mkt.1.bids }; let oracle_pk = mkt.1.oracle; let last_side_write_version = last_write_versions .get(&side_pk.to_string()) .unwrap_or(&(0, 0)); let last_oracle_write_version = last_write_versions .get(&oracle_pk.to_string()) .unwrap_or(&(0, 0)); match ( chain_cache.account(&side_pk), chain_cache.account(&oracle_pk), ) { (Ok(side_info), Ok(oracle_info)) => { let side_pk_string = side_pk.to_string(); let oracle_pk_string = oracle_pk.to_string(); if !side_info .is_newer_than(last_side_write_version.0, last_side_write_version.1) && !oracle_info.is_newer_than( last_oracle_write_version.0, last_oracle_write_version.1, ) { // neither bookside nor oracle was updated continue; } last_write_versions.insert( side_pk_string.clone(), (side_info.slot, side_info.write_version), ); last_write_versions.insert( oracle_pk_string.clone(), (oracle_info.slot, oracle_info.write_version), ); let keyed_account = KeyedSharedDataAccountReader { key: oracle_pk, shared: oracle_info.account.clone(), }; let oracle_config = OracleConfigParams { conf_filter: 100_000.0, // use a large value to never fail the confidence check max_staleness_slots: None, // don't check oracle staleness to get an orderbook }; if let Ok(unchecked_oracle_state) = oracle_state_unchecked(&keyed_account, mkt.1.base_decimals) { if unchecked_oracle_state .check_confidence_and_maybe_staleness( &oracle_pk, &oracle_config.to_oracle_config(), None, // force this to always return a price no matter how stale ) .is_ok() { let oracle_price = unchecked_oracle_state.price; let account = &side_info.account; let bookside: BookSide = BookSide::try_deserialize( solana_sdk::account::ReadableAccount::data(account) .borrow_mut(), ) .unwrap(); let side = match bookside.nodes.order_tree_type() { OrderTreeType::Bids => OrderbookSide::Bid, OrderTreeType::Asks => OrderbookSide::Ask, }; let time_now = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap() .as_secs(); let oracle_price_lots = (oracle_price * I80F48::from_num(mkt.1.base_lot_size) / I80F48::from_num(mkt.1.quote_lot_size)) .to_num(); let bookside: Vec = bookside .iter_valid(time_now, oracle_price_lots) .map(|item| Order { price: price_lots_to_ui_perp( item.price_lots, mkt.1.base_decimals, mkt.1.quote_decimals, mkt.1.base_lot_size, mkt.1.quote_lot_size, ), quantity: base_lots_to_ui_perp( item.node.quantity, mkt.1.base_decimals, mkt.1.base_lot_size, ), owner_pubkey: item.node.owner.to_string(), }) .collect(); let other_bookside = bookside_cache.get(&other_side_pk.to_string()); match bookside_cache.get(&side_pk_string) { Some(old_bookside) => publish_changes( side_info.slot, side_info.write_version, mkt, side, &bookside, old_bookside, other_bookside, &book_update_sender, &mut metric_book_events_new, &mut metric_level_events_new, ), _ => info!( "bookside_cache could not find {}", side_pk_string ), } bookside_cache.insert(side_pk_string.clone(), bookside.clone()); } } } (side, oracle) => debug!( "chain_cache could not find for mkt={} side={} oracle={}", mkt_pk, side.is_err(), oracle.is_err() ), } } } for mkt in serum_market_configs.iter() { for side in 0..2 { let side_pk = if side == 0 { mkt.1.bids } else { mkt.1.asks }; let other_side_pk = if side == 0 { mkt.1.asks } else { mkt.1.bids }; let last_write_version = last_write_versions .get(&side_pk.to_string()) .unwrap_or(&(0, 0)); match chain_cache.account(&side_pk) { Ok(account_info) => { let side_pk_string = side_pk.to_string(); let write_version = (account_info.slot, account_info.write_version); // todo: should this be <= so we don't overwrite with old data received late? if write_version <= *last_write_version { continue; } last_write_versions.insert(side_pk_string.clone(), write_version); debug!("W {}", mkt.1.name); let account = &mut account_info.account.clone(); let data = account.data_as_mut_slice(); let len = data.len(); let inner = &mut data[5..len - 7]; let slab = Slab::new(&mut inner[size_of::()..]); let bookside: Vec = slab .iter(side == 0) .map(|item| { let owner_bytes: [u8; 32] = bytemuck::cast(item.owner()); Order { price: price_lots_to_ui( u64::from(item.price()) as i64, mkt.1.base_decimals, mkt.1.quote_decimals, mkt.1.base_lot_size, mkt.1.quote_lot_size, ), quantity: base_lots_to_ui( item.quantity() as i64, mkt.1.base_decimals, mkt.1.quote_decimals, mkt.1.base_lot_size, mkt.1.quote_lot_size, ), owner_pubkey: Pubkey::new_from_array(owner_bytes) .to_string(), } }) .collect(); let other_bookside = serum_bookside_cache.get(&other_side_pk.to_string()); match serum_bookside_cache.get(&side_pk_string) { Some(old_bookside) => publish_changes( account_info.slot, account_info.write_version, mkt, if side == 0 { OrderbookSide::Bid } else { OrderbookSide::Ask }, &bookside, old_bookside, other_bookside, &book_update_sender, &mut metric_book_events_new, &mut metric_level_events_new, ), _ => info!("bookside_cache could not find {}", side_pk_string), } serum_bookside_cache.insert(side_pk_string.clone(), bookside); } Err(_) => debug!("chain_cache could not find {}", side_pk), } } } } }); Ok(( account_write_queue_sender, slot_queue_sender, book_update_receiver, )) }