mod allocation;
pub mod config;
use std::{
collections::{HashMap, HashSet},
sync::LazyLock,
time::{Duration, Instant},
};
pub use allocation::ExclusiveAccess;
use allocation::{allocate, Allocation, OrderClass};
use config::WorkerPoolRouterConfig;
use futures::stream::{FuturesUnordered, StreamExt};
use metrics::{counter, histogram};
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tracing::{debug, warn};
use tycho_execution::encoding::{
evm::gas_estimator::estimate_gas_usage,
models::{Solution, Strategy},
};
use tycho_simulation::tycho_common::Bytes;
use crate::{
encoding::encoder::Encoder, feed::exclusivity::is_exclusive, price_guard::guard::PriceGuard,
worker_pool::task_queue::TaskQueueHandle, BlockInfo, EncodingOptions, Order, OrderQuote, Quote,
QuoteOptions, QuoteRequest, QuoteStatus, SolveError, SolveParams, SurplusInfo,
};
const ENV_USER_IMPROVEMENT_SHARE_BPS: &str = "EXCLUSIVE_ROUTE_USER_SHARE_BPS";
const DEFAULT_USER_IMPROVEMENT_SHARE_BPS: u32 = 1_000;
const BPS_DENOMINATOR: u32 = 10_000;
static USER_IMPROVEMENT_SHARE_BPS: LazyLock<u32> = LazyLock::new(user_improvement_share_bps_env);
fn user_improvement_share_bps_env() -> u32 {
let Ok(raw) = std::env::var(ENV_USER_IMPROVEMENT_SHARE_BPS) else {
return DEFAULT_USER_IMPROVEMENT_SHARE_BPS;
};
match parse_user_improvement_share_bps(&raw) {
Some(bps) => bps,
None => {
warn!(
value = %raw,
default_bps = DEFAULT_USER_IMPROVEMENT_SHARE_BPS,
"{ENV_USER_IMPROVEMENT_SHARE_BPS} must be an integer from 0 to \
{BPS_DENOMINATOR} basis points; using the default",
);
DEFAULT_USER_IMPROVEMENT_SHARE_BPS
}
}
}
fn parse_user_improvement_share_bps(raw: &str) -> Option<u32> {
raw.trim()
.parse::<u32>()
.ok()
.filter(|bps| *bps <= BPS_DENOMINATOR)
}
fn user_margin(improvement: &BigUint, user_share_bps: u32) -> BigUint {
let denominator = BigUint::from(BPS_DENOMINATOR);
(improvement * BigUint::from(user_share_bps) + (&denominator - 1u32)) / denominator
}
const NO_PUBLIC_ROUTE_FEE_BPS: u32 = 5;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LiquidityScope {
#[default]
PublicOnly,
IncludeExclusive,
}
#[derive(Clone)]
pub struct SolverPoolHandle {
name: String,
queue: TaskQueueHandle,
liquidity_scope: LiquidityScope,
}
impl SolverPoolHandle {
pub fn new(name: impl Into<String>, queue: TaskQueueHandle) -> Self {
Self { name: name.into(), queue, liquidity_scope: LiquidityScope::default() }
}
pub fn with_liquidity_scope(mut self, scope: LiquidityScope) -> Self {
self.liquidity_scope = scope;
self
}
pub fn name(&self) -> &str {
&self.name
}
pub fn queue(&self) -> &TaskQueueHandle {
&self.queue
}
pub fn liquidity_scope(&self) -> LiquidityScope {
self.liquidity_scope
}
}
#[derive(Debug)]
pub(crate) struct OrderResponses {
order_id: String,
quotes: Vec<(String, OrderQuote)>,
failed_solvers: Vec<(String, SolveError)>,
}
impl OrderResponses {
fn public_only(&self, pool_scopes: &HashMap<String, LiquidityScope>) -> OrderResponses {
let quotes = self
.quotes
.iter()
.filter(|(pool, _)| pool_scopes.get(pool) != Some(&LiquidityScope::IncludeExclusive))
.cloned()
.collect();
OrderResponses {
order_id: self.order_id.clone(),
quotes,
failed_solvers: self.failed_solvers.clone(),
}
}
}
pub struct WorkerPoolRouter {
solver_pools: Vec<SolverPoolHandle>,
config: WorkerPoolRouterConfig,
encoder: Encoder,
price_guard: Option<PriceGuard>,
}
impl WorkerPoolRouter {
pub fn new(
solver_pools: Vec<SolverPoolHandle>,
config: WorkerPoolRouterConfig,
encoder: Encoder,
) -> Self {
Self { solver_pools, config, encoder, price_guard: None }
}
pub fn with_price_guard(mut self, price_guard: PriceGuard) -> Self {
self.price_guard = Some(price_guard);
self
}
pub fn num_pools(&self) -> usize {
self.solver_pools.len()
}
pub async fn quote(
&self,
request: QuoteRequest,
access: ExclusiveAccess,
) -> Result<Quote, SolveError> {
let start = Instant::now();
let deadline = start + self.effective_timeout(request.options());
let min_responses = request
.options()
.min_responses()
.unwrap_or(self.config.min_responses());
if self.solver_pools.is_empty() {
return Err(SolveError::Internal("no solver pools configured".to_string()));
}
let params = match request.options().state_label().cloned() {
Some(label) => SolveParams::default().with_state_label(label),
None => SolveParams::default(),
};
counter!(
"worker_router_exclusive_access_total",
"access" => match access {
ExclusiveAccess::Granted => "granted",
ExclusiveAccess::Denied => "denied",
}
)
.increment(1);
let class = OrderClass::new(access);
let allocations: Vec<Allocation<'_>> = request
.orders()
.iter()
.map(|_| allocate(&self.solver_pools, class))
.collect();
if allocations
.iter()
.any(Allocation::is_empty)
{
return Err(SolveError::Internal(format!(
"no solver pool serves this request: {class:?}"
)));
}
let order_futures: Vec<_> = request
.orders()
.iter()
.zip(&allocations)
.map(|(order, allocation)| {
self.solve_order(order.clone(), params.clone(), deadline, min_responses, allocation)
})
.collect();
let mut order_responses = futures::future::join_all(order_futures).await;
if let Some(encoding_options) = request.options().encoding_options() {
refine_gas_estimates(&mut order_responses, encoding_options)?;
}
let ranked_quotes: Vec<Vec<OrderQuote>> = order_responses
.into_iter()
.zip(&allocations)
.map(|(responses, allocation)| {
if allocation.exclusive_routing_active() {
let public_ranked = self.rank_quotes(
&responses.public_only(allocation.scopes()),
request.options(),
);
combine_with_surplus(
&responses,
allocation.scopes(),
request.options(),
public_ranked,
*USER_IMPROVEMENT_SHARE_BPS,
)
} else {
self.rank_quotes(&responses, request.options())
}
})
.collect();
let price_guard_config = request
.options()
.encoding_options()
.map(|e| e.price_guard())
.filter(|c| c.enabled());
let mut order_quotes: Vec<OrderQuote> = match (&self.price_guard, price_guard_config) {
(Some(guard), Some(config)) => guard
.validate(ranked_quotes, config)
.map_err(|e| {
warn!(error = %e, "price guard validation error");
SolveError::Internal(e.to_string())
})?,
(None, Some(_)) => {
return Err(SolveError::Internal(
"price guard config provided but price guard is not enabled on this server"
.to_string(),
));
}
_ => ranked_quotes
.into_iter()
.filter_map(|candidates| candidates.into_iter().next())
.collect(),
};
if let Some(encoding_options) = request.options().encoding_options() {
let encode_start = Instant::now();
let encoded = self
.encoder
.encode(order_quotes, encoding_options.clone())
.await;
histogram!("encoding_duration_seconds").record(encode_start.elapsed().as_secs_f64());
order_quotes = match encoded {
Ok(quotes) => quotes,
Err(e) => {
counter!("encoding_failures_total").increment(1);
return Err(e);
}
};
}
let total_gas_estimate = order_quotes
.iter()
.map(|o| o.gas_estimate())
.fold(BigUint::ZERO, |acc, g| acc + g);
let solve_time_ms = start.elapsed().as_millis() as u64;
Ok(Quote::new(order_quotes, total_gas_estimate, solve_time_ms))
}
async fn solve_order(
&self,
order: Order,
params: SolveParams,
deadline: Instant,
min_responses: usize,
allocation: &Allocation<'_>,
) -> OrderResponses {
let start_time = Instant::now();
let order_id = order.id().to_string();
let allocated: Vec<(&str, LiquidityScope)> = allocation
.worker_pools()
.iter()
.map(|worker_pool| (worker_pool.name(), worker_pool.liquidity_scope()))
.collect();
debug!(
order_id = %order_id,
worker_pools = ?allocated,
"dispatching order to allocated worker pools"
);
let mut pending: FuturesUnordered<_> = allocation
.worker_pools()
.iter()
.map(|worker_pool| {
let order_clone = order.clone();
let worker_pool_name = worker_pool.name().to_string();
let queue = worker_pool.queue().clone();
let task_params = params.clone();
async move {
let result = queue
.enqueue(order_clone, task_params)
.await;
(worker_pool_name, result)
}
})
.collect();
let mut quotes = Vec::new();
let mut failed_solvers: Vec<(String, SolveError)> = Vec::new();
let mut remaining_worker_pools: HashSet<String> = allocation
.worker_pools()
.iter()
.map(|p| p.name().to_string())
.collect();
let mut has_public_response = false;
let mut has_exclusive_access_response = false;
loop {
let deadline_instant = tokio::time::Instant::from_std(deadline);
tokio::select! {
biased;
_ = tokio::time::sleep_until(deadline_instant) => {
let elapsed_ms = deadline.saturating_duration_since(Instant::now())
.as_millis() as u64;
for worker_pool_name in remaining_worker_pools.drain() {
failed_solvers.push((
worker_pool_name,
SolveError::Timeout { elapsed_ms },
));
}
break;
}
result = pending.next() => {
match result {
Some((worker_pool_name, Ok(single_quote))) => {
remaining_worker_pools.remove(&worker_pool_name);
if allocation.is_exclusive(&worker_pool_name) {
has_exclusive_access_response = true;
} else {
has_public_response = true;
}
quotes.push((worker_pool_name.clone(), single_quote.order().clone()));
let scope_ready = if allocation.exclusive_routing_active() {
has_public_response && has_exclusive_access_response
} else {
true
};
if min_responses > 0
&& quotes.len() >= min_responses
&& scope_ready
{
debug!(
order_id = %order_id,
responses = quotes.len(),
min_responses,
"early return: min_responses reached"
);
counter!("worker_router_early_returns_total").increment(1);
break;
}
}
Some((worker_pool_name, Err(e))) => {
remaining_worker_pools.remove(&worker_pool_name);
if allocation.is_exclusive(&worker_pool_name) {
has_exclusive_access_response = true;
}
debug!(
pool = %worker_pool_name,
order_id = %order_id,
error = %e,
"solver pool failed"
);
failed_solvers.push((worker_pool_name, e));
}
None => {
break;
}
}
}
}
}
let duration = start_time.elapsed().as_secs_f64();
histogram!("worker_router_solve_duration_seconds").record(duration);
histogram!("worker_router_solver_responses").record(quotes.len() as f64);
for (worker_pool_name, error) in &failed_solvers {
let error_type = match error {
SolveError::Timeout { .. } => "timeout",
SolveError::NoRouteFound { .. } => "no_route",
SolveError::QueueFull => "queue_full",
SolveError::Internal(_) => "internal",
SolveError::PriceCheckFailed { .. } => "price_check_failed",
_ => "other",
};
counter!("worker_router_solver_failures_total", "pool" => worker_pool_name.clone(), "error_type" => error_type).increment(1);
}
if !failed_solvers.is_empty() {
let timeout_count = failed_solvers
.iter()
.filter(|(_, e)| matches!(e, SolveError::Timeout { .. }))
.count();
let other_count = failed_solvers.len() - timeout_count;
warn!(
order_id = %order_id,
timeout_count,
other_failures = other_count,
"some solver pools failed"
);
}
OrderResponses { order_id, quotes, failed_solvers }
}
fn rank_quotes(&self, responses: &OrderResponses, options: &QuoteOptions) -> Vec<OrderQuote> {
let mut valid_quotes: Vec<_> = responses
.quotes
.iter()
.filter(|(_, q)| q.status() == QuoteStatus::Success)
.filter(|(_, q)| {
options
.max_gas()
.map(|max| q.gas_estimate() <= max)
.unwrap_or(true)
})
.collect();
valid_quotes.sort_by(|(_, a), (_, b)| {
b.amount_out_net_gas()
.cmp(a.amount_out_net_gas())
});
if !valid_quotes.is_empty() {
counter!("worker_router_orders_total", "status" => "success").increment(1);
let (worker_pool_name, best) = valid_quotes[0];
counter!("worker_router_best_quote_pool", "pool" => worker_pool_name.clone())
.increment(1);
debug!(
order_id = %best.order_id(),
number_of_candidates = valid_quotes.len(),
"ranked quotes"
);
return valid_quotes
.into_iter()
.map(|(_, q)| q.clone())
.collect();
}
let fallback = if let Some((_, any_q)) = responses.quotes.first() {
counter!("worker_router_orders_total", "status" => "no_route").increment(1);
let mut fallback = OrderQuote::new(
responses.order_id.clone(),
QuoteStatus::NoRouteFound,
any_q.amount_in().clone(),
BigUint::ZERO,
BigUint::ZERO,
BigUint::ZERO,
any_q.block().clone(),
String::new(),
any_q.sender().clone(),
any_q.receiver().clone(),
any_q.solved_against().clone(),
);
let over_max_gas = responses.quotes.iter().any(|(_, q)| {
options
.max_gas()
.is_some_and(|max| q.gas_estimate() > max)
});
fallback.set_no_route_cause(over_max_gas.then_some(SolveError::MaxGasExceeded));
fallback
} else {
let status = if responses.failed_solvers.is_empty() {
QuoteStatus::NoRouteFound
} else {
let all_timeouts = responses
.failed_solvers
.iter()
.all(|(_, e)| matches!(e, SolveError::Timeout { .. }));
let all_not_ready = responses
.failed_solvers
.iter()
.all(|(_, e)| matches!(e, SolveError::NotReady(_)));
if all_timeouts {
QuoteStatus::Timeout
} else if all_not_ready {
QuoteStatus::NotReady
} else {
QuoteStatus::NoRouteFound
}
};
let status_label = match status {
QuoteStatus::Timeout => "timeout",
QuoteStatus::NotReady => "not_ready",
_ => "no_route",
};
counter!("worker_router_orders_total", "status" => status_label).increment(1);
let label = options
.state_label()
.cloned()
.unwrap_or_else(|| "0".to_string());
let mut fallback = OrderQuote::new(
responses.order_id.clone(),
status,
BigUint::ZERO,
BigUint::ZERO,
BigUint::ZERO,
BigUint::ZERO,
BlockInfo::new(0, String::new(), 0),
String::new(),
Bytes::default(),
Bytes::default(),
label,
);
fallback.set_no_route_cause(aggregate_no_route_cause(&responses.failed_solvers));
fallback
};
vec![fallback]
}
fn effective_timeout(&self, options: &QuoteOptions) -> Duration {
options
.timeout_ms()
.map(Duration::from_millis)
.unwrap_or(self.config.default_timeout())
}
}
fn combine_with_surplus(
responses: &OrderResponses,
pool_scopes: &HashMap<String, LiquidityScope>,
options: &QuoteOptions,
public_ranked: Vec<OrderQuote>,
user_share_bps: u32,
) -> Vec<OrderQuote> {
let Some(exclusive_candidate) = best_exclusive_candidate(responses, pool_scopes, options)
else {
return public_ranked;
};
let commitment = match public_ranked
.first()
.filter(|q| q.status() == QuoteStatus::Success)
{
Some(public_reference) => {
matched_commitment(public_reference, exclusive_candidate, user_share_bps)
}
None => default_fee_commitment(exclusive_candidate),
};
let Some(committed_amount_out) = commitment else {
return public_ranked;
};
let mut result = Vec::with_capacity(public_ranked.len() + 1);
result.push(pin_commitment(exclusive_candidate, committed_amount_out));
result.extend(public_ranked);
result
}
fn best_exclusive_candidate<'a>(
responses: &'a OrderResponses,
pool_scopes: &HashMap<String, LiquidityScope>,
options: &QuoteOptions,
) -> Option<&'a OrderQuote> {
responses
.quotes
.iter()
.filter(|(pool, _)| pool_scopes.get(pool) == Some(&LiquidityScope::IncludeExclusive))
.filter(|(_, q)| q.status() == QuoteStatus::Success)
.filter(|(_, q)| {
options
.max_gas()
.map(|max| q.gas_estimate() <= max)
.unwrap_or(true)
})
.filter(|(_, q)| has_valid_exclusive_route(q))
.max_by(|(_, a), (_, b)| {
a.amount_out_net_gas()
.cmp(b.amount_out_net_gas())
})
.map(|(_, q)| q)
}
fn matched_commitment(
public_reference: &OrderQuote,
exclusive_candidate: &OrderQuote,
user_share_bps: u32,
) -> Option<BigUint> {
let public_net_amount_out = public_reference.amount_out_net_gas();
if exclusive_candidate.amount_out_net_gas() < public_net_amount_out {
return None;
}
let improvement = exclusive_candidate.amount_out_net_gas() - public_net_amount_out;
let required_net_amount_out = public_net_amount_out + user_margin(&improvement, user_share_bps);
let public_amount_out = public_reference.amount_out();
if exclusive_candidate.amount_out() < public_amount_out {
return None;
}
let gas_cost = exclusive_candidate.amount_out() - exclusive_candidate.amount_out_net_gas();
Some((required_net_amount_out + gas_cost).max(public_amount_out.clone()))
}
fn default_fee_commitment(exclusive_candidate: &OrderQuote) -> Option<BigUint> {
let exclusive_leg_amount_out = exclusive_candidate
.route()?
.swaps()
.iter()
.find(|swap| is_exclusive(swap.protocol_component()))?
.amount_out();
let realized_amount_out = exclusive_candidate.amount_out();
let fee = exclusive_leg_amount_out * BigUint::from(NO_PUBLIC_ROUTE_FEE_BPS) /
BigUint::from(BPS_DENOMINATOR);
let committed_amount_out = realized_amount_out - fee;
let gas_cost = realized_amount_out - exclusive_candidate.amount_out_net_gas();
if committed_amount_out <= gas_cost {
return None;
}
Some(committed_amount_out)
}
fn pin_commitment(exclusive_candidate: &OrderQuote, committed_amount_out: BigUint) -> OrderQuote {
let exclusive_route_amount_out = exclusive_candidate.amount_out();
let exclusive_gas_cost = exclusive_route_amount_out - exclusive_candidate.amount_out_net_gas();
let surplus_amount = exclusive_route_amount_out - &committed_amount_out;
let mut surplus_quote = exclusive_candidate.clone();
let path_final_outs: Vec<BigUint> = surplus_quote
.route()
.map(|route| {
let swaps = route.swaps();
let mut finals = vec![BigUint::ZERO; swaps.len()];
let mut current_final = BigUint::ZERO;
for i in (0..swaps.len()).rev() {
let is_terminal =
i == swaps.len() - 1 || swaps[i + 1].token_in() != swaps[i].token_out();
if is_terminal {
current_final = swaps[i].amount_out().clone();
}
finals[i] = current_final.clone();
}
finals
})
.unwrap_or_default();
if let Some(route) = surplus_quote.route_mut() {
let mut surplus = exclusive_route_amount_out - &committed_amount_out;
for (i, swap) in route.swaps_mut().iter_mut().enumerate() {
if is_exclusive(swap.protocol_component()) {
let Some(path_final_out) = path_final_outs.get(i) else {
continue;
};
let captured = surplus
.clone()
.min(path_final_out.clone());
let captured_leg = if *path_final_out == BigUint::ZERO {
BigUint::ZERO
} else {
&captured * swap.amount_out() / path_final_out
};
debug_assert!(
captured_leg <= *swap.amount_out(),
"captured amount ({captured_leg}) must not exceed the leg's output ({})",
swap.amount_out(),
);
let committed_leg = swap.amount_out() - &captured_leg;
surplus -= captured;
swap.set_committed_amount_out(committed_leg);
}
}
}
surplus_quote.set_amount_out(committed_amount_out.clone());
surplus_quote.set_amount_out_net_gas(&committed_amount_out - &exclusive_gas_cost);
let surplus_info = SurplusInfo::new(surplus_amount, committed_amount_out);
surplus_quote.with_surplus(surplus_info)
}
fn has_valid_exclusive_route(quote: &OrderQuote) -> bool {
let Some(route) = quote.route() else {
return false;
};
let swaps = route.swaps();
if swaps.is_empty() {
return false;
}
let Some(output_token) = route.output_token() else {
return false;
};
let mut exclusive_count = 0;
for swap in swaps {
if !is_exclusive(swap.protocol_component()) {
continue;
}
if *swap.token_out() != output_token {
return false;
}
exclusive_count += 1;
}
exclusive_count == 1
}
fn aggregate_no_route_cause(failed_solvers: &[(String, SolveError)]) -> Option<SolveError> {
failed_solvers
.iter()
.map(|(_, error)| error)
.min_by_key(|error| cause_tier(error))
.cloned()
}
fn cause_tier(error: &SolveError) -> u8 {
use crate::algorithm::NoPathReason;
match error {
SolveError::NoRouteFound { reason: Some(reason), .. } => match reason {
NoPathReason::SourceTokenNotInGraph | NoPathReason::DestinationTokenNotInGraph => 0,
NoPathReason::AmountTooSmall => 1,
NoPathReason::NoScorablePaths => 2,
NoPathReason::NoGraphPath => 3,
},
SolveError::InsufficientLiquidity { .. } | SolveError::MaxGasExceeded => 2,
SolveError::MissingData(_) |
SolveError::MarketDataStale { .. } |
SolveError::ComputationFailed(_) |
SolveError::NotReady(_) => 3,
SolveError::SimulationFailed(_) | SolveError::AlgorithmError(_) => 4,
SolveError::Timeout { .. } |
SolveError::QueueFull |
SolveError::Internal(_) |
SolveError::InvalidOrder(_) |
SolveError::FailedEncoding(_) |
SolveError::EncodingUnavailable(_) |
SolveError::PriceCheckFailed { .. } => 5,
SolveError::NoRouteFound { reason: None, .. } => 6,
}
}
fn refine_gas_estimates(
order_responses: &mut Vec<OrderResponses>,
encoding_options: &EncodingOptions,
) -> Result<(), SolveError> {
for responses in order_responses {
for (_, quote) in &mut responses.quotes {
if quote.status() != QuoteStatus::Success {
continue;
}
let solution = Solution::try_from(&*quote)?
.with_user_transfer_type(encoding_options.transfer_type().clone());
let refined_gas = estimate_gas_usage(&solution, derive_strategy(quote));
let naive_gas = quote.gas_estimate().clone();
if naive_gas > BigUint::ZERO {
let gas_cost_in_token_out = quote.amount_out() - quote.amount_out_net_gas();
let new_gas_cost = &gas_cost_in_token_out * &refined_gas / &naive_gas;
let new_net = if new_gas_cost <= *quote.amount_out() {
quote.amount_out() - &new_gas_cost
} else {
BigUint::ZERO
};
quote.set_amount_out_net_gas(new_net);
quote.set_gas_estimate(refined_gas);
}
}
}
Ok(())
}
fn derive_strategy(quote: &OrderQuote) -> Strategy {
let Some(route) = quote.route() else { return Strategy::Single };
let swaps = route.swaps();
if swaps.len() == 1 {
Strategy::Single
} else if swaps.iter().any(|s| *s.split() > 0.0) {
Strategy::Split
} else {
Strategy::Sequential
}
}
#[cfg(test)]
mod tests {
use std::{
collections::HashMap,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use rstest::rstest;
use tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry;
use tycho_simulation::{
tycho_common::models::Chain,
tycho_core::{
models::{token::Token, Address, Chain as SimChain},
Bytes,
},
};
use super::*;
use crate::{
algorithm::test_utils::{component, MockProtocolSim},
feed::exclusivity::mark_exclusive,
types::internal::SolveTask,
EncodingOptions, OrderSide, Route, SingleOrderQuote, Swap,
};
fn default_encoder() -> Encoder {
let registry = SwapEncoderRegistry::new(Chain::Ethereum)
.add_default_encoders(None)
.expect("default encoders should always succeed");
let encoder =
Encoder::new(Chain::Ethereum, registry).expect("encoder creation should succeed");
encoder
.router_fees()
.set(crate::encoding::router_fees::RouterFees::new(
100_000_000,
100_000,
20_000_000,
std::collections::HashMap::new(),
));
encoder
}
fn make_address(byte: u8) -> Address {
Address::from([byte; 20])
}
fn make_order() -> Order {
Order::new(
make_address(0x01),
make_address(0x02),
BigUint::from(1000u64),
OrderSide::Sell,
make_address(0xAA),
)
.with_id("test-order".to_string())
}
fn make_single_quote(amount_out_net_gas: u64) -> SingleOrderQuote {
let make_token = |addr: Address| Token {
address: addr,
symbol: "T".to_string(),
decimals: 18,
tax: Default::default(),
gas: vec![],
chain: SimChain::Ethereum,
quality: 100,
};
let tin = make_address(0x01);
let tout = make_address(0x02);
let tin_token = make_token(tin.clone());
let tout_token = make_token(tout.clone());
let swap = Swap::new(
"pool-1".to_string(),
"uniswap_v2".to_string(),
tin.clone(),
tout.clone(),
BigUint::from(1000u64),
BigUint::from(990u64),
BigUint::from(50_000u64),
component(
"0x0000000000000000000000000000000000000001",
&[tin_token.clone(), tout_token.clone()],
),
Box::new(MockProtocolSim::default()),
);
let mut tokens = HashMap::new();
tokens.insert(tin, tin_token);
tokens.insert(tout, tout_token);
let quote = OrderQuote::new(
"test-order".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(990u64),
BigUint::from(100_000u64),
BigUint::from(amount_out_net_gas),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
.with_route(Route::new(vec![swap], tokens).expect("non-empty route"));
SingleOrderQuote::new(quote, 5)
}
fn create_mock_worker_pool(
name: &str,
response: Result<SingleOrderQuote, SolveError>,
delay_ms: u64,
) -> (SolverPoolHandle, tokio::task::JoinHandle<()>) {
let (pool, worker, _) = create_counting_mock_worker_pool(name, response, delay_ms);
(pool, worker)
}
fn create_counting_mock_worker_pool(
name: &str,
response: Result<SingleOrderQuote, SolveError>,
delay_ms: u64,
) -> (SolverPoolHandle, tokio::task::JoinHandle<()>, Arc<AtomicUsize>) {
let (tx, rx) = async_channel::bounded::<SolveTask>(10);
let handle = TaskQueueHandle::from_sender(tx);
let received = Arc::new(AtomicUsize::new(0));
let worker = {
let received = Arc::clone(&received);
tokio::spawn(async move {
while let Ok(task) = rx.recv().await {
received.fetch_add(1, Ordering::SeqCst);
if delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(delay_ms)).await;
}
task.respond(response.clone());
}
})
};
(SolverPoolHandle::new(name, handle), worker, received)
}
#[test]
fn test_config_default() {
let config = WorkerPoolRouterConfig::default();
assert_eq!(config.default_timeout(), Duration::from_secs(1));
assert_eq!(config.min_responses(), 1);
}
#[test]
fn test_config_builder() {
let config = WorkerPoolRouterConfig::default()
.with_timeout(Duration::from_millis(500))
.with_min_responses(2);
assert_eq!(config.default_timeout(), Duration::from_millis(500));
assert_eq!(config.min_responses(), 2);
}
#[tokio::test]
async fn test_router_no_pools() {
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
assert!(matches!(result, Err(SolveError::Internal(_))));
}
#[tokio::test]
async fn test_router_single_pool_success() {
let (pool, worker) = create_mock_worker_pool("pool_a", Ok(make_single_quote(900)), 0);
let worker_router =
WorkerPoolRouter::new(vec![pool], WorkerPoolRouterConfig::default(), default_encoder());
let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
let request = QuoteRequest::new(vec![make_order()], options);
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
assert!(result.is_ok());
let quote = result.unwrap();
assert_eq!(quote.orders().len(), 1);
assert_eq!(quote.orders()[0].status(), QuoteStatus::Success);
assert_eq!(*quote.orders()[0].amount_out_net_gas(), BigUint::from(873u64));
assert!(!quote.orders()[0]
.transaction()
.unwrap()
.data()
.is_empty());
drop(worker_router);
worker.abort();
}
#[tokio::test]
async fn test_router_selects_best_of_two() {
let (pool_a, worker_a) = create_mock_worker_pool("pool_a", Ok(make_single_quote(800)), 0);
let (pool_b, worker_b) = create_mock_worker_pool("pool_b", Ok(make_single_quote(950)), 0);
let config = WorkerPoolRouterConfig::default().with_min_responses(2);
let worker_router = WorkerPoolRouter::new(vec![pool_a, pool_b], config, default_encoder());
let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
let request = QuoteRequest::new(vec![make_order()], options);
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
assert!(result.is_ok());
let quote = result.unwrap();
assert_eq!(quote.orders().len(), 1);
assert_eq!(*quote.orders()[0].amount_out_net_gas(), BigUint::from(938u64));
assert!(!quote.orders()[0]
.transaction()
.unwrap()
.data()
.is_empty());
drop(worker_router);
worker_a.abort();
worker_b.abort();
}
#[tokio::test]
async fn test_router_timeout() {
let (pool, worker) = create_mock_worker_pool("slow_pool", Ok(make_single_quote(900)), 500);
let config = WorkerPoolRouterConfig::default().with_timeout(Duration::from_millis(50));
let worker_router = WorkerPoolRouter::new(vec![pool], config, default_encoder());
let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
assert!(result.is_ok());
let quote = result.unwrap();
assert_eq!(quote.orders().len(), 1);
assert!(matches!(
quote.orders()[0].status(),
QuoteStatus::Timeout | QuoteStatus::NoRouteFound
));
drop(worker_router);
worker.abort();
}
#[tokio::test]
async fn test_router_early_return_on_min_responses() {
let (pool_a, worker_a) =
create_mock_worker_pool("fast_pool", Ok(make_single_quote(800)), 0);
let (pool_b, worker_b) =
create_mock_worker_pool("slow_pool", Ok(make_single_quote(950)), 500);
let config = WorkerPoolRouterConfig::default()
.with_timeout(Duration::from_millis(1000))
.with_min_responses(1);
let worker_router = WorkerPoolRouter::new(vec![pool_a, pool_b], config, default_encoder());
let start = Instant::now();
let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
let request = QuoteRequest::new(vec![make_order()], options);
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
let elapsed = start.elapsed();
assert!(result.is_ok());
assert!(elapsed < Duration::from_millis(200));
let quote = result.unwrap();
assert_eq!(quote.orders().len(), 1);
assert_eq!(quote.orders()[0].status(), QuoteStatus::Success);
assert!(!quote.orders()[0]
.transaction()
.unwrap()
.data()
.is_empty());
drop(worker_router);
worker_a.abort();
worker_b.abort();
}
#[rstest]
#[case::pending_exclusive_pool(ExclusiveAccess::Granted, 0, Some(300), 1, true)]
#[case::pending_public_pool(ExclusiveAccess::Granted, 300, Some(0), 1, true)]
#[case::failed_exclusive_pool(ExclusiveAccess::Granted, 0, None, 1, false)]
#[case::denied_access(ExclusiveAccess::Denied, 0, Some(500), 2, false)]
#[tokio::test]
async fn test_router_early_return_scope_gating(
#[case] access: ExclusiveAccess,
#[case] public_delay_ms: u64,
#[case] exclusive_delay_ms: Option<u64>,
#[case] min_responses: usize,
#[case] expect_surplus: bool,
) {
let (public_pool, public_worker) =
create_mock_worker_pool("public_pool", Ok(make_single_quote(800)), public_delay_ms);
let exclusive_response = match exclusive_delay_ms {
Some(_) => Ok(make_exclusive_quote(1100)),
None => {
Err(SolveError::NoRouteFound { order_id: "test-order".to_string(), reason: None })
}
};
let public_pool = public_pool.with_liquidity_scope(LiquidityScope::PublicOnly);
let (exclusive_pool, exclusive_worker) = create_mock_worker_pool(
"exclusive_pool",
exclusive_response,
exclusive_delay_ms.unwrap_or(0),
);
let exclusive_pool = exclusive_pool.with_liquidity_scope(LiquidityScope::IncludeExclusive);
let config = WorkerPoolRouterConfig::default()
.with_timeout(Duration::from_millis(2000))
.with_min_responses(min_responses);
let worker_router =
WorkerPoolRouter::new(vec![public_pool, exclusive_pool], config, default_encoder());
let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
let start = Instant::now();
let result = worker_router
.quote(request, access)
.await
.expect("quote should succeed");
let elapsed = start.elapsed();
assert!(elapsed < Duration::from_millis(500), "took {elapsed:?}");
let order = &result.orders()[0];
assert_eq!(order.status(), QuoteStatus::Success);
assert_eq!(*order.amount_out(), BigUint::from(990u64));
assert_eq!(order.surplus_amount().is_some(), expect_surplus);
drop(worker_router);
public_worker.abort();
exclusive_worker.abort();
}
#[rstest]
#[case::denied(ExclusiveAccess::Denied, false)]
#[case::granted(ExclusiveAccess::Granted, true)]
#[tokio::test]
async fn test_router_exclusive_access_allocation(
#[case] access: ExclusiveAccess,
#[case] expect_exclusive_leg: bool,
) {
let (public_pool, public_worker) =
create_mock_worker_pool("public_pool", Ok(make_single_quote(800)), 0);
let (exclusive_pool, exclusive_worker, exclusive_tasks) =
create_counting_mock_worker_pool("exclusive_pool", Ok(make_exclusive_quote(1100)), 0);
let exclusive_pool = exclusive_pool.with_liquidity_scope(LiquidityScope::IncludeExclusive);
let worker_router = WorkerPoolRouter::new(
vec![public_pool, exclusive_pool],
WorkerPoolRouterConfig::default().with_timeout(Duration::from_millis(2000)),
default_encoder(),
);
let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
let result = worker_router
.quote(request, access)
.await
.expect("quote should succeed");
assert_eq!(
exclusive_tasks.load(Ordering::SeqCst) > 0,
expect_exclusive_leg,
"exclusive pool dispatch"
);
let order = &result.orders()[0];
assert_eq!(order.status(), QuoteStatus::Success);
let routes_through_exclusive = order
.route()
.expect("successful quote has a route")
.swaps()
.iter()
.any(|swap| is_exclusive(swap.protocol_component()));
assert_eq!(routes_through_exclusive, expect_exclusive_leg);
assert_eq!(order.surplus_amount().is_some(), expect_exclusive_leg);
assert_eq!(*order.amount_out(), BigUint::from(990u64));
drop(worker_router);
public_worker.abort();
exclusive_worker.abort();
}
#[rstest]
#[case::under_limit(100, Some(200), true)]
#[case::at_limit(200, Some(200), true)]
#[case::over_limit(300, Some(200), false)]
#[case::no_limit(500, None, true)]
fn test_max_gas_constraint(
#[case] gas_estimate: u64,
#[case] max_gas: Option<u64>,
#[case] should_pass: bool,
) {
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![(
"pool".to_string(),
OrderQuote::new(
"test".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(990u64),
BigUint::from(gas_estimate),
BigUint::from(900u64),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
),
)],
failed_solvers: vec![],
};
let options = match max_gas {
Some(gas) => QuoteOptions::default().with_max_gas(BigUint::from(gas)),
None => QuoteOptions::default(),
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &options);
if should_pass {
assert_eq!(result[0].status(), QuoteStatus::Success);
} else {
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
}
}
#[tokio::test]
async fn test_router_captures_solver_errors() {
let (pool, worker) =
create_mock_worker_pool("error_pool", Err(SolveError::no_route_found("test-order")), 0);
let worker_router =
WorkerPoolRouter::new(vec![pool], WorkerPoolRouterConfig::default(), default_encoder());
let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
let result = worker_router
.quote(request, ExclusiveAccess::Denied)
.await;
assert!(result.is_ok());
let quote = result.unwrap();
assert_eq!(quote.orders().len(), 1);
assert_eq!(quote.orders()[0].status(), QuoteStatus::NoRouteFound);
drop(worker_router);
worker.abort();
}
#[test]
fn test_rank_quotes_all_timeouts_returns_timeout_status() {
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![],
failed_solvers: vec![
("pool_a".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
("pool_b".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result.len(), 1);
assert_eq!(result[0].status(), QuoteStatus::Timeout);
}
#[test]
fn test_rank_quotes_mixed_failures_returns_no_route_found() {
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![],
failed_solvers: vec![
("pool_a".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
("pool_b".to_string(), SolveError::no_route_found("test")),
],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result.len(), 1);
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
}
#[test]
fn test_rank_quotes_no_failures_returns_no_route_found() {
let responses =
OrderResponses { order_id: "test".to_string(), quotes: vec![], failed_solvers: vec![] };
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result.len(), 1);
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
}
#[test]
fn rank_quotes_attaches_no_route_reason() {
use crate::algorithm::NoPathReason;
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![],
failed_solvers: vec![
(
"pool_a".to_string(),
SolveError::no_route_found_with_reason("test", NoPathReason::NoGraphPath),
),
(
"pool_b".to_string(),
SolveError::no_route_found_with_reason(
"test",
NoPathReason::DestinationTokenNotInGraph,
),
),
],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result.len(), 1);
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
assert_eq!(result[0].no_route_reason(), Some(NoPathReason::DestinationTokenNotInGraph));
}
#[test]
fn test_rank_quotes_no_route_reason_single_failure() {
use crate::algorithm::NoPathReason;
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![],
failed_solvers: vec![(
"pool_a".to_string(),
SolveError::no_route_found_with_reason("test", NoPathReason::NoGraphPath),
)],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result[0].no_route_reason(), Some(NoPathReason::NoGraphPath));
}
#[test]
fn test_aggregate_cause_surfaces_amount_too_small() {
use crate::algorithm::NoPathReason;
let failed = vec![(
"bf".to_string(),
SolveError::no_route_found_with_reason("o1", NoPathReason::AmountTooSmall),
)];
assert!(matches!(
aggregate_no_route_cause(&failed),
Some(SolveError::NoRouteFound { reason: Some(NoPathReason::AmountTooSmall), .. })
));
}
#[test]
fn test_aggregate_no_route_cause_independent_of_pool_order() {
use crate::algorithm::NoPathReason;
let dust = || SolveError::no_route_found_with_reason("o1", NoPathReason::AmountTooSmall);
let no_path = || SolveError::no_route_found_with_reason("o1", NoPathReason::NoGraphPath);
let forward = vec![("bf1".to_string(), no_path()), ("bf3".to_string(), dust())];
let reversed = vec![("bf3".to_string(), dust()), ("bf1".to_string(), no_path())];
for failed in [forward, reversed] {
assert!(matches!(
aggregate_no_route_cause(&failed),
Some(SolveError::NoRouteFound { reason: Some(NoPathReason::AmountTooSmall), .. })
));
}
}
#[test]
fn test_aggregate_token_not_in_graph_wins_over_amount_too_small() {
use crate::algorithm::NoPathReason;
let failed = vec![
(
"bf".to_string(),
SolveError::no_route_found_with_reason("o1", NoPathReason::AmountTooSmall),
),
(
"ml".to_string(),
SolveError::no_route_found_with_reason(
"o2",
NoPathReason::DestinationTokenNotInGraph,
),
),
];
assert!(matches!(
aggregate_no_route_cause(&failed),
Some(SolveError::NoRouteFound {
reason: Some(NoPathReason::DestinationTokenNotInGraph),
..
})
));
}
#[test]
fn test_aggregate_prefers_liquidity_over_infra() {
let failed = vec![
("a".to_string(), SolveError::QueueFull),
("b".to_string(), SolveError::insufficient_liquidity(1u32.into(), 0u32.into())),
];
assert!(matches!(
aggregate_no_route_cause(&failed),
Some(SolveError::InsufficientLiquidity { .. })
));
}
#[test]
fn test_rank_quotes_max_gas_filtered_sets_max_gas_exceeded() {
let responses = OrderResponses {
order_id: "o1".to_string(),
quotes: vec![(
"pool".to_string(),
OrderQuote::new(
"o1".to_string(),
QuoteStatus::Success,
BigUint::from(1_000u64),
BigUint::from(990u64),
BigUint::from(100_000u64),
BigUint::from(990u64),
BlockInfo::new(1, "0xabc".to_string(), 0),
"test".to_string(),
Bytes::default(),
Bytes::default(),
"1".to_string(),
),
)],
failed_solvers: vec![],
};
let options = QuoteOptions::default().with_max_gas(BigUint::from(1u64));
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &options);
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
assert!(matches!(result[0].no_route_cause(), Some(SolveError::MaxGasExceeded)));
}
#[test]
fn test_rank_quotes_no_cause_when_gas_within_max() {
let responses = OrderResponses {
order_id: "o1".to_string(),
quotes: vec![(
"pool".to_string(),
OrderQuote::new(
"o1".to_string(),
QuoteStatus::NoRouteFound,
BigUint::from(1_000u64),
BigUint::from(990u64),
BigUint::from(100u64),
BigUint::from(990u64),
BlockInfo::new(1, "0xabc".to_string(), 0),
"test".to_string(),
Bytes::default(),
Bytes::default(),
"1".to_string(),
),
)],
failed_solvers: vec![],
};
let options = QuoteOptions::default().with_max_gas(BigUint::from(1_000u64));
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &options);
assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
assert!(
result[0].no_route_cause().is_none(),
"gas within max_gas must not be labelled MaxGasExceeded: {:?}",
result[0].no_route_cause()
);
}
#[test]
fn test_all_timeout_fallback_carries_timeout_cause() {
let responses = OrderResponses {
order_id: "o1".to_string(),
quotes: vec![],
failed_solvers: vec![("pool".to_string(), SolveError::Timeout { elapsed_ms: 9 })],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result[0].status(), QuoteStatus::Timeout);
assert!(matches!(result[0].no_route_cause(), Some(SolveError::Timeout { .. })));
}
#[test]
fn test_rank_quotes_returns_sorted_candidates() {
let responses = OrderResponses {
order_id: "test".to_string(),
quotes: vec![
(
"pool_a".to_string(),
OrderQuote::new(
"test".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(800u64),
BigUint::from(100_000u64),
BigUint::from(800u64),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
),
),
(
"pool_b".to_string(),
OrderQuote::new(
"test".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(950u64),
BigUint::from(100_000u64),
BigUint::from(950u64),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
),
),
],
failed_solvers: vec![],
};
let worker_router =
WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
assert_eq!(result.len(), 2);
assert_eq!(*result[0].amount_out_net_gas(), BigUint::from(950u64));
assert_eq!(*result[1].amount_out_net_gas(), BigUint::from(800u64));
}
fn make_exclusive_quote_with_leg(
amount_out: u64,
amount_out_net_gas: u64,
leg_amount_out: u64,
) -> SingleOrderQuote {
let make_token = |addr: Address| Token {
address: addr,
symbol: "T".to_string(),
decimals: 18,
tax: Default::default(),
gas: vec![],
chain: SimChain::Ethereum,
quality: 100,
};
let tin = make_address(0x01);
let tout = make_address(0x02);
let tin_token = make_token(tin.clone());
let tout_token = make_token(tout.clone());
let mut comp = component(
"0x0000000000000000000000000000000000000002",
&[tin_token.clone(), tout_token.clone()],
);
mark_exclusive(&mut comp);
let swap = Swap::new(
"pool-perm".to_string(),
"vm:exclusive".to_string(),
tin.clone(),
tout.clone(),
BigUint::from(1000u64),
BigUint::from(leg_amount_out),
BigUint::from(50_000u64),
comp,
Box::new(MockProtocolSim::default()),
);
let mut tokens = HashMap::new();
tokens.insert(tin, tin_token);
tokens.insert(tout, tout_token);
let quote = OrderQuote::new(
"test-order".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(amount_out),
BigUint::from(100_000u64),
BigUint::from(amount_out_net_gas),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
.with_route(Route::new(vec![swap], tokens).expect("non-empty route"));
SingleOrderQuote::new(quote, 5)
}
fn make_exclusive_quote(amount_out: u64) -> SingleOrderQuote {
make_exclusive_quote_with_leg(amount_out, amount_out, amount_out)
}
fn make_exclusive_split_quote(public_leg_out: u64, exclusive_leg_out: u64) -> SingleOrderQuote {
let make_token = |addr: Address| Token {
address: addr,
symbol: "T".to_string(),
decimals: 18,
tax: Default::default(),
gas: vec![],
chain: SimChain::Ethereum,
quality: 100,
};
let tin = make_address(0x01);
let tout = make_address(0x02);
let tin_token = make_token(tin.clone());
let tout_token = make_token(tout.clone());
let public_swap = Swap::new(
"pool-pub".to_string(),
"uniswap_v2".to_string(),
tin.clone(),
tout.clone(),
BigUint::from(500u64),
BigUint::from(public_leg_out),
BigUint::from(50_000u64),
component(
"0x0000000000000000000000000000000000000001",
&[tin_token.clone(), tout_token.clone()],
),
Box::new(MockProtocolSim::default()),
);
let mut exclusive_comp = component(
"0x0000000000000000000000000000000000000002",
&[tin_token.clone(), tout_token.clone()],
);
mark_exclusive(&mut exclusive_comp);
let exclusive_swap = Swap::new(
"pool-perm".to_string(),
"vm:exclusive".to_string(),
tin.clone(),
tout.clone(),
BigUint::from(500u64),
BigUint::from(exclusive_leg_out),
BigUint::from(50_000u64),
exclusive_comp,
Box::new(MockProtocolSim::default()),
);
let mut tokens = HashMap::new();
tokens.insert(tin, tin_token);
tokens.insert(tout, tout_token);
let total = public_leg_out + exclusive_leg_out;
let quote = OrderQuote::new(
"test-order".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(total),
BigUint::from(100_000u64),
BigUint::from(total),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
.with_route(
Route::new(vec![public_swap, exclusive_swap], tokens).expect("non-empty route"),
);
SingleOrderQuote::new(quote, 5)
}
fn make_public_quote_with_net(amount_out: u64, amount_out_net_gas: u64) -> SingleOrderQuote {
let make_token = |addr: Address| Token {
address: addr,
symbol: "T".to_string(),
decimals: 18,
tax: Default::default(),
gas: vec![],
chain: SimChain::Ethereum,
quality: 100,
};
let tin = make_address(0x01);
let tout = make_address(0x02);
let tin_token = make_token(tin.clone());
let tout_token = make_token(tout.clone());
let swap = Swap::new(
"pool-1".to_string(),
"uniswap_v2".to_string(),
tin.clone(),
tout.clone(),
BigUint::from(1000u64),
BigUint::from(amount_out),
BigUint::from(50_000u64),
component(
"0x0000000000000000000000000000000000000001",
&[tin_token.clone(), tout_token.clone()],
),
Box::new(MockProtocolSim::default()),
);
let mut tokens = HashMap::new();
tokens.insert(tin, tin_token);
tokens.insert(tout, tout_token);
let quote = OrderQuote::new(
"test-order".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(amount_out),
BigUint::from(100_000u64),
BigUint::from(amount_out_net_gas),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
.with_route(Route::new(vec![swap], tokens).expect("non-empty route"));
SingleOrderQuote::new(quote, 5)
}
fn make_public_quote_zero_gas(amount_out: u64) -> SingleOrderQuote {
make_public_quote_with_net(amount_out, amount_out)
}
fn exclusive_access_responses(public_out: u64, exclusive_out: u64) -> OrderResponses {
let public = make_public_quote_zero_gas(public_out)
.order()
.clone();
let exclusive_access = make_exclusive_quote(exclusive_out)
.order()
.clone();
OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![
("public_pool".to_string(), public),
("exclusive_access_pool".to_string(), exclusive_access),
],
failed_solvers: vec![],
}
}
fn exclusive_access_pool_scopes() -> HashMap<String, LiquidityScope> {
HashMap::from([
("public_pool".to_string(), LiquidityScope::PublicOnly),
("exclusive_access_pool".to_string(), LiquidityScope::IncludeExclusive),
])
}
#[rstest]
#[case::exclusive_beats_public((900, 900), (950, 950), 1_000, (905, 905, Some(45)))]
#[case::exclusive_below_public((950, 950), (900, 900), 1_000, (950, 950, None))]
#[case::exclusive_ties_public_net((900, 900), (900, 900), 1_000, (900, 900, Some(0)))]
#[case::exclusive_marginally_better((999, 999), (1000, 1000), 1_000, (1000, 1000, Some(0)))]
#[case::exclusive_cannot_cover_public_gross((1000, 950), (990, 980), 1_000, (1000, 950, None))]
#[case::exclusive_with_higher_gas((1000, 950), (1100, 960), 1_000, (1091, 951, Some(9)))]
#[case::exclusive_with_lower_gas((1000, 950), (1100, 1060), 1_000, (1001, 961, Some(99)))]
#[case::sub_bps_markup((1_000_000, 1_000_000), (1_000_050, 1_000_050), 1_000,
(1_000_005, 1_000_005, Some(45)))]
#[case::zero_share((900, 900), (950, 950), 0, (900, 900, Some(50)))]
#[case::full_share((900, 900), (950, 950), 10_000, (950, 950, Some(0)))]
fn test_combine_head_selection(
#[case] public: (u64, u64),
#[case] exclusive: (u64, u64),
#[case] user_share_bps: u32,
#[case] expected: (u64, u64, Option<u64>),
) {
let (public_out, public_net) = public;
let (exclusive_out, exclusive_net) = exclusive;
let (expected_amount_out, expected_net, expected_surplus) = expected;
let responses = responses_with_gas(public_out, public_net, exclusive_out, exclusive_net);
let public_ranked = vec![make_public_quote_with_net(public_out, public_net)
.order()
.clone()];
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
public_ranked,
user_share_bps,
);
let expected_surplus = expected_surplus.map(BigUint::from);
assert_eq!(combined.len(), if expected_surplus.is_some() { 2 } else { 1 });
assert_eq!(*combined[0].amount_out(), BigUint::from(expected_amount_out));
assert_eq!(*combined[0].amount_out_net_gas(), BigUint::from(expected_net));
assert_eq!(combined[0].surplus_amount(), expected_surplus.as_ref());
if expected_surplus.is_some() {
assert_eq!(
combined[0].committed_amount_out(),
Some(&BigUint::from(expected_amount_out))
);
}
}
#[rstest]
#[case::over_max_gas(
make_exclusive_quote(1100).order().clone(),
Some(50_000)
)]
#[case::mid_route_exclusive_leg(
make_route_quote(&[("vm:exclusive", 0x01, 0x02), ("uniswap_v2", 0x02, 0x03)]),
None
)]
fn test_combine_filters_exclusive_candidate(
#[case] exclusive_quote: OrderQuote,
#[case] max_gas: Option<u64>,
) {
let mut options = QuoteOptions::default();
if let Some(max) = max_gas {
options = options.with_max_gas(BigUint::from(max));
}
let responses = OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![
(
"public_pool".to_string(),
make_public_quote_zero_gas(900)
.order()
.clone(),
),
("exclusive_access_pool".to_string(), exclusive_quote),
],
failed_solvers: vec![],
};
let public_ranked = vec![make_public_quote_zero_gas(900)
.order()
.clone()];
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&options,
public_ranked,
1_000,
);
assert_eq!(combined.len(), 1);
assert_eq!(*combined[0].amount_out(), BigUint::from(900u64));
assert_eq!(combined[0].surplus_amount(), None);
}
fn no_route_quote() -> OrderQuote {
OrderQuote::new(
"test-order".to_string(),
QuoteStatus::NoRouteFound,
BigUint::from(1000u64),
BigUint::ZERO,
BigUint::ZERO,
BigUint::ZERO,
BlockInfo::new(1, "0x123".to_string(), 1000),
String::new(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
}
fn no_public_route_responses(exclusive: OrderQuote) -> OrderResponses {
OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![("exclusive_access_pool".to_string(), exclusive)],
failed_solvers: vec![(
"public_pool".to_string(),
SolveError::NoRouteFound { order_id: "test-order".to_string(), reason: None },
)],
}
}
#[test]
fn test_combine_no_public_route_applies_default_fee() {
let responses = no_public_route_responses(
make_exclusive_quote(1_000_000)
.order()
.clone(),
);
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
vec![no_route_quote()],
1_000,
);
assert_eq!(combined.len(), 2);
assert_eq!(combined[0].status(), QuoteStatus::Success);
assert_eq!(*combined[0].amount_out(), BigUint::from(999_500u64));
assert_eq!(*combined[0].amount_out_net_gas(), BigUint::from(999_500u64));
assert_eq!(combined[0].surplus_amount(), Some(&BigUint::from(500u64)));
let exclusive_leg = combined[0]
.route()
.expect("surplus quote should have a route")
.swaps()
.iter()
.find(|s| is_exclusive(s.protocol_component()))
.expect("should have an exclusive swap")
.committed_amount_out()
.cloned();
assert_eq!(exclusive_leg, Some(BigUint::from(999_500u64)));
assert_eq!(combined[1].status(), QuoteStatus::NoRouteFound);
}
#[test]
fn test_combine_no_public_route_split_route_fee_on_exclusive_leg() {
let responses = no_public_route_responses(
make_exclusive_split_quote(600_000, 500_000)
.order()
.clone(),
);
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
vec![no_route_quote()],
1_000,
);
assert_eq!(*combined[0].amount_out(), BigUint::from(1_099_750u64));
assert_eq!(combined[0].surplus_amount(), Some(&BigUint::from(250u64)));
let route = combined[0]
.route()
.expect("surplus quote should have a route");
let public_leg = route
.swaps()
.iter()
.find(|s| !is_exclusive(s.protocol_component()))
.expect("should have a public swap");
assert_eq!(public_leg.committed_amount_out(), None);
let exclusive_leg = route
.swaps()
.iter()
.find(|s| is_exclusive(s.protocol_component()))
.expect("should have an exclusive swap");
assert_eq!(exclusive_leg.committed_amount_out(), Some(&BigUint::from(499_750u64)));
}
#[test]
fn test_combine_no_public_route_fee_below_gas() {
let responses = no_public_route_responses(
make_exclusive_quote_with_leg(10_000, 3, 10_000)
.order()
.clone(),
);
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
vec![no_route_quote()],
1_000,
);
assert_eq!(combined.len(), 1);
assert_eq!(combined[0].status(), QuoteStatus::NoRouteFound);
}
#[test]
fn test_combine_stamps_per_leg_committed_amount_out() {
let responses = exclusive_access_responses(900, 1000);
let public_ranked = vec![make_public_quote_zero_gas(900)
.order()
.clone()];
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
public_ranked,
1_000,
);
let surplus_quote = &combined[0];
let route = surplus_quote
.route()
.expect("surplus quote should have a route");
let perm_swap = route
.swaps()
.iter()
.find(|s| is_exclusive(s.protocol_component()))
.expect("should have an exclusive swap");
assert_eq!(perm_swap.committed_amount_out(), Some(&BigUint::from(910u64)),);
}
#[test]
fn test_combine_committed_leg_deduction() {
let responses = OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![
(
"public_pool".to_string(),
make_public_quote_zero_gas(900)
.order()
.clone(),
),
(
"exclusive_access_pool".to_string(),
make_exclusive_quote_with_leg(1000, 1000, 995)
.order()
.clone(),
),
],
failed_solvers: vec![],
};
let public_ranked = vec![make_public_quote_zero_gas(900)
.order()
.clone()];
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
public_ranked,
1_000,
);
let route = combined[0]
.route()
.expect("surplus quote should have a route");
let perm_swap = route
.swaps()
.iter()
.find(|s| is_exclusive(s.protocol_component()))
.expect("should have an exclusive swap");
assert_eq!(perm_swap.committed_amount_out(), Some(&BigUint::from(905u64)));
}
#[rstest]
#[case::zero("0", Some(0))]
#[case::whole_improvement("10000", Some(10_000))]
#[case::padded(" 2500 ", Some(2_500))]
#[case::above_whole_improvement("10001", None)]
#[case::negative("-1", None)]
#[case::fractional("0.5", None)]
#[case::empty("", None)]
fn test_parse_user_improvement_share_bps(#[case] raw: &str, #[case] expected: Option<u32>) {
assert_eq!(parse_user_improvement_share_bps(raw), expected);
}
#[test]
fn test_combine_split_route_attribution() {
let responses = OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![
(
"public_pool".to_string(),
make_public_quote_zero_gas(1000)
.order()
.clone(),
),
(
"exclusive_access_pool".to_string(),
make_exclusive_split_quote(600, 500)
.order()
.clone(),
),
],
failed_solvers: vec![],
};
let public_ranked = vec![make_public_quote_zero_gas(1000)
.order()
.clone()];
let combined = combine_with_surplus(
&responses,
&exclusive_access_pool_scopes(),
&QuoteOptions::default(),
public_ranked,
1_000,
);
assert_eq!(*combined[0].amount_out(), BigUint::from(1010u64));
let route = combined[0]
.route()
.expect("surplus quote should have a route");
let public_leg = route
.swaps()
.iter()
.find(|s| !is_exclusive(s.protocol_component()))
.expect("should have a public swap");
assert_eq!(public_leg.committed_amount_out(), None);
let exclusive_leg = route
.swaps()
.iter()
.find(|s| is_exclusive(s.protocol_component()))
.expect("should have an exclusive swap");
assert_eq!(exclusive_leg.committed_amount_out(), Some(&BigUint::from(410u64)));
}
fn responses_with_gas(
public_out: u64,
public_net: u64,
exclusive_out: u64,
exclusive_net: u64,
) -> OrderResponses {
OrderResponses {
order_id: "test-order".to_string(),
quotes: vec![
(
"public_pool".to_string(),
make_public_quote_with_net(public_out, public_net)
.order()
.clone(),
),
(
"exclusive_access_pool".to_string(),
make_exclusive_quote_with_leg(exclusive_out, exclusive_net, exclusive_out)
.order()
.clone(),
),
],
failed_solvers: vec![],
}
}
fn make_route_quote(legs: &[(&str, u8, u8)]) -> OrderQuote {
let make_token = |addr: &Address| Token {
address: addr.clone(),
symbol: "T".to_string(),
decimals: 18,
tax: Default::default(),
gas: vec![],
chain: SimChain::Ethereum,
quality: 100,
};
let mut tokens = HashMap::new();
let mut swaps = Vec::new();
for (protocol_system, tin_byte, tout_byte) in legs {
let tin = make_address(*tin_byte);
let tout = make_address(*tout_byte);
let tin_token = make_token(&tin);
let tout_token = make_token(&tout);
let mut comp = component(
"0x0000000000000000000000000000000000000002",
&[tin_token.clone(), tout_token.clone()],
);
comp.protocol_system = protocol_system.to_string();
if *protocol_system == "vm:exclusive" {
mark_exclusive(&mut comp);
}
swaps.push(Swap::new(
format!("pool-{tin_byte}-{tout_byte}"),
protocol_system.to_string(),
tin.clone(),
tout.clone(),
BigUint::from(1000u64),
BigUint::from(1000u64),
BigUint::from(50_000u64),
comp,
Box::new(MockProtocolSim::default()),
));
tokens.insert(tin, tin_token);
tokens.insert(tout, tout_token);
}
OrderQuote::new(
"test-order".to_string(),
QuoteStatus::Success,
BigUint::from(1000u64),
BigUint::from(1000u64),
BigUint::from(100_000u64),
BigUint::from(1000u64),
BlockInfo::new(1, "0x123".to_string(), 1000),
"test".to_string(),
Bytes::from(make_address(0xAA).as_ref()),
Bytes::from(make_address(0xAA).as_ref()),
"1".to_string(),
)
.with_route(Route::new(swaps, tokens).expect("non-empty route"))
}
#[rstest]
#[case::terminal_exclusive_leg(
&[("uniswap_v2", 0x01, 0x02), ("vm:exclusive", 0x02, 0x03)], true)]
#[case::mid_route_exclusive_leg(
&[("vm:exclusive", 0x01, 0x02), ("uniswap_v2", 0x02, 0x03)], false)]
#[case::exclusive_leg_ending_its_path(
&[("vm:exclusive", 0x01, 0x02), ("uniswap_v2", 0x01, 0x03), ("uniswap_v2", 0x03, 0x02)],
true)]
#[case::no_exclusive_leg(
&[("uniswap_v2", 0x01, 0x02), ("uniswap_v2", 0x02, 0x03)], false)]
#[case::two_exclusive_legs(
&[("vm:exclusive", 0x01, 0x02), ("vm:exclusive", 0x01, 0x02)], false)]
#[case::exclusive_leg_feeding_diamond_merge(
&[
("vm:exclusive", 0x01, 0x02),
("uniswap_v2", 0x01, 0x03),
("uniswap_v2", 0x02, 0x04),
("uniswap_v2", 0x03, 0x04),
],
false)]
#[case::exclusive_leg_feeding_diamond_merge_reordered(
&[
("vm:exclusive", 0x01, 0x02),
("uniswap_v2", 0x02, 0x04),
("uniswap_v2", 0x01, 0x03),
("uniswap_v2", 0x03, 0x04),
],
false)]
#[case::exclusive_leg_on_sibling_branch(
&[
("uniswap_v2", 0x01, 0x02),
("uniswap_v2", 0x02, 0x03),
("uniswap_v2", 0x03, 0x04),
("uniswap_v2", 0x02, 0x05),
("vm:exclusive", 0x05, 0x04),
],
true)]
fn test_exclusive_route_validation(#[case] legs: &[(&str, u8, u8)], #[case] expected: bool) {
let quote = make_route_quote(legs);
assert_eq!(has_valid_exclusive_route("e), expected);
}
}