use std::collections::VecDeque;
use std::str::FromStr;
use std::sync::Arc;
use std::time::Duration;
use phoenix_eternal_types::program_ids;
use solana_commitment_config::CommitmentConfig;
use solana_keypair::Keypair;
use solana_pubsub_client::nonblocking::pubsub_client::PubsubClient;
use solana_signer::Signer;
use tracing::warn;
use super::super::config::{current_user_config, rpc_http_url_from_env, ws_url_from_env};
fn derive_spline_collection(
symbol: &str,
market_pubkey: &str,
api_spline_pubkey: &str,
) -> Result<solana_pubkey::Pubkey, solana_pubkey::ParsePubkeyError> {
let market_pk = solana_pubkey::Pubkey::from_str(market_pubkey)?;
let (derived, _) = program_ids::get_spline_collection_address_default(&market_pk);
if let Ok(api_pk) = solana_pubkey::Pubkey::from_str(api_spline_pubkey) {
if api_pk != derived {
warn!(
api = %api_pk,
derived = %derived,
symbol = %symbol,
"spline pubkey mismatch; using derived address"
);
}
}
Ok(derived)
}
const PUBLIC_SOLANA_RPC_URL: &str = "https://api.mainnet-beta.solana.com";
pub(super) const BLOCKHASH_FETCH_TIMEOUT: Duration = Duration::from_secs(5);
pub(super) fn is_public_mainnet_rpc(url: &str) -> bool {
url.contains("api.mainnet-beta.solana.com")
}
pub struct MarketAddrs {
pub perp_asset_map: solana_pubkey::Pubkey,
pub orderbook: solana_pubkey::Pubkey,
pub spline_collection: solana_pubkey::Pubkey,
pub global_trader_index: Vec<solana_pubkey::Pubkey>,
pub active_trader_buffer: Vec<solana_pubkey::Pubkey>,
}
pub struct TxContext {
pub rpc_client: solana_rpc_client::nonblocking::rpc_client::RpcClient,
pub secondary_send_rpc: Option<Arc<solana_rpc_client::nonblocking::rpc_client::RpcClient>>,
pub http_client: Arc<phoenix_rise::PhoenixHttpClient>,
pub metadata: phoenix_rise::PhoenixMetadata,
pub authority_v2: solana_pubkey::Pubkey,
pub trader_pda_v2: solana_pubkey::Pubkey,
pub market_addrs: MarketAddrs,
pub blockhash_pool: tokio::sync::Mutex<VecDeque<[u8; 32]>>,
pub(super) ws_url: String,
pub(super) sig_pubsub: tokio::sync::Mutex<Option<Arc<PubsubClient>>>,
}
impl TxContext {
pub async fn new(
keypair: &Keypair,
symbol: &str,
http: Arc<phoenix_rise::PhoenixHttpClient>,
) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
use solana_rpc_client::nonblocking::rpc_client::RpcClient;
let rpc_url = rpc_http_url_from_env();
let fanout_enabled = current_user_config().fanout_public_rpc;
let secondary_send_rpc = if !fanout_enabled || is_public_mainnet_rpc(&rpc_url) {
None
} else {
Some(Arc::new(RpcClient::new_with_commitment(
PUBLIC_SOLANA_RPC_URL.to_string(),
CommitmentConfig::processed(),
)))
};
let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::processed());
let exchange = http.get_exchange().await?;
let metadata = phoenix_rise::PhoenixMetadata::new(exchange.into());
let keys = metadata.keys();
let market = metadata
.get_market(symbol)
.ok_or_else(|| format!("{} market not found in exchange metadata", symbol))?;
let market_addrs = MarketAddrs {
perp_asset_map: solana_pubkey::Pubkey::from_str(&keys.perp_asset_map)?,
orderbook: solana_pubkey::Pubkey::from_str(&market.market_pubkey)?,
spline_collection: derive_spline_collection(
&market.symbol,
&market.market_pubkey,
&market.spline_pubkey,
)?,
global_trader_index: keys
.global_trader_index
.iter()
.map(|s| solana_pubkey::Pubkey::from_str(s))
.collect::<Result<Vec<_>, _>>()?,
active_trader_buffer: keys
.active_trader_buffer
.iter()
.map(|s| solana_pubkey::Pubkey::from_str(s))
.collect::<Result<Vec<_>, _>>()?,
};
let authority_v2 = solana_pubkey::Pubkey::from_str(&keypair.pubkey().to_string())?;
let trader_pda_v2 = phoenix_rise::TraderKey::derive_pda(&authority_v2, 0, 0);
Ok(Self {
rpc_client,
secondary_send_rpc,
http_client: http,
metadata,
authority_v2,
trader_pda_v2,
market_addrs,
blockhash_pool: tokio::sync::Mutex::new(VecDeque::with_capacity(30)),
ws_url: ws_url_from_env(),
sig_pubsub: tokio::sync::Mutex::new(None),
})
}
pub async fn push_blockhash(&self) {
if let Ok(Ok((bh, _))) = tokio::time::timeout(
BLOCKHASH_FETCH_TIMEOUT,
self.rpc_client
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()),
)
.await
{
let mut pool = self.blockhash_pool.lock().await;
let bytes = bh.to_bytes();
if pool.back() != Some(&bytes) {
pool.push_back(bytes);
if pool.len() > 30 {
pool.pop_front();
}
}
}
}
pub async fn pop_blockhash(&self) -> Result<solana_hash::Hash, String> {
let mut pool = self.blockhash_pool.lock().await;
if let Some(bytes) = pool.pop_back() {
Ok(solana_hash::Hash::new_from_array(bytes))
} else {
drop(pool);
tokio::time::timeout(
BLOCKHASH_FETCH_TIMEOUT,
self.rpc_client
.get_latest_blockhash_with_commitment(CommitmentConfig::confirmed()),
)
.await
.map_err(|_| "blockhash fetch timed out after 5s; check RPC health".to_string())?
.map(|(hash, _)| hash)
.map_err(|e| format!("{}", e))
}
}
pub fn trader_pda_for_subaccount(&self, subaccount_index: u8) -> solana_pubkey::Pubkey {
phoenix_rise::TraderKey::derive_pda(&self.authority_v2, 0, subaccount_index)
}
pub fn market_isolated_only(&self, symbol: &str) -> bool {
self.metadata
.get_market(symbol)
.map(|market| market.isolated_only)
.unwrap_or(false)
}
pub fn max_leverage_for_symbol(&self, symbol: &str) -> Option<f64> {
self.metadata
.get_market(symbol)?
.leverage_tiers
.first()
.map(|tier| tier.max_leverage)
}
pub fn market_addrs_for_symbol(&self, symbol: &str) -> Option<MarketAddrs> {
let keys = self.metadata.keys();
let market = self.metadata.get_market(symbol)?;
Some(MarketAddrs {
perp_asset_map: solana_pubkey::Pubkey::from_str(&keys.perp_asset_map).ok()?,
orderbook: solana_pubkey::Pubkey::from_str(&market.market_pubkey).ok()?,
spline_collection: derive_spline_collection(
&market.symbol,
&market.market_pubkey,
&market.spline_pubkey,
)
.ok()?,
global_trader_index: keys
.global_trader_index
.iter()
.map(|s| solana_pubkey::Pubkey::from_str(s))
.collect::<Result<Vec<_>, _>>()
.ok()?,
active_trader_buffer: keys
.active_trader_buffer
.iter()
.map(|s| solana_pubkey::Pubkey::from_str(s))
.collect::<Result<Vec<_>, _>>()
.ok()?,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_spline_collection_overrides_stale_api_value() {
let market_pubkey = "11111111111111111111111111111112";
let market_pk = solana_pubkey::Pubkey::from_str(market_pubkey).unwrap();
let (canonical, _) = program_ids::get_spline_collection_address_default(&market_pk);
let stale_api = solana_pubkey::Pubkey::new_unique().to_string();
assert_ne!(stale_api, canonical.to_string());
let resolved = derive_spline_collection("TEST-PERP", market_pubkey, &stale_api).unwrap();
assert_eq!(
resolved, canonical,
"write path must use the derived spline PDA, not the API-reported pubkey"
);
}
#[test]
fn derive_spline_collection_returns_canonical_when_api_matches() {
let market_pubkey = "11111111111111111111111111111112";
let market_pk = solana_pubkey::Pubkey::from_str(market_pubkey).unwrap();
let (canonical, _) = program_ids::get_spline_collection_address_default(&market_pk);
let resolved =
derive_spline_collection("TEST-PERP", market_pubkey, &canonical.to_string()).unwrap();
assert_eq!(resolved, canonical);
}
}