use super::{
pumpswap_preparation::canonical, CacheReadContext, CachedQuoteRequest, CachedRouteStep,
PoolTradeHint, PreparedPumpSwapTrade, SubscriptionAccountCache,
};
use crate::instruction::utils::{pumpswap as ps, pumpswap_types as types};
use anyhow::{anyhow, ensure, Context, Result};
use smallvec::{smallvec, SmallVec};
use solana_sdk::pubkey::Pubkey;
use std::collections::BTreeMap;
#[derive(Clone, Debug)]
pub struct PumpSwapRouteSearch {
pub pool: Pubkey,
pub token_mint: Pubkey,
pub asset_mint: Pubkey,
pub conversion_pools: Vec<PoolTradeHint>,
pub maximum_conversion_hops: usize,
pub bridge_slippage_basis_points: u16,
pub maximum_bridge_price_impact_basis_points: u16,
pub maximum_search_nodes: usize,
}
impl PumpSwapRouteSearch {
pub fn new(
pool: Pubkey,
token_mint: Pubkey,
asset_mint: Pubkey,
conversion_pools: Vec<PoolTradeHint>,
) -> Self {
Self {
pool,
token_mint,
asset_mint,
conversion_pools,
maximum_conversion_hops: 2,
bridge_slippage_basis_points: 100,
maximum_bridge_price_impact_basis_points: 100,
maximum_search_nodes: 512,
}
}
}
#[derive(Clone, Debug)]
pub struct PumpSwapRouteRejection {
pub pools: Vec<Pubkey>,
pub reason: String,
}
#[derive(Clone)]
pub struct PumpSwapRouteSelection {
pub trade: PreparedPumpSwapTrade,
pub routes_evaluated: usize,
pub rejected_routes: Vec<PumpSwapRouteRejection>,
}
pub(crate) fn canonical_conversion_pools(
pools: impl IntoIterator<Item = PoolTradeHint>,
) -> Result<Vec<PoolTradeHint>> {
let mut seen = std::collections::HashMap::new();
let mut unique = Vec::new();
for hint in pools {
let a = canonical(hint.input_mint);
let b = canonical(hint.output_mint);
ensure!(
hint.pool != Pubkey::default()
&& a != b
&& a != Pubkey::default()
&& b != Pubkey::default(),
"Invalid bridge pool identity"
);
let pair = (a.min(b), a.max(b));
if let Some(previous) = seen.insert(hint.pool, pair) {
ensure!(previous == pair, "Conflicting bridge pool identity");
} else {
unique.push(PoolTradeHint { pool: hint.pool, input_mint: a, output_mint: b });
ensure!(unique.len() <= 128, "Too many bridge pools");
}
}
Ok(unique)
}
pub(crate) struct ConversionGraph {
adjacency: BTreeMap<Pubkey, Vec<PoolTradeHint>>,
}
impl ConversionGraph {
pub(crate) fn new(pools: &[PoolTradeHint]) -> Result<Self> {
ensure!(pools.len() <= 128, "PumpSwap search accepts at most 128 conversion pool hints");
let mut candidates = canonical_conversion_pools(pools.iter().copied())?;
candidates.sort_unstable_by_key(|hint| hint.pool);
let mut adjacency: BTreeMap<Pubkey, Vec<PoolTradeHint>> = BTreeMap::new();
for hint in candidates {
adjacency.entry(hint.input_mint).or_default().push(hint);
adjacency.entry(hint.output_mint).or_default().push(hint.reversed());
}
Ok(Self { adjacency })
}
}
struct PathSearch<'a> {
graph: &'a ConversionGraph,
excluded_pool: Pubkey,
target: Pubkey,
excluded_mint: Pubkey,
maximum_hops: usize,
nodes_remaining: usize,
paths: Vec<SmallVec<[PoolTradeHint; 4]>>,
}
impl PathSearch<'_> {
fn walk(
&mut self,
mint: Pubkey,
path: &mut SmallVec<[PoolTradeHint; 4]>,
visited: &mut SmallVec<[Pubkey; 4]>,
) -> Result<()> {
ensure!(self.nodes_remaining > 0,
"PumpSwap route search budget exhausted; reduce candidates/hops or increase maximum_search_nodes");
self.nodes_remaining -= 1;
if mint == self.target {
self.paths.push(path.clone());
return Ok(());
}
if path.len() == self.maximum_hops {
return Ok(());
}
for pool in self.graph.adjacency.get(&mint).into_iter().flatten() {
if pool.pool == self.excluded_pool {
continue;
}
let output = pool.output_mint;
if output == self.excluded_mint || visited.contains(&output) {
continue;
}
visited.push(output);
path.push(PoolTradeHint { input_mint: mint, output_mint: output, ..*pool });
self.walk(output, path, visited)?;
path.pop();
visited.pop();
}
Ok(())
}
}
impl SubscriptionAccountCache {
pub fn prepare_best_pumpswap_trade(
&self,
search: &PumpSwapRouteSearch,
request: CachedQuoteRequest,
ctx: CacheReadContext,
is_buy: bool,
) -> Result<PumpSwapRouteSelection> {
ensure!(
search.conversion_pools.iter().all(|hint| hint.pool != search.pool),
"Conversion candidates must not reuse the selected PumpSwap pool"
);
let graph = ConversionGraph::new(&search.conversion_pools)?;
self.prepare_best_pumpswap_trade_with_graph(search, request, ctx, is_buy, &graph)
}
pub(crate) fn prepare_best_pumpswap_trade_with_graph(
&self,
search: &PumpSwapRouteSearch,
request: CachedQuoteRequest,
ctx: CacheReadContext,
is_buy: bool,
graph: &ConversionGraph,
) -> Result<PumpSwapRouteSelection> {
ensure!(
search.conversion_pools.len() <= 128,
"PumpSwap search accepts at most 128 conversion pool hints"
);
ensure!(
search.bridge_slippage_basis_points < 10_000
&& search.maximum_bridge_price_impact_basis_points < 10_000
&& search.maximum_conversion_hops <= 3
&& (1..=4096).contains(&search.maximum_search_nodes),
"Invalid PumpSwap route search limits"
);
ensure!(
request.amount_in > 0
&& request.slippage_basis_points < 10_000
&& (1..=32).contains(&request.maximum_arrays),
"Invalid cached quote request"
);
let token = canonical(search.token_mint);
let asset = canonical(search.asset_mint);
ensure!(token != asset, "PumpSwap payment/receipt asset must differ from the traded token");
let data = self.get(search.pool, ps::accounts::AMM_PROGRAM, ctx)?;
ensure!(
data.get(..8) == Some(types::POOL_DISCRIMINATOR.as_slice()),
"Invalid cached PumpSwap pool discriminator"
);
let pool = types::pool_decode(&data[8..])
.ok_or_else(|| anyhow!("Invalid cached PumpSwap pool"))?;
let quote = if token == pool.base_mint {
pool.quote_mint
} else {
ensure!(
token == pool.quote_mint,
"Traded token does not belong to selected PumpSwap pool"
);
pool.base_mint
};
let pump = PoolTradeHint {
pool: search.pool,
input_mint: if is_buy { quote } else { token },
output_mint: if is_buy { token } else { quote },
};
self.pumpswap(pump, ctx).context("Selected PumpSwap pool is not ready")?;
let (from, to) = if is_buy { (asset, quote) } else { (quote, asset) };
let mut paths = PathSearch {
graph,
excluded_pool: search.pool,
target: to,
excluded_mint: token,
maximum_hops: search.maximum_conversion_hops,
nodes_remaining: search.maximum_search_nodes,
paths: Vec::new(),
};
paths.walk(from, &mut SmallVec::new(), &mut smallvec![from])?;
ensure!(
!paths.paths.is_empty(),
"No conversion path from {from} to {to} within the supplied candidates and hop limit"
);
let routes_evaluated = paths.paths.len();
let mut best: Option<PreparedPumpSwapTrade> = None;
let mut rejected_routes = Vec::new();
for mut path in paths.paths {
if is_buy {
path.push(pump);
} else {
path.insert(0, pump);
}
let steps: SmallVec<[_; 4]> =
path.iter().map(|h| CachedRouteStep { pool: *h, input_amount: None }).collect();
let (input, output) = if is_buy {
(search.asset_mint, search.token_mint)
} else {
(search.token_mint, search.asset_mint)
};
match self.prepare_pumpswap_trade_with_bridge_limits(
&steps,
input,
output,
request,
ctx,
is_buy,
Some(super::cached_quote::BridgeQuoteLimits {
slippage_basis_points: search.bridge_slippage_basis_points,
maximum_price_impact_basis_points: search
.maximum_bridge_price_impact_basis_points,
target_pool: search.pool,
}),
) {
Ok(trade) => {
let better = best.as_ref().is_none_or(|previous| {
let output = trade.preview().minimum_output_credit;
let old_output = previous.preview().minimum_output_credit;
output > old_output
|| (output == old_output
&& trade.steps().len() < previous.steps().len())
});
if better {
best = Some(trade);
}
}
Err(error) => rejected_routes.push(PumpSwapRouteRejection {
pools: path.iter().map(|h| h.pool).collect(),
reason: format!("{error:#}"),
}),
}
}
let trade = best.ok_or_else(|| {
anyhow!(
"No executable PumpSwap route among {routes_evaluated} paths: {}",
rejected_routes.first().map(|r| r.reason.as_str()).unwrap_or("no quote")
)
})?;
Ok(PumpSwapRouteSelection { trade, routes_evaluated, rejected_routes })
}
}
#[cfg(test)]
mod path_tests {
use super::*;
#[test]
fn usdc_input_starts_at_usdc_and_never_loops_back() {
let usdc = crate::constants::USDC_TOKEN_ACCOUNT;
let wsol = crate::constants::WSOL_TOKEN_ACCOUNT;
let stock = Pubkey::new_unique();
let pools = vec![
PoolTradeHint { pool: Pubkey::new_unique(), input_mint: wsol, output_mint: usdc },
PoolTradeHint { pool: Pubkey::new_unique(), input_mint: usdc, output_mint: stock },
PoolTradeHint { pool: Pubkey::new_unique(), input_mint: wsol, output_mint: stock },
];
let graph = ConversionGraph::new(&pools).unwrap();
let mut search = PathSearch {
graph: &graph,
excluded_pool: Pubkey::default(),
target: stock,
excluded_mint: Pubkey::new_unique(),
maximum_hops: 3,
nodes_remaining: 512,
paths: Vec::new(),
};
search.walk(usdc, &mut SmallVec::new(), &mut smallvec![usdc]).unwrap();
assert_eq!(search.paths.len(), 2);
assert!(search.paths.iter().any(|p| p.len() == 1 && p[0].pool == pools[1].pool));
assert!(search.paths.iter().any(|p| p.len() == 2 && p[0].output_mint == wsol));
for path in search.paths {
assert_eq!(path[0].input_mint, usdc);
assert!(path.iter().all(|hop| hop.output_mint != usdc));
assert_eq!(path.last().unwrap().output_mint, stock);
}
}
}