use std::time::{Duration, Instant};
use async_trait::async_trait;
use num_bigint::BigUint;
use num_traits::Zero;
use petgraph::graph::NodeIndex;
use rustc_hash::{FxHashMap, FxHashSet};
use tracing::{debug, instrument, trace, warn, Span};
use tycho_simulation::{
tycho_common::models::Address, tycho_core::simulation::protocol_sim::Price,
};
use crate::{
algorithm::{
bellman_ford::{BellmanFordContext, FindRouteOptions, ReachOutcome, ReachedToken},
Algorithm, AlgorithmConfig, BellmanFordAlgorithm,
},
derived::{
computation::{
ComputationId, ComputationOutput, ComputationRequirements, DerivedComputation,
FailedItem, FailedItemError,
},
error::ComputationError,
manager::{ChangedComponents, SharedDerivedDataRef},
store::DerivedData,
types::{TokenGasPrices, TokenPriceEntry, TokenPricesWithDeps},
},
feed::market_data::MarketData,
graph::{GraphManager, PetgraphStableDiGraphManager},
types::{ComponentId, Order, OrderSide, RouteExclusions},
};
struct PricingPass<'a> {
algorithm: &'a BellmanFordAlgorithm,
graph: &'a <BellmanFordAlgorithm as Algorithm>::GraphType,
ctx: BellmanFordContext,
computation: &'a TokenGasPriceComputation,
buys: ReachOutcome,
gas_node: NodeIndex,
hops_to_gas: FxHashMap<NodeIndex, usize>,
token_nodes: FxHashMap<Address, NodeIndex>,
}
struct SellLeg {
amount_out: BigUint,
components: FxHashSet<ComponentId>,
}
impl<'a> PricingPass<'a> {
fn new(
algorithm: &'a BellmanFordAlgorithm,
graph: &'a <BellmanFordAlgorithm as Algorithm>::GraphType,
ctx: BellmanFordContext,
computation: &'a TokenGasPriceComputation,
) -> Self {
let buys = algorithm.reach_from_source_token(&ctx, &computation.probe_amount);
let gas_node = ctx.token_in_node;
let token_nodes = ctx
.node_address
.iter()
.map(|(&node, address)| (address.clone(), node))
.collect();
let hops_to_gas = BellmanFordAlgorithm::get_hops_to_reach(
graph,
gas_node,
gas_node,
algorithm.max_hops(),
&RouteExclusions::default(),
);
Self { algorithm, graph, ctx, computation, buys, gas_node, hops_to_gas, token_nodes }
}
fn sell_loop(&mut self, tokens_to_price: FxHashSet<Address>, block: u64) -> PricingPassOutcome {
let deadline = Instant::now() + self.computation.pass_budget;
let mut prices = FxHashMap::default();
let mut failed_items = Vec::new();
let mut unattempted = FxHashSet::default();
let mut unreachable_tokens = 0usize;
let mut remaining = tokens_to_price.into_iter();
for token in &mut remaining {
if Instant::now() >= deadline {
unattempted.insert(token);
break;
}
let Some(buy_leg) = self.buys.reached.remove(&token) else {
if self.buys.timed_out {
unattempted.insert(token);
} else {
unreachable_tokens += 1;
}
continue;
};
match self.price_token(&token, &buy_leg) {
Ok(priced) => {
prices.insert(token, priced);
}
Err(error) => failed_items.push(FailedItem { key: token.to_string(), error }),
}
}
unattempted.extend(remaining);
if unattempted.is_empty() {
debug!(
priced = prices.len(),
failed = failed_items.len(),
unreachable = unreachable_tokens,
block,
"token pricing pass complete"
);
} else {
warn!(
priced = prices.len(),
failed = failed_items.len(),
unreachable = unreachable_tokens,
unattempted = unattempted.len(),
buy_pass_timed_out = self.buys.timed_out,
block,
"token pricing pass cut short; unattempted tokens keep previous prices"
);
}
PricingPassOutcome { prices, block, failed_items, unattempted }
}
fn price_token(
&mut self,
token: &Address,
buy_leg: &ReachedToken,
) -> Result<TokenPriceEntry, FailedItemError> {
let SellLeg { amount_out: sell_out, mut components } =
self.solve_sell_leg(token, buy_leg.amount_out.clone())?;
trace!(%token, buy_out = %buy_leg.amount_out, sell_out = %sell_out, "token priced");
components.extend(buy_leg.components.iter().cloned());
let mid_price = Price {
numerator: &buy_leg.amount_out * (&self.computation.probe_amount + &sell_out),
denominator: BigUint::from(2u8) * &self.computation.probe_amount * sell_out,
};
Ok(TokenPriceEntry { price: mid_price, path_components: components })
}
fn solve_sell_leg(
&mut self,
token: &Address,
amount: BigUint,
) -> Result<SellLeg, FailedItemError> {
let token_node = *self
.token_nodes
.get(token)
.ok_or_else(|| {
FailedItemError::MissingSellRoute("token is not in the pass subgraph".into())
})?;
let candidate_components = self
.ctx
.reroot_toward(
self.graph,
token_node,
self.gas_node,
&self.hops_to_gas,
self.algorithm.max_hops(),
)
.ok_or_else(|| {
FailedItemError::MissingSellRoute("no pruned subgraph toward the gas token".into())
})?;
let mut components: FxHashSet<ComponentId> = candidate_components
.into_iter()
.cloned()
.collect();
let order = Order::new(
token.clone(),
self.computation.gas_token.clone(),
amount,
OrderSide::Sell,
Address::zero(20),
);
let result = self
.algorithm
.find_single_route(&self.ctx, &order, FindRouteOptions::default())
.map_err(|error| FailedItemError::MissingSellRoute(error.to_string()))?;
let route = result.route();
let amount_out = route.amount_out(&self.computation.gas_token);
if amount_out.is_zero() {
return Err(FailedItemError::MissingSellRoute("the sell route returns zero".into()));
}
components.extend(
route
.swaps()
.iter()
.map(|swap| swap.component_id().to_string()),
);
Ok(SellLeg { amount_out, components })
}
}
struct PricingPassOutcome {
prices: FxHashMap<Address, TokenPriceEntry>,
block: u64,
failed_items: Vec<FailedItem>,
unattempted: FxHashSet<Address>,
}
#[derive(Debug, Clone)]
pub struct TokenGasPriceComputation {
gas_token: Address,
max_hops: usize,
probe_amount: BigUint,
pass_budget: Duration,
}
impl Default for TokenGasPriceComputation {
fn default() -> Self {
Self {
gas_token: Address::zero(20), max_hops: crate::solver::defaults::POOL_MAX_HOPS,
probe_amount: BigUint::from(10u64).pow(18), pass_budget: Duration::from_secs(30),
}
}
}
impl TokenGasPriceComputation {
#[cfg(test)]
pub fn new(gas_token: Address, max_hops: usize, probe_amount: BigUint) -> Self {
Self { gas_token, max_hops, probe_amount, ..Self::default() }
}
pub fn with_pass_budget(self, pass_budget: Duration) -> Self {
Self { pass_budget, ..self }
}
pub fn with_max_hops(self, max_hops: usize) -> Self {
Self { max_hops, ..self }
}
pub fn with_gas_token(self, gas_token: Address) -> Self {
Self { gas_token, ..self }
}
async fn solve_token_prices(
&self,
market: &MarketData,
filter_tokens: Option<&FxHashSet<Address>>,
) -> Result<PricingPassOutcome, ComputationError> {
let (topology, block) = {
let guard = market.read().await;
let block = guard
.last_updated()
.map(|b| b.number())
.unwrap_or(0);
(guard.component_topology(), block)
};
let mut graph_manager = PetgraphStableDiGraphManager::new();
graph_manager.initialize_graph(&topology);
let config = AlgorithmConfig::new(1, self.max_hops, Duration::from_secs(1), None)
.map_err(|error| ComputationError::InvalidConfiguration(error.to_string()))?
.with_gas_aware(false);
let algorithm = BellmanFordAlgorithm::with_config(config);
let tokens_to_price = self.tokens_to_price(&topology, filter_tokens);
let graph = graph_manager.graph();
let Some(ctx) = algorithm
.build_context_from_source_token(
graph,
market.clone(),
&self.gas_token,
self.max_hops + 1,
filter_tokens,
)
.await
else {
warn!(unattempted = tokens_to_price.len(), "no subgraph around the gas token");
return Ok(PricingPassOutcome {
prices: FxHashMap::default(),
block,
failed_items: Vec::new(),
unattempted: tokens_to_price,
});
};
let block = ctx
.market_data
.last_updated()
.map_or(block, |b| b.number());
let computation = self.clone();
let span = Span::current();
tokio::task::spawn_blocking(move || {
let _entered = span.enter();
let mut pass = PricingPass::new(&algorithm, graph_manager.graph(), ctx, &computation);
pass.sell_loop(tokens_to_price, block)
})
.await
.map_err(|join_error| {
ComputationError::Internal(format!("token pricing pass did not complete: {join_error}"))
})
}
fn tokens_to_price(
&self,
topology: &FxHashMap<ComponentId, Vec<Address>>,
filter_tokens: Option<&FxHashSet<Address>>,
) -> FxHashSet<Address> {
topology
.values()
.flatten()
.filter(|token| *token != &self.gas_token)
.filter(|token| filter_tokens.is_none_or(|filter| filter.contains(*token)))
.cloned()
.collect()
}
async fn try_incremental_compute(
&self,
market: &MarketData,
store: &SharedDerivedDataRef,
changed: &ChangedComponents,
) -> Result<Option<ComputationOutput<TokenGasPrices>>, ComputationError> {
let (existing_deps, existing_prices) = {
let store_guard = store.read().await;
let Some(existing_deps) = store_guard.token_prices_deps().cloned() else {
return Ok(None);
};
let Some(existing_prices) = store_guard.token_prices().cloned() else {
return Ok(None);
};
(existing_deps, existing_prices)
};
let changed_components = changed.all_changed_ids();
let tokens_to_recompute: FxHashSet<Address> = existing_deps
.iter()
.filter(|(_, entry)| {
!entry
.path_components
.is_disjoint(&changed_components)
})
.map(|(addr, _)| addr.clone())
.collect();
if tokens_to_recompute.is_empty() {
return Ok(Some(ComputationOutput::success(existing_prices)));
}
debug!(
affected_tokens = tokens_to_recompute.len(),
total_tokens = existing_prices.len(),
"incremental token price recomputation"
);
let solved = self
.solve_token_prices(market, Some(&tokens_to_recompute))
.await?;
let mut result = existing_prices;
let mut new_deps = existing_deps;
for token in &tokens_to_recompute {
if let Some(entry) = solved.prices.get(token) {
result.insert(token.clone(), entry.price.clone());
new_deps.insert(token.clone(), entry.clone());
} else if !solved.unattempted.contains(token) {
result.remove(token);
new_deps.remove(token);
}
}
store
.write()
.await
.set_token_prices_deps(new_deps, solved.block);
Span::current().record("updated_token_prices", result.len());
Ok(Some(ComputationOutput::with_failures(result, solved.failed_items)))
}
}
#[async_trait]
impl DerivedComputation for TokenGasPriceComputation {
type Output = TokenGasPrices;
const ID: ComputationId = "token_prices";
fn requirements(&self) -> ComputationRequirements {
ComputationRequirements::none()
}
fn persist(
store: &mut DerivedData,
output: ComputationOutput<Self::Output>,
block: u64,
is_full_recompute: bool,
) {
store.set_token_prices(output.data, output.failed_items, block, is_full_recompute);
}
#[instrument(level = "debug", skip(market, store, changed), fields(computation_id = Self::ID, updated_token_prices))]
async fn compute(
&self,
market: &MarketData,
store: &SharedDerivedDataRef,
changed: &ChangedComponents,
) -> Result<ComputationOutput<Self::Output>, ComputationError> {
if !changed.is_full_recompute && !changed.is_topology_change() {
if let Some(result) = self
.try_incremental_compute(market, store, changed)
.await?
{
return Ok(result);
}
}
let solved = self
.solve_token_prices(market, None)
.await?;
let mut token_prices_with_deps = TokenPricesWithDeps::default();
let mut token_prices = TokenGasPrices::default();
for (token, entry) in solved.prices {
token_prices.insert(token.clone(), entry.price.clone());
token_prices_with_deps.insert(token, entry);
}
if !solved.unattempted.is_empty() {
let store_guard = store.read().await;
if let Some(previous) = store_guard.token_prices_deps() {
for token in &solved.unattempted {
let Some(entry) = previous.get(token) else {
continue;
};
token_prices_with_deps.insert(token.clone(), entry.clone());
token_prices.insert(token.clone(), entry.price.clone());
}
}
}
let gas_token_price =
Price { numerator: self.probe_amount.clone(), denominator: self.probe_amount.clone() };
token_prices_with_deps.insert(
self.gas_token.clone(),
TokenPriceEntry {
price: gas_token_price.clone(),
path_components: FxHashSet::default(),
},
);
token_prices.insert(self.gas_token.clone(), gas_token_price);
store
.write()
.await
.set_token_prices_deps(token_prices_with_deps, solved.block);
debug!(priced = token_prices.len() - 1, "token price computation complete");
Span::current().record("updated_token_prices", token_prices.len());
Ok(ComputationOutput::with_failures(token_prices, solved.failed_items))
}
}
#[cfg(test)]
mod tests {
use num_traits::ToPrimitive;
use tycho_simulation::tycho_core::models::token::Token;
use super::*;
use crate::{
algorithm::test_utils::{setup_market_weighted, token, MockProtocolSim},
derived::store::DerivedData,
};
const PROBE_AMOUNT: u128 = 1_000_000_000_000_000_000;
fn computation_for(gas_token: &Address) -> TokenGasPriceComputation {
TokenGasPriceComputation::new(gas_token.clone(), 3, BigUint::from(PROBE_AMOUNT))
}
fn ratio(price: &Price) -> f64 {
let numerator = price
.numerator
.to_f64()
.expect("price numerator fits in f64");
let denominator = price
.denominator
.to_f64()
.expect("price denominator fits in f64");
numerator / denominator
}
async fn prices_for(
gas_token: &Token,
pools: Vec<(&str, &Token, &Token, MockProtocolSim)>,
) -> TokenGasPrices {
let (market, _) = setup_market_weighted(pools);
let store = DerivedData::new_shared();
computation_for(&gas_token.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail")
.data
}
#[tokio::test]
async fn test_price_via_direct_pool() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices =
prices_for(ð, vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]).await;
assert!((ratio(&prices[&usdc.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_gas_token_price() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices =
prices_for(ð, vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]).await;
let eth_price = prices
.get(ð.address)
.expect("gas token should be priced");
assert_eq!(eth_price.numerator, BigUint::from(PROBE_AMOUNT));
assert_eq!(eth_price.denominator, BigUint::from(PROBE_AMOUNT));
}
#[tokio::test]
async fn test_price_with_pool_fee() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices = prices_for(
ð,
vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0).with_fee(0.01))],
)
.await;
let expected_mean = (1980.0 + 2000.0 / 0.99) / 2.0;
assert!((ratio(&prices[&usdc.address]) - expected_mean).abs() < 1e-6);
}
#[tokio::test]
async fn test_parallel_pools_price_via_best_output() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let prices = prices_for(
ð,
vec![
("tight", ð, &usdc, MockProtocolSim::new(2000.0)),
("wide", ð, &usdc, MockProtocolSim::new(2500.0).with_fee(0.01)),
],
)
.await;
assert!((ratio(&prices[&usdc.address]) - 2237.5).abs() < 1e-6);
}
#[tokio::test]
async fn test_price_via_multi_hop_route() {
let eth = token(0, "ETH");
let mid = token(2, "MID");
let target = token(3, "TARGET");
let prices = prices_for(
ð,
vec![
("eth_mid", ð, &mid, MockProtocolSim::new(2.0)),
("mid_target", &mid, &target, MockProtocolSim::new(3.0)),
],
)
.await;
assert!((ratio(&prices[&target.address]) - 6.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_price_at_exactly_max_hops() {
let eth = token(0, "ETH");
let mid = token(2, "MID");
let next = token(3, "NEXT");
let far = token(4, "FAR");
let prices = prices_for(
ð,
vec![
("eth_mid", ð, &mid, MockProtocolSim::new(2.0)),
("mid_next", &mid, &next, MockProtocolSim::new(2.0)),
("next_far", &next, &far, MockProtocolSim::new(2.0)),
],
)
.await;
assert!((ratio(&prices[&far.address]) - 8.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_full_solve_past_deadline() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
let output = computation_for(ð.address)
.with_pass_budget(Duration::ZERO)
.compute(
&market,
&store,
&ChangedComponents { is_full_recompute: true, ..ChangedComponents::default() },
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
assert!(output.failed_items.is_empty(), "an unattempted token is not a failure");
let guard = store.read().await;
assert!(
guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&usdc.address),
"carried tokens must stay visible to incremental invalidation"
);
}
#[tokio::test]
async fn test_vanished_gas_subgraph() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let aaa = token(2, "AAA");
let (market, _) = setup_market_weighted(vec![
("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0)),
("usdc_aaa", &usdc, &aaa, MockProtocolSim::new(1.0)),
]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
market
.write()
.await
.remove_components(["eth_usdc".to_string()].iter());
let output = computation_for(ð.address)
.compute(
&market,
&store,
&ChangedComponents { is_full_recompute: true, ..ChangedComponents::default() },
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
assert!((ratio(&output.data[&aaa.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_incremental_solve_past_deadline() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let store = DerivedData::new_shared();
let full = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
let output = computation_for(ð.address)
.with_pass_budget(Duration::ZERO)
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_usdc".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
let guard = store.read().await;
assert!(
guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&usdc.address),
"a carried token must stay visible to incremental invalidation"
);
}
#[tokio::test]
async fn test_incremental_resolves_only_affected_tokens() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) = setup_market_weighted(vec![
("eth_aaa", ð, &aaa, MockProtocolSim::new(2000.0)),
("eth_bbb", ð, &bbb, MockProtocolSim::new(2500.0)),
]);
let computation =
TokenGasPriceComputation::new(eth.address.clone(), 1, BigUint::from(PROBE_AMOUNT));
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([
("eth_aaa".to_string(), Box::new(MockProtocolSim::new(4000.0)) as Box<dyn ProtocolSim>),
("eth_bbb".to_string(), Box::new(MockProtocolSim::new(5000.0)) as Box<dyn ProtocolSim>),
]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_aaa".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&aaa.address]) - 4000.0).abs() < 1e-6, "AAA re-solved");
assert!((ratio(&output.data[&bbb.address]) - 2500.0).abs() < 1e-6, "BBB untouched");
}
#[tokio::test]
async fn test_incremental_with_disjoint_change_keeps_all_prices() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let (market, _) =
setup_market_weighted(vec![("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0))]);
let computation = computation_for(ð.address);
let store = DerivedData::new_shared();
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([(
"eth_usdc".to_string(),
Box::new(MockProtocolSim::new(9000.0)) as Box<dyn ProtocolSim>,
)]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["unrelated_pool".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!((ratio(&output.data[&usdc.address]) - 2000.0).abs() < 1e-6);
}
#[tokio::test]
async fn test_token_without_sell_route_is_a_failed_item() {
let eth = token(0, "ETH");
let oneway = token(1, "ONEWAY");
let (market, _) = setup_market_weighted(vec![(
"eth_oneway",
ð,
&oneway,
MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000),
)]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert!(
!output
.data
.contains_key(&oneway.address),
"an unsellable token has no price"
);
assert_eq!(output.failed_items.len(), 1);
assert_eq!(output.failed_items[0].key, oneway.address.to_string());
let FailedItemError::MissingSellRoute(reason) = &output.failed_items[0].error else {
panic!("expected MissingSellRoute, got {:?}", output.failed_items[0].error);
};
assert!(!reason.is_empty(), "the failure carries why the sell solve failed");
}
#[tokio::test]
async fn test_incremental_removes_unsellable_token() {
use tycho_simulation::tycho_common::simulation::protocol_sim::ProtocolSim;
let eth = token(0, "ETH");
let oneway = token(1, "ONEWAY");
let (market, _) =
setup_market_weighted(vec![("eth_oneway", ð, &oneway, MockProtocolSim::new(0.5))]);
let store = DerivedData::new_shared();
let computation = computation_for(ð.address);
let full = computation
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
TokenGasPriceComputation::persist(&mut *store.write().await, full, 1, true);
market.write().await.update_states([(
"eth_oneway".to_string(),
Box::new(MockProtocolSim::new(0.5).with_liquidity(600_000_000_000_000_000))
as Box<dyn ProtocolSim>,
)]);
let output = computation
.compute(
&market,
&store,
&ChangedComponents {
updated: vec!["eth_oneway".to_string()],
..ChangedComponents::default()
},
)
.await
.expect("pricing must not fail");
assert!(
!output
.data
.contains_key(&oneway.address),
"an unsellable token has no price"
);
let guard = store.read().await;
assert!(
!guard
.token_prices_deps()
.expect("deps are stored")
.contains_key(&oneway.address),
"a dropped token must leave the dependency map too"
);
}
#[tokio::test]
async fn test_deps_cover_rival_routes() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let mid = token(2, "MID");
let (market, _) = setup_market_weighted(vec![
("direct", ð, &usdc, MockProtocolSim::new(2000.0)),
("eth_mid", ð, &mid, MockProtocolSim::new(1.0)),
("mid_usdc", &mid, &usdc, MockProtocolSim::new(1500.0)),
]);
let store = DerivedData::new_shared();
computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
let guard = store.read().await;
let deps = &guard
.token_prices_deps()
.expect("deps are stored")[&usdc.address]
.path_components;
for component in ["direct", "eth_mid", "mid_usdc"] {
assert!(deps.contains(component), "{component} must invalidate USDC's price");
}
}
#[tokio::test]
async fn test_empty_market() {
let eth = token(0, "ETH");
let prices = prices_for(ð, vec![]).await;
assert_eq!(prices.len(), 1);
assert!((ratio(&prices[ð.address]) - 1.0).abs() < 1e-9);
}
#[tokio::test]
async fn test_gas_token_outside_graph() {
let eth = token(0, "ETH");
let aaa = token(1, "AAA");
let bbb = token(2, "BBB");
let (market, _) =
setup_market_weighted(vec![("aaa_bbb", &aaa, &bbb, MockProtocolSim::new(1.0))]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert_eq!(output.data.len(), 1);
assert!((ratio(&output.data[ð.address]) - 1.0).abs() < 1e-9);
assert!(output.failed_items.is_empty(), "unreachable tokens are not failures");
}
#[tokio::test]
async fn test_unreachable_token() {
let eth = token(0, "ETH");
let usdc = token(1, "USDC");
let island = token(4, "ISLAND");
let other = token(5, "OTHER");
let (market, _) = setup_market_weighted(vec![
("eth_usdc", ð, &usdc, MockProtocolSim::new(2000.0)),
("island_other", &island, &other, MockProtocolSim::new(1.0)),
]);
let store = DerivedData::new_shared();
let output = computation_for(ð.address)
.compute(&market, &store, &ChangedComponents::default())
.await
.expect("pricing must not fail");
assert!(output.data.contains_key(&usdc.address));
assert!(
!output
.data
.contains_key(&island.address),
"an unreachable token has no price"
);
assert!(
output.failed_items.is_empty(),
"unreachable tokens are counted, not reported as failed items"
);
}
}