1mod config;
32mod models;
33
34use std::{
35 cmp::{Ordering, Reverse},
36 time::{Duration, Instant},
37};
38
39use models::{
40 CandidatePathState, CandidateSearchConfig, Deadline, Discovery, ExchangeMove,
41 FullAmountOutcome, FullAmountRanking, ScoredEdge, SetupResult, SolveInput, SolveStage,
42 SplitCandidate, StepResult,
43};
44use num_bigint::{BigInt, BigUint};
45use num_traits::Zero;
46use petgraph::{graph::NodeIndex, prelude::EdgeRef};
47use rustc_hash::{FxHashMap, FxHashSet};
48use tracing::{debug, instrument, trace};
49use tycho_simulation::tycho_common::{models::Address, simulation::protocol_sim::ProtocolSim};
50
51use super::{
52 most_liquid::DepthAndPrice,
53 paths,
54 sim_meter::{self, MeteredProtocolSim},
55 split_primitives::{
56 build_split_route, HopDescriptor, MarketOverrides, PathAllocation, SimulatedHop,
57 },
58 Algorithm, AlgorithmConfig, NoPathReason,
59};
60use crate::{
61 algorithm::{
62 paths::read_market,
63 swap_cache::{PoolDirection, Refusal, SwapCache, SwapResult},
64 water_fill::config::{
65 BASELINE_CANDIDATES, CANDIDATE_CONNECTOR_EDGES_PER_TOKEN,
66 CANDIDATE_DIRECT_EDGES_PER_TOKEN, CANDIDATE_EDGES_PER_STATE, CANDIDATE_STATES_PER_NODE,
67 COARSE_CHUNKS, DEFAULT_MAX_CANDIDATES, DEFAULT_MAX_PATHS, DERIVED_ANCHOR_COUNT,
68 EXCHANGE_DELTA_FLOOR, EXCHANGE_MAX_SIMS, FINE_CHUNKS, MAX_DISCOVERY_CANDIDATES,
69 SHARED_FULL_PATHS, SHARED_MARGIN_PATHS, SHARED_MARGIN_PROBE_PATHS,
70 SHARED_MAX_CANDIDATES,
71 },
72 },
73 derived::{computation::ComputationRequirements, types::TokenGasPrices, SharedDerivedDataRef},
74 feed::market_data::{MarketData, MarketDataView, MarketState, StateLabel},
75 graph::{EdgeData, GraphQueryFilter, Path, TopologyGraph, TopologyGraphManager},
76 types::{ComponentId, Order, RouteResult},
77 AlgorithmError,
78};
79
80pub struct WaterFillAlgorithm {
83 query: GraphQueryFilter,
85 timeout: Duration,
86 max_candidates: usize,
87 max_paths: usize,
88}
89
90impl WaterFillAlgorithm {
91 pub(crate) fn with_config(config: AlgorithmConfig) -> Result<Self, AlgorithmError> {
93 Ok(Self {
94 query: GraphQueryFilter {
95 min_hops: config.min_hops(),
96 max_hops: config.max_hops(),
97 connector_tokens: config.connector_tokens().cloned(),
98 },
99 timeout: config.timeout(),
100 max_candidates: config
101 .max_routes()
102 .unwrap_or(DEFAULT_MAX_CANDIDATES)
103 .max(DEFAULT_MAX_PATHS),
104 max_paths: DEFAULT_MAX_PATHS,
105 })
106 }
107
108 fn simulate_step<'g>(
112 path: &Path<'g, DepthAndPrice>,
113 market: &MarketState,
114 overlay: &MarketOverrides,
115 amount: BigUint,
116 ) -> Option<StepResult> {
117 let mut current = amount;
118 let mut total_gas = BigUint::zero();
119 let path_reuses_component = path_reuses_component(path);
124 let mut intra_path_states: FxHashMap<ComponentId, Box<dyn ProtocolSim>> =
125 FxHashMap::default();
126 let mut new_states: Vec<(ComponentId, Box<dyn ProtocolSim>)> =
127 Vec::with_capacity(path.len());
128
129 for (address_in, edge, address_out) in path.iter() {
130 let token_in = market.get_token(address_in)?;
131 let token_out = market.get_token(address_out)?;
132 let component_id = &edge.component_id;
133 let state = hop_state(market, component_id, Some(&intra_path_states), Some(overlay))?;
134 let result = state
135 .get_amount_out_metered(
136 component_id,
137 SolveStage::Chunking.label(),
138 current.clone(),
139 token_in,
140 token_out,
141 )
142 .ok()?;
143 total_gas += &result.gas;
144 if path_reuses_component {
145 intra_path_states.insert(component_id.clone(), result.new_state.clone_box());
146 }
147 new_states.push((component_id.clone(), result.new_state));
148 current = result.amount;
149 }
150 Some(StepResult { amount_out: current, gas: total_gas, new_states })
151 }
152
153 fn gas_cost_in_token(
155 total_gas: &BigUint,
156 gas_price_wei: &BigUint,
157 token_prices: Option<&TokenGasPrices>,
158 token_out: &Address,
159 ) -> Option<BigUint> {
160 let price = token_prices?.get(token_out)?;
161 if price.denominator.is_zero() {
162 return None;
163 }
164 Some(total_gas * gas_price_wei * &price.numerator / &price.denominator)
165 }
166
167 fn activation_cost(input: &SolveInput, gas: &BigUint) -> BigInt {
171 Self::gas_cost_in_token(
172 gas,
173 &input.gas_price,
174 input.token_prices.as_ref(),
175 input.order.token_out(),
176 )
177 .map(BigInt::from)
178 .unwrap_or_else(BigInt::zero)
179 }
180
181 fn select_disjoint(ranked: &[Path<DepthAndPrice>], max_paths: usize) -> Vec<usize> {
188 let mut visited_components: FxHashSet<&ComponentId> = FxHashSet::default();
189 let mut selected = Vec::new();
190 for (idx, path) in ranked.iter().enumerate() {
191 let path_components: Vec<&ComponentId> = path
192 .edge_iter()
193 .iter()
194 .map(|e| &e.component_id)
195 .collect();
196 if path_components
197 .iter()
198 .any(|c| visited_components.contains(*c))
199 {
200 continue;
201 }
202 for c in path_components {
203 visited_components.insert(c);
204 }
205 selected.push(idx);
206 if selected.len() >= max_paths {
207 break;
208 }
209 }
210 selected
211 }
212
213 #[instrument(level = "debug", skip_all)]
216 async fn setup<'o, 'g>(
217 &self,
218 graph: &'g TopologyGraph<DepthAndPrice>,
219 market: MarketData,
220 label: Option<StateLabel>,
221 derived: Option<SharedDerivedDataRef>,
222 order: &'o Order,
223 deadline: Deadline,
224 ) -> Result<SetupResult<'o, 'g>, AlgorithmError> {
225 let token_prices = if let Some(ref derived) = derived {
226 derived
227 .read()
228 .await
229 .token_prices()
230 .cloned()
231 } else {
232 None
233 };
234
235 let mut scored_paths = self.top_scored_paths(graph, order)?;
236 let mut joined_paths = Vec::new();
237
238 let market_view = read_market(&market, label).await?;
239
240 let anchor_tokens = derive_anchor_tokens(graph);
245
246 let mut cache = SwapCache::new();
250 sim_meter::start_solve();
251
252 let discovered_paths = discover_paths(
253 graph,
254 &market_view,
255 order,
256 &mut cache,
257 CandidateSearchConfig {
258 query: &self.query,
259 max_candidates: MAX_DISCOVERY_CANDIDATES,
260 anchor_tokens: &anchor_tokens,
261 source_token: order.token_in(),
262 deadline,
263 },
264 )
265 .inspect_err(|e| {
266 debug!(error = %e, "water-fill bounded discovery failed; using exhaustive candidates only")
267 })
268 .unwrap_or_default();
269
270 let mut keys: FxHashSet<Vec<ComponentId>> = scored_paths
271 .iter()
272 .map(path_key)
273 .collect();
274 for path in discovered_paths {
275 if keys.insert(path_key(&path)) {
276 joined_paths.push(path);
277 }
278 }
279 joined_paths.append(&mut scored_paths);
280
281 let component_ids: FxHashSet<&ComponentId> = joined_paths
282 .iter()
283 .flat_map(|p| {
284 p.edge_iter()
285 .iter()
286 .map(|e| &e.component_id)
287 })
288 .collect();
289 let market_state = market_view.extract_subset_with_overlay(&component_ids);
290 let gas_price = paths::fetch_gas_price(&market_state)?;
291 drop(market_view);
292
293 let amount_in = order.amount().clone();
294 let mut input = SolveInput {
296 ordered: joined_paths,
297 market: market_state,
298 gas_price,
299 token_prices,
300 order,
301 deadline,
302 };
303 let ranking = self.rank_at_full_amount(&input, &mut cache);
304
305 let build_baseline = |path_ix: usize| {
312 paths::simulate_pool_path(
313 &input.ordered[path_ix],
314 &input.market,
315 input.token_prices.as_ref(),
316 amount_in.clone(),
317 )
318 .ok()
319 };
320 let best_single = ranking
321 .by_output_net_gas
322 .iter()
323 .take(BASELINE_CANDIDATES)
324 .filter_map(|&path_ix| build_baseline(path_ix))
325 .max_by(|a, b| {
326 a.net_amount_out()
327 .cmp(b.net_amount_out())
328 })
329 .or_else(|| {
330 ranking
332 .by_output_net_gas
333 .iter()
334 .skip(BASELINE_CANDIDATES)
335 .find_map(|&path_ix| build_baseline(path_ix))
336 });
337
338 input.ordered = ranking
342 .by_output
343 .iter()
344 .map(|&path_ix| input.ordered[path_ix].clone())
345 .collect();
346
347 debug!(
348 candidate_paths = input.ordered.len(),
349 elapsed_ms = deadline.elapsed().as_millis(),
350 "water-fill discovery + full-amount ranking"
351 );
352 Ok(SetupResult { input, best_single, cache })
353 }
354
355 fn rank_at_full_amount<'g>(
367 &self,
368 input: &SolveInput<'_, 'g>,
369 cache: &mut SwapCache<'g>,
370 ) -> FullAmountRanking {
371 let paths = &input.ordered;
372 let order_amount = input.order.amount();
373 let mut outcomes_by_path: Vec<Option<FullAmountOutcome>> = vec![None; paths.len()];
374
375 for (path_ix, path) in paths.iter().enumerate() {
376 if input.deadline.expired() {
377 break;
378 }
379 if path_reuses_component(path) {
380 continue;
381 }
382 outcomes_by_path[path_ix] = Some(
383 match simulate_path(
384 path,
385 &input.market,
386 cache,
387 order_amount.clone(),
388 SolveStage::Ranking,
389 ) {
390 Some(paid) => FullAmountOutcome::Filled(paid),
391 None => FullAmountOutcome::Unfilled,
392 },
393 );
394 }
395
396 rank_outcomes(
397 outcomes_by_path,
398 &input.gas_price,
399 input.token_prices.as_ref(),
400 input.order.token_out(),
401 )
402 }
403
404 fn top_scored_paths<'a>(
407 &self,
408 graph: &'a TopologyGraph<DepthAndPrice>,
409 order: &Order,
410 ) -> Result<Vec<Path<'a, DepthAndPrice>>, AlgorithmError> {
411 let all_paths =
412 paths::find_paths(graph, order.token_in(), order.token_out(), &self.query, None)?;
413 if all_paths.is_empty() {
414 return Err(AlgorithmError::NoPath {
415 from: order.token_in().clone(),
416 to: order.token_out().clone(),
417 reason: NoPathReason::NoGraphPath,
418 });
419 }
420
421 let path_count = all_paths.len();
422 let mut scored_count = 0usize;
423
424 let mut scored: Vec<(Path<DepthAndPrice>, f64)> = all_paths
428 .into_iter()
429 .map(|path| {
430 let score = match paths::try_score_path(&path) {
431 Some(score) => {
432 scored_count += 1;
433 score
434 }
435 None => f64::MIN,
436 };
437 (path, score)
438 })
439 .collect();
440 scored.sort_by(|(_, a), (_, b)| {
441 b.partial_cmp(a)
442 .unwrap_or(std::cmp::Ordering::Equal)
443 });
444
445 trace!(path_count, scored_count, limit = self.max_candidates, "water-fill path scoring");
446
447 scored.truncate(self.max_candidates);
448
449 Ok(scored
450 .into_iter()
451 .map(|(p, _)| p)
452 .collect())
453 }
454}
455
456impl Algorithm for WaterFillAlgorithm {
457 type GraphType = TopologyGraph<DepthAndPrice>;
458 type GraphManager = TopologyGraphManager<DepthAndPrice>;
459
460 fn name(&self) -> &str {
461 "water_fill"
462 }
463
464 #[instrument(level = "debug", skip_all, fields(order_id = %order.id()))]
465 async fn find_best_route(
466 &self,
467 graph: &Self::GraphType,
468 market: MarketData,
469 label: Option<StateLabel>,
470 derived: Option<SharedDerivedDataRef>,
471 order: &Order,
472 ) -> Result<RouteResult, AlgorithmError> {
473 let deadline = Deadline::new(Instant::now(), self.timeout);
474 if !order.is_sell() {
475 return Err(AlgorithmError::ExactOutNotSupported);
476 }
477
478 let SetupResult { input, best_single, mut cache } = self
479 .setup(graph, market, label, derived, order, deadline)
480 .await?;
481
482 let mut candidates: Vec<SplitCandidate> = Vec::new();
484 let disjoint = Self::select_disjoint(&input.ordered, self.max_paths);
489 let coarse = (disjoint.len() >= 2)
490 .then(|| self.disjoint_waterfill(&input, &disjoint, COARSE_CHUNKS, true))
491 .flatten();
492 if let Some(coarse) = coarse.as_deref() {
493 if let Some(c) = self.build_disjoint_legs(&input, &disjoint, coarse) {
495 candidates.push(c);
496 }
497 if let Some(c) =
500 self.disjoint_refine(&input, &disjoint, coarse, FINE_CHUNKS, &mut cache)
501 {
502 candidates.push(c);
503 }
504 }
505 if let Some(c) = self.fillspill_alloc(&input, FINE_CHUNKS, &mut cache) {
508 candidates.push(c);
509 }
510
511 let candidate_count = candidates.len();
514 let baseline_net = best_single
515 .as_ref()
516 .map(|b| b.net_amount_out().clone());
517 let mut best: Option<(BigInt, SplitCandidate)> = None;
518 for cand in candidates {
519 let net = cand.net(&input);
520 let beats = match (&best, &baseline_net) {
521 (Some((current, _)), _) => net > *current,
522 (None, Some(base)) => net > *base,
523 (None, None) => true,
524 };
525 if beats {
526 best = Some((net, cand));
527 }
528 }
529 let split_won = best.is_some();
530 sim_meter::report(&input.market, || deadline.elapsed().as_millis() as u64);
531 debug!(
532 candidate_count,
533 split_won,
534 elapsed_ms = deadline.elapsed().as_millis(),
535 "water-fill selected {}",
536 if split_won { "split candidate" } else { "single path" }
537 );
538 match best {
539 Some((net, cand)) => Ok(RouteResult::new(cand.route, net, input.gas_price.clone())),
540 None => best_single.ok_or(AlgorithmError::InsufficientLiquidity),
542 }
543 }
544
545 fn computation_requirements(&self) -> ComputationRequirements {
546 ComputationRequirements::none()
547 .allow_stale("token_prices")
548 .expect("Conflicting Computation Requirements")
549 }
550
551 fn timeout(&self) -> Duration {
552 self.timeout
553 }
554}
555
556impl WaterFillAlgorithm {
557 fn disjoint_refine<'g>(
563 &self,
564 input: &SolveInput<'_, 'g>,
565 disjoint: &[usize],
566 coarse: &[BigUint],
567 fine_chunks: usize,
568 cache: &mut SwapCache<'g>,
569 ) -> Option<SplitCandidate> {
570 let active: Vec<usize> = disjoint
572 .iter()
573 .copied()
574 .zip(coarse.iter())
575 .filter(|(_, amt)| !amt.is_zero())
576 .map(|(idx, _)| idx)
577 .collect();
578 if active.is_empty() {
579 return None;
580 }
581
582 let fine = self.disjoint_waterfill(input, &active, fine_chunks, false)?;
584
585 let refined = self.disjoint_exchange(input, &active, fine_chunks, fine, cache);
589 self.build_disjoint_legs(input, &active, &refined)
590 }
591
592 fn path_net<'g>(
598 input: &SolveInput<'_, 'g>,
599 path: &Path<'g, DepthAndPrice>,
600 amount: &BigUint,
601 cache: &mut SwapCache<'g>,
602 ) -> Option<BigInt> {
603 if amount.is_zero() {
604 return Some(BigInt::zero());
605 }
606 let result =
607 simulate_path(path, &input.market, cache, amount.clone(), SolveStage::Exchange)?;
608 let activation = Self::activation_cost(input, &result.gas);
609 Some(BigInt::from(result.amount_out) - activation)
610 }
611
612 fn disjoint_exchange<'g>(
621 &self,
622 input: &SolveInput<'_, 'g>,
623 active: &[usize],
624 fine_chunks: usize,
625 alloc: Vec<BigUint>,
626 cache: &mut SwapCache<'g>,
627 ) -> Vec<BigUint> {
628 let path_count = active.len();
629 if path_count < 2 {
630 return alloc;
631 }
632 let amount_in = input.order.amount().clone();
633 let fine_chunks = fine_chunks.max(1);
634 let mut delta = &amount_in / fine_chunks;
635 let min_delta = &amount_in / (fine_chunks * EXCHANGE_DELTA_FLOOR);
636 if delta.is_zero() {
637 return alloc;
638 }
639
640 let mut cumulative_amount_in = alloc;
641 let mut net_cache: Vec<BigInt> = Vec::with_capacity(path_count);
644 for (i, &path_idx) in active.iter().enumerate() {
645 let Some(net) =
646 Self::path_net(input, &input.ordered[path_idx], &cumulative_amount_in[i], cache)
647 else {
648 return cumulative_amount_in;
650 };
651 net_cache.push(net);
652 }
653
654 let mut sims = 0usize;
655 while delta >= min_delta && !delta.is_zero() {
656 if input.deadline.expired() || sims >= EXCHANGE_MAX_SIMS {
657 break;
658 }
659
660 let mut best: Option<ExchangeMove> = None;
661 for donor in 0..path_count {
662 if sims >= EXCHANGE_MAX_SIMS {
663 break;
664 }
665 if cumulative_amount_in[donor] < delta {
666 continue;
667 }
668 let donor_amt = &cumulative_amount_in[donor] - δ
669 let Some(donor_net) =
670 Self::path_net(input, &input.ordered[active[donor]], &donor_amt, cache)
671 else {
672 continue;
673 };
674 sims += 1;
675 for recipient in 0..path_count {
676 if sims >= EXCHANGE_MAX_SIMS {
677 break;
678 }
679 if recipient == donor {
680 continue;
681 }
682 let recip_amt = &cumulative_amount_in[recipient] + δ
683 let Some(recip_net) =
684 Self::path_net(input, &input.ordered[active[recipient]], &recip_amt, cache)
685 else {
686 continue;
687 };
688 sims += 1;
689 let before = &net_cache[donor] + &net_cache[recipient];
690 let after = &donor_net + &recip_net;
691 if after <= before {
692 continue;
693 }
694 let gain = after - before;
695 if best
696 .as_ref()
697 .map(|m| gain > m.gain)
698 .unwrap_or(true)
699 {
700 best = Some(ExchangeMove {
701 donor,
702 recipient,
703 donor_net: donor_net.clone(),
704 recip_net,
705 gain,
706 });
707 }
708 }
709 }
710
711 let Some(mv) = best else {
712 delta = &delta / 2usize;
713 continue;
714 };
715 cumulative_amount_in[mv.donor] = &cumulative_amount_in[mv.donor] - δ
716 cumulative_amount_in[mv.recipient] = &cumulative_amount_in[mv.recipient] + δ
717 net_cache[mv.donor] = mv.donor_net;
718 net_cache[mv.recipient] = mv.recip_net;
719 }
720 cumulative_amount_in
721 }
722
723 fn allocation_commit<'g>(
726 path: &Path<'g, DepthAndPrice>,
727 market: &MarketState,
728 overrides: &mut MarketOverrides,
729 amount: BigUint,
730 flow_fraction: f64,
731 ) -> Option<PathAllocation> {
732 let amount_in = amount;
733 let mut current = amount_in.clone();
734 let mut hops = Vec::with_capacity(path.len());
735
736 for (address_in, edge, address_out) in path.iter() {
737 let token_in = market.get_token(address_in)?;
738 let token_out = market.get_token(address_out)?;
739 let component_id = &edge.component_id;
740 let state = hop_state(market, component_id, None, Some(overrides))?;
741 let result = state
742 .get_amount_out_metered(
743 component_id,
744 SolveStage::Assembly.label(),
745 current.clone(),
746 token_in,
747 token_out,
748 )
749 .ok()?;
750 hops.push(SimulatedHop {
751 descriptor: HopDescriptor::new(
752 component_id.clone(),
753 token_in.clone(),
754 token_out.clone(),
755 ),
756 amount_out: result.amount.clone(),
757 gas: result.gas.clone(),
758 });
759 overrides.insert(component_id.clone(), result.new_state);
760 current = result.amount;
761 }
762
763 Some(PathAllocation {
764 hops,
765 flow_fraction,
766 amount_in,
767 amount_out: current,
768 marginal_price_product: 0.0,
769 })
770 }
771
772 fn candidate_from_allocations(
776 input: &SolveInput,
777 allocations: &[PathAllocation],
778 ) -> Option<SplitCandidate> {
779 let route = build_split_route(allocations, &input.market, input.order).ok()?;
780 let token_out = input.order.token_out();
781 let gross = route
782 .swaps()
783 .iter()
784 .filter(|s| s.token_out() == token_out)
785 .fold(BigUint::zero(), |acc, s| acc + s.amount_out());
786 if gross.is_zero() {
787 return None;
788 }
789 let gas = route.total_gas();
790 Some(SplitCandidate { route, gross, gas })
791 }
792
793 fn build_disjoint_legs<'g>(
797 &self,
798 input: &SolveInput<'_, 'g>,
799 subset: &[usize],
800 alloc: &[BigUint],
801 ) -> Option<SplitCandidate> {
802 let amount_in = input.order.amount().clone();
803 let mut allocations = Vec::new();
804 for (i, &path_idx) in subset.iter().enumerate() {
805 if alloc[i].is_zero() {
806 continue;
807 }
808 let mut overrides = MarketOverrides::empty();
811 let allocation = Self::allocation_commit(
812 &input.ordered[path_idx],
813 &input.market,
814 &mut overrides,
815 alloc[i].clone(),
816 ratio(&alloc[i], &amount_in),
817 )?;
818 allocations.push(allocation);
819 }
820 if allocations.is_empty() {
821 return None;
822 }
823 Self::candidate_from_allocations(input, &allocations)
824 }
825
826 fn disjoint_waterfill<'g>(
830 &self,
831 input: &SolveInput<'_, 'g>,
832 subset: &[usize],
833 num_chunks: usize,
834 gate: bool,
835 ) -> Option<Vec<BigUint>> {
836 let amount_in = input.order.amount().clone();
837 let num_chunks = num_chunks.max(1);
838 let base_chunk = &amount_in / num_chunks;
839 if base_chunk.is_zero() {
840 return None;
841 }
842 let remainder = &amount_in - &base_chunk * num_chunks;
843 let path_count = subset.len();
844
845 let mut committed: Vec<MarketOverrides> = (0..path_count)
846 .map(|_| MarketOverrides::empty())
847 .collect();
848 let mut cumulative_amount_in: Vec<BigUint> = vec![BigUint::zero(); path_count];
849 let mut activated: Vec<bool> = vec![!gate; path_count];
850
851 let mut marginals: Vec<Option<StepResult>> = (0..path_count).map(|_| None).collect();
856 let mut marginals_chunk: Option<BigUint> = None;
859
860 for chunk_idx in 0..num_chunks {
861 if input.deadline.expired() {
862 break;
863 }
864 let chunk = if chunk_idx == 0 { &base_chunk + &remainder } else { base_chunk.clone() };
865 if marginals_chunk.as_ref() != Some(&chunk) {
866 marginals
867 .iter_mut()
868 .for_each(|m| *m = None);
869 marginals_chunk = Some(chunk.clone());
870 }
871
872 let mut best: Option<(usize, BigInt)> = None;
873 for (i, &path_idx) in subset.iter().enumerate() {
874 if marginals[i].is_none() {
875 marginals[i] = Self::simulate_step(
876 &input.ordered[path_idx],
877 &input.market,
878 &committed[i],
879 chunk.clone(),
880 );
881 }
882 let Some(step) = marginals[i].as_ref() else {
883 continue;
884 };
885 let gross_marginal = BigInt::from(step.amount_out.clone());
886 let net_marginal = if activated[i] {
887 gross_marginal
888 } else {
889 let activation = Self::activation_cost(input, &step.gas);
890 gross_marginal - activation
891 };
892 if best
893 .as_ref()
894 .map(|(_, m)| &net_marginal > m)
895 .unwrap_or(true)
896 {
897 best = Some((i, net_marginal));
898 }
899 }
900
901 let Some((best_i, _)) = best else {
902 break;
903 };
904 let Some(step) = marginals[best_i].take() else {
907 break;
908 };
909
910 for (id, state) in step.new_states {
911 committed[best_i].insert(id, state);
912 }
913 cumulative_amount_in[best_i] += &chunk;
914 activated[best_i] = true;
915 }
916 Some(cumulative_amount_in)
917 }
918
919 fn select_shared_candidates<'g>(
923 &self,
924 input: &SolveInput<'_, 'g>,
925 cache: &mut SwapCache<'g>,
926 ) -> Vec<usize> {
927 let mut candidates: Vec<usize> = (0..input
928 .ordered
929 .len()
930 .min(SHARED_FULL_PATHS))
931 .collect();
932 let first_chunk = input.order.amount() / COARSE_CHUNKS;
933 if first_chunk.is_zero() {
934 return candidates;
935 }
936 let mut marginal: Vec<(usize, BigInt)> = Vec::new();
937 for (idx, path) in input
938 .ordered
939 .iter()
940 .enumerate()
941 .take(SHARED_MARGIN_PROBE_PATHS)
942 {
943 if input.deadline.expired() {
944 break;
945 }
946 let Some(probe) = simulate_path(
949 path,
950 &input.market,
951 cache,
952 first_chunk.clone(),
953 SolveStage::SetSelection,
954 ) else {
955 continue;
956 };
957 let activation = Self::activation_cost(input, &probe.gas);
958 marginal.push((idx, BigInt::from(probe.amount_out) - activation));
959 }
960 marginal.sort_by(|(_, a), (_, b)| b.cmp(a));
961 for (idx, net) in marginal
962 .into_iter()
963 .take(SHARED_MARGIN_PATHS)
964 {
965 if net <= BigInt::zero() {
966 continue;
967 }
968 if !candidates.contains(&idx) {
969 candidates.push(idx);
970 }
971 if candidates.len() >= SHARED_MAX_CANDIDATES {
972 break;
973 }
974 }
975 candidates
976 }
977
978 fn fillspill_alloc<'g>(
983 &self,
984 input: &SolveInput<'_, 'g>,
985 fine_chunks: usize,
986 cache: &mut SwapCache<'g>,
987 ) -> Option<SplitCandidate> {
988 let candidates = self.select_shared_candidates(input, cache);
989 if candidates.len() < 2 {
990 return None;
991 }
992
993 let coarse = self.fillspill_waterfill(input, &candidates, COARSE_CHUNKS, true)?;
996 let took_a_chunk: FxHashSet<usize> = coarse.iter().map(|(i, _)| *i).collect();
997 let active: Vec<usize> = candidates
998 .iter()
999 .copied()
1000 .enumerate()
1001 .filter(|(i, _)| took_a_chunk.contains(i))
1002 .map(|(_, idx)| idx)
1003 .collect();
1004 if active.len() < 2 {
1005 return None;
1006 }
1007
1008 let schedule = self.fillspill_waterfill(input, &active, fine_chunks, false)?;
1010 if schedule.is_empty() {
1011 return None;
1012 }
1013
1014 self.build_fillspill_route(input, &active, &schedule)
1015 }
1016
1017 fn fillspill_waterfill<'g>(
1021 &self,
1022 input: &SolveInput<'_, 'g>,
1023 subset: &[usize],
1024 num_chunks: usize,
1025 gate: bool,
1026 ) -> Option<Vec<(usize, BigUint)>> {
1027 let amount_in = input.order.amount().clone();
1028 let num_chunks = num_chunks.max(1);
1029 let base_chunk = &amount_in / num_chunks;
1030 if base_chunk.is_zero() {
1031 return None;
1032 }
1033 let remainder = &amount_in - &base_chunk * num_chunks;
1034
1035 let mut overlay = MarketOverrides::empty();
1036 let mut activated: Vec<bool> = vec![!gate; subset.len()];
1037 let mut active_count = if gate { 0 } else { subset.len() };
1038 let mut schedule: Vec<(usize, BigUint)> = Vec::with_capacity(num_chunks);
1039 let mut marginals: Vec<Option<StepResult>> = (0..subset.len())
1044 .map(|_| None)
1045 .collect();
1046 let mut marginals_chunk: Option<BigUint> = None;
1049
1050 for chunk_idx in 0..num_chunks {
1051 if input.deadline.expired() {
1052 break;
1053 }
1054 let chunk = if chunk_idx == 0 { &base_chunk + &remainder } else { base_chunk.clone() };
1055 if marginals_chunk.as_ref() != Some(&chunk) {
1056 marginals
1057 .iter_mut()
1058 .for_each(|m| *m = None);
1059 marginals_chunk = Some(chunk.clone());
1060 }
1061
1062 let mut best: Option<(usize, BigInt)> = None;
1063 for (i, &path_idx) in subset.iter().enumerate() {
1064 if !activated[i] && active_count >= self.max_paths {
1065 continue;
1066 }
1067 if marginals[i].is_none() {
1068 marginals[i] = Self::simulate_step(
1069 &input.ordered[path_idx],
1070 &input.market,
1071 &overlay,
1072 chunk.clone(),
1073 );
1074 }
1075 let Some(step) = marginals[i].as_ref() else {
1076 continue;
1077 };
1078 let gross_marginal = BigInt::from(step.amount_out.clone());
1079 let net_marginal = if activated[i] {
1080 gross_marginal
1081 } else {
1082 let activation = Self::activation_cost(input, &step.gas);
1083 gross_marginal - activation
1084 };
1085 if best
1086 .as_ref()
1087 .map(|(_, m)| &net_marginal > m)
1088 .unwrap_or(true)
1089 {
1090 best = Some((i, net_marginal));
1091 }
1092 }
1093
1094 let Some((best_i, _)) = best else {
1095 break;
1096 };
1097 let Some(step) = marginals[best_i].take() else {
1101 break;
1102 };
1103
1104 let pools_moved: FxHashSet<&ComponentId> = step
1105 .new_states
1106 .iter()
1107 .map(|(id, _)| id)
1108 .collect();
1109 for (i, &path_idx) in subset.iter().enumerate() {
1110 if input.ordered[path_idx]
1111 .edge_iter()
1112 .iter()
1113 .any(|e| pools_moved.contains(&e.component_id))
1114 {
1115 marginals[i] = None;
1116 }
1117 }
1118
1119 for (id, state) in step.new_states {
1120 overlay.insert(id, state);
1121 }
1122 if !activated[best_i] {
1123 activated[best_i] = true;
1124 active_count += 1;
1125 }
1126 schedule.push((best_i, chunk));
1127 }
1128
1129 Some(schedule)
1130 }
1131
1132 fn build_fillspill_route<'g>(
1136 &self,
1137 input: &SolveInput<'_, 'g>,
1138 active: &[usize],
1139 schedule: &[(usize, BigUint)],
1140 ) -> Option<SplitCandidate> {
1141 let amount_in = input.order.amount().clone();
1142 let mut cand_in: Vec<BigUint> = vec![BigUint::zero(); active.len()];
1143 for (i, chunk) in schedule {
1144 cand_in[*i] += chunk;
1145 }
1146 let mut execution_order: Vec<usize> = (0..active.len())
1147 .filter(|&i| !cand_in[i].is_zero())
1148 .collect();
1149 if execution_order.len() < 2 {
1150 return None;
1151 }
1152 execution_order.sort_by(|&a, &b| cand_in[b].cmp(&cand_in[a]));
1153
1154 let mut overrides = MarketOverrides::empty();
1155 let mut allocations = Vec::new();
1156 for i in execution_order {
1157 let allocation = Self::allocation_commit(
1158 &input.ordered[active[i]],
1159 &input.market,
1160 &mut overrides,
1161 cand_in[i].clone(),
1162 ratio(&cand_in[i], &amount_in),
1163 )?;
1164 allocations.push(allocation);
1165 }
1166 Self::candidate_from_allocations(input, &allocations)
1167 }
1168}
1169
1170fn path_reuses_component<W>(path: &Path<'_, W>) -> bool {
1173 let mut seen: FxHashSet<&ComponentId> =
1174 FxHashSet::with_capacity_and_hasher(path.len(), Default::default());
1175 !path
1176 .edge_iter()
1177 .iter()
1178 .all(|e| seen.insert(&e.component_id))
1179}
1180
1181fn simulate_hop(
1184 market: &MarketState,
1185 component_id: &ComponentId,
1186 address_in: &Address,
1187 address_out: &Address,
1188 amount_in: &BigUint,
1189 pass: SolveStage,
1190) -> Result<SwapResult, Refusal> {
1191 let (Some(token_in), Some(token_out), Some(state)) = (
1192 market.get_token(address_in),
1193 market.get_token(address_out),
1194 market.get_simulation_state(component_id),
1195 ) else {
1196 return Err(Refusal::Failed);
1197 };
1198 state
1199 .get_amount_out_metered(component_id, pass.label(), amount_in.clone(), token_in, token_out)
1200 .map(|result| SwapResult { amount_out: result.amount, gas: result.gas })
1201 .map_err(|error| Refusal::of(&error))
1202}
1203
1204fn hop_state<'s>(
1211 market: &'s MarketState,
1212 component_id: &ComponentId,
1213 intra_path: Option<&'s FxHashMap<ComponentId, Box<dyn ProtocolSim>>>,
1214 committed: Option<&'s MarketOverrides>,
1215) -> Option<&'s dyn ProtocolSim> {
1216 intra_path
1217 .and_then(|states| {
1218 states
1219 .get(component_id)
1220 .map(Box::as_ref)
1221 })
1222 .or_else(|| committed.and_then(|overrides| overrides.get(component_id)))
1223 .or_else(|| market.get_simulation_state(component_id))
1224}
1225
1226fn simulate_path<'a>(
1235 path: &Path<'a, DepthAndPrice>,
1236 market: &MarketState,
1237 cache: &mut SwapCache<'a>,
1238 amount_in: BigUint,
1239 stage: SolveStage,
1240) -> Option<SwapResult> {
1241 let mut hop_amount_in = amount_in;
1243 let mut path_gas = BigUint::zero();
1244
1245 for (address_in, edge, address_out) in path.iter() {
1246 let direction = PoolDirection { component_id: &edge.component_id, address_in, address_out };
1247 let hop = cache.swap(
1248 direction,
1249 &hop_amount_in,
1250 stage.label(),
1251 || {
1252 simulate_hop(
1253 market,
1254 &edge.component_id,
1255 address_in,
1256 address_out,
1257 &hop_amount_in,
1258 stage,
1259 )
1260 },
1261 stage.may_interpolate(),
1262 )?;
1263 hop_amount_in = hop.amount_out;
1264 path_gas += hop.gas;
1265 }
1266
1267 Some(SwapResult { amount_out: hop_amount_in, gas: path_gas })
1268}
1269
1270fn rank_outcomes(
1275 outcomes_by_path: Vec<Option<FullAmountOutcome>>,
1276 gas_price: &BigUint,
1277 token_prices: Option<&TokenGasPrices>,
1278 token_out: &Address,
1279) -> FullAmountRanking {
1280 let mut by_output: Vec<(usize, Option<BigInt>)> = Vec::with_capacity(outcomes_by_path.len());
1283 let mut by_output_net_gas: Vec<(usize, BigInt)> = Vec::new();
1284
1285 for (path_ix, outcome) in outcomes_by_path.into_iter().enumerate() {
1286 let Some(outcome) = outcome else {
1287 continue;
1288 };
1289 let net_output = match outcome {
1290 FullAmountOutcome::Filled(paid) => {
1291 let gas_cost = WaterFillAlgorithm::gas_cost_in_token(
1292 &paid.gas,
1293 gas_price,
1294 token_prices,
1295 token_out,
1296 );
1297 let net_output = BigInt::from(paid.amount_out.clone()) -
1298 gas_cost.map_or_else(BigInt::zero, BigInt::from);
1299 by_output_net_gas.push((path_ix, net_output.clone()));
1300 Some(net_output)
1301 }
1302 FullAmountOutcome::Unfilled => None,
1303 };
1304 by_output.push((path_ix, net_output));
1305 }
1306
1307 by_output.sort_by(|(_, a), (_, b)| b.cmp(a));
1311 by_output_net_gas.sort_by(|(_, a), (_, b)| b.cmp(a));
1312 FullAmountRanking {
1313 by_output: by_output
1314 .into_iter()
1315 .map(|(path_ix, _)| path_ix)
1316 .collect(),
1317 by_output_net_gas: by_output_net_gas
1318 .into_iter()
1319 .map(|(path_ix, _)| path_ix)
1320 .collect(),
1321 }
1322}
1323
1324fn path_key<W>(path: &Path<'_, W>) -> Vec<ComponentId> {
1326 path.edge_iter()
1327 .iter()
1328 .map(|e| e.component_id.clone())
1329 .collect()
1330}
1331
1332fn ratio(numerator: &BigUint, denominator: &BigUint) -> f64 {
1334 use num_traits::ToPrimitive;
1335 let n = numerator.to_f64().unwrap_or(0.0);
1336 let d = denominator.to_f64().unwrap_or(1.0);
1337 if d == 0.0 {
1338 0.0
1339 } else {
1340 n / d
1341 }
1342}
1343
1344fn discover_paths<'a, W>(
1357 graph: &'a TopologyGraph<W>,
1358 market: &MarketDataView<'_>,
1359 order: &Order,
1360 cache: &mut SwapCache<'a>,
1361 cfg: CandidateSearchConfig<'_>,
1362) -> Result<Vec<Path<'a, W>>, AlgorithmError>
1363where
1364 W: Clone,
1365{
1366 let (from_idx, to_idx) = get_token_ixs(graph, order)?;
1367
1368 let mut found = Vec::new();
1369 let mut frontier = vec![CandidatePathState {
1370 node: from_idx,
1371 path: Path::new(),
1372 amount_out: order.amount().clone(),
1373 }];
1374
1375 for _depth in 0..cfg.query.max_hops {
1376 if cfg.deadline.expired() || frontier.is_empty() {
1377 break;
1378 }
1379 let mut next_by_node: FxHashMap<NodeIndex, Vec<CandidatePathState<'a, W>>> =
1380 FxHashMap::default();
1381 for state in frontier {
1382 if state.node == to_idx && from_idx != to_idx {
1383 continue;
1384 }
1385
1386 let mut discovery = Discovery { graph, market, cfg: &cfg, cache };
1387 expand_candidate_state(&mut discovery, to_idx, state, &mut found, &mut next_by_node);
1388 }
1389 frontier = prune_candidate_frontier(next_by_node);
1390 }
1391
1392 rank_found_candidate_paths(found, cfg.max_candidates, order)
1393}
1394
1395fn get_token_ixs<W>(
1401 graph: &TopologyGraph<W>,
1402 order: &Order,
1403) -> Result<(NodeIndex, NodeIndex), AlgorithmError> {
1404 let missing = |reason| AlgorithmError::NoPath {
1405 from: order.token_in().clone(),
1406 to: order.token_out().clone(),
1407 reason,
1408 };
1409 let from_idx = graph
1410 .get_token_ix(order.token_in())
1411 .ok_or_else(|| missing(NoPathReason::SourceTokenNotInGraph))?;
1412 let to_idx = graph
1413 .get_token_ix(order.token_out())
1414 .ok_or_else(|| missing(NoPathReason::DestinationTokenNotInGraph))?;
1415 Ok((from_idx, to_idx))
1416}
1417
1418fn expand_candidate_state<'a, W>(
1419 discovery: &mut Discovery<'a, '_, W>,
1420 target: NodeIndex,
1421 state: CandidatePathState<'a, W>,
1422 found: &mut Vec<(Path<'a, W>, BigUint)>,
1423 next_by_node: &mut FxHashMap<NodeIndex, Vec<CandidatePathState<'a, W>>>,
1424) where
1425 W: Clone,
1426{
1427 let cfg = discovery.cfg;
1428 let graph = discovery.graph;
1429 let edges = candidate_edges_for_state(discovery, target, &state);
1430 for candidate in edges {
1431 if cfg.deadline.expired() {
1432 break;
1433 }
1434 let mut path = state.path.clone();
1435 path.add_hop(&graph[state.node], candidate.edge, &graph[candidate.target]);
1436 let path_state = CandidatePathState {
1437 node: candidate.target,
1438 path: path.clone(),
1439 amount_out: candidate.amount_out,
1440 };
1441 if candidate.target == target && path.len() >= cfg.query.min_hops {
1442 found.push((path.clone(), path_state.amount_out.clone()));
1443 }
1444 if path.len() < cfg.query.max_hops {
1445 next_by_node
1446 .entry(candidate.target)
1447 .or_default()
1448 .push(path_state);
1449 }
1450 }
1451}
1452
1453fn candidate_edges_for_state<'a, W>(
1454 discovery: &mut Discovery<'a, '_, W>,
1455 target: NodeIndex,
1456 state: &CandidatePathState<'a, W>,
1457) -> Vec<ScoredEdge<'a, W>> {
1458 let mut preferred = score_candidate_edges(discovery, target, state, true);
1459 if preferred.is_empty() {
1460 preferred = score_candidate_edges(discovery, target, state, false);
1461 }
1462 select_candidate_edges(preferred, CANDIDATE_EDGES_PER_STATE)
1463}
1464
1465fn score_candidate_edges<'a, W>(
1466 discovery: &mut Discovery<'a, '_, W>,
1467 target: NodeIndex,
1468 state: &CandidatePathState<'a, W>,
1469 preferred_only: bool,
1470) -> Vec<ScoredEdge<'a, W>> {
1471 let Discovery { graph, market, cfg, cache } = discovery;
1472 let graph = *graph;
1473 let market = *market;
1474 let cfg = *cfg;
1475 let mut scored = Vec::new();
1476 for edge in graph.edges(state.node) {
1477 let next_node = edge.target();
1478 let priority = match candidate_priority(graph, next_node, target, cfg) {
1481 Some(priority) => priority,
1482 None if preferred_only => continue,
1483 None => 3,
1484 };
1485 for pool in edge.weight().pools() {
1486 if !can_extend_path(graph, state, next_node, target, pool, cfg) {
1487 continue;
1488 }
1489 let address_in = &graph[state.node];
1490 let address_out = &graph[next_node];
1491 let direction =
1492 PoolDirection { component_id: &pool.component_id, address_in, address_out };
1493 let Some(hop) = cache.swap(
1494 direction,
1495 &state.amount_out,
1496 SolveStage::Discovery.label(),
1497 || simulate_edge(market, &state.amount_out, address_in, pool, address_out),
1498 SolveStage::Discovery.may_interpolate(),
1499 ) else {
1500 continue;
1501 };
1502 scored.push(ScoredEdge {
1503 target: next_node,
1504 edge: pool,
1505 amount_out: hop.amount_out,
1506 priority,
1507 });
1508 }
1509 }
1510 scored
1511}
1512
1513fn derive_anchor_tokens<W>(graph: &TopologyGraph<W>) -> FxHashSet<Address> {
1523 let mut by_pool_count: Vec<(NodeIndex, usize)> = graph
1524 .node_indices()
1525 .map(|node| {
1526 let pools = graph
1527 .edges(node)
1528 .map(|edge| edge.weight().pools().len())
1529 .sum();
1530 (node, pools)
1531 })
1532 .collect();
1533 by_pool_count.sort_unstable_by_key(|(_, pools)| Reverse(*pools));
1534 let mut anchors: FxHashSet<Address> = by_pool_count
1535 .into_iter()
1536 .take(DERIVED_ANCHOR_COUNT)
1537 .map(|(node, _)| graph[node].clone())
1538 .collect();
1539 anchors.insert(Address::from([0u8; 20]));
1540 anchors
1541}
1542
1543fn candidate_priority<W>(
1544 graph: &TopologyGraph<W>,
1545 node: NodeIndex,
1546 target: NodeIndex,
1547 cfg: &CandidateSearchConfig<'_>,
1548) -> Option<u8> {
1549 if node == target {
1550 return Some(0);
1551 }
1552 let token = &graph[node];
1553 match cfg.query.connector_tokens.as_ref() {
1554 Some(tokens) => tokens.contains(token).then_some(1),
1555 None => cfg
1556 .anchor_tokens
1557 .contains(token)
1558 .then_some(2),
1559 }
1560}
1561
1562fn can_extend_path<W>(
1563 graph: &TopologyGraph<W>,
1564 state: &CandidatePathState<'_, W>,
1565 next_node: NodeIndex,
1566 target: NodeIndex,
1567 edge: &EdgeData<W>,
1568 cfg: &CandidateSearchConfig<'_>,
1569) -> bool {
1570 let next_addr = &graph[next_node];
1571 if state
1572 .path
1573 .edge_iter()
1574 .iter()
1575 .any(|existing| existing.component_id == edge.component_id)
1576 {
1577 return false;
1578 }
1579 if state.path.tokens.contains(&next_addr) {
1580 return false;
1581 }
1582 if next_addr == cfg.source_token {
1583 return false;
1584 }
1585 if next_node == target {
1586 return true;
1587 }
1588 cfg.query
1589 .connector_tokens
1590 .as_ref()
1591 .map(|tokens| tokens.contains(next_addr))
1592 .unwrap_or(true)
1593}
1594
1595fn simulate_edge<W>(
1596 market: &MarketDataView<'_>,
1597 amount: &BigUint,
1598 token_in_addr: &Address,
1599 edge: &EdgeData<W>,
1600 token_out_addr: &Address,
1601) -> Result<SwapResult, Refusal> {
1602 let (Some(token_in), Some(token_out), Some(state)) = (
1603 market.get_token(token_in_addr),
1604 market.get_token(token_out_addr),
1605 market.get_simulation_state(&edge.component_id),
1606 ) else {
1607 return Err(Refusal::Failed);
1608 };
1609 state
1610 .get_amount_out_metered(
1611 &edge.component_id,
1612 SolveStage::Discovery.label(),
1613 amount.clone(),
1614 token_in,
1615 token_out,
1616 )
1617 .map(|result| SwapResult { amount_out: result.amount, gas: result.gas })
1618 .map_err(|error| Refusal::of(&error))
1619}
1620
1621fn select_candidate_edges<W>(
1622 mut scored: Vec<ScoredEdge<'_, W>>,
1623 max_edges: usize,
1624) -> Vec<ScoredEdge<'_, W>> {
1625 scored.sort_by(compare_scored_edges);
1626 let mut selected = Vec::new();
1627 let mut per_target: FxHashMap<NodeIndex, usize> = FxHashMap::default();
1628 for edge in scored {
1629 let limit = if edge.priority == 0 {
1630 CANDIDATE_DIRECT_EDGES_PER_TOKEN
1631 } else {
1632 CANDIDATE_CONNECTOR_EDGES_PER_TOKEN
1633 };
1634 let count = per_target
1635 .entry(edge.target)
1636 .or_default();
1637 if *count >= limit {
1638 continue;
1639 }
1640 *count += 1;
1641 selected.push(edge);
1642 if selected.len() >= max_edges {
1643 break;
1644 }
1645 }
1646 selected
1647}
1648
1649fn compare_scored_edges<W>(a: &ScoredEdge<'_, W>, b: &ScoredEdge<'_, W>) -> Ordering {
1650 a.priority
1651 .cmp(&b.priority)
1652 .then_with(|| b.amount_out.cmp(&a.amount_out))
1653}
1654
1655fn prune_candidate_frontier<W>(
1656 by_node: FxHashMap<NodeIndex, Vec<CandidatePathState<'_, W>>>,
1657) -> Vec<CandidatePathState<'_, W>> {
1658 by_node
1659 .into_values()
1660 .flat_map(|mut states| {
1661 states.sort_by(|a, b| b.amount_out.cmp(&a.amount_out));
1662 states.truncate(CANDIDATE_STATES_PER_NODE);
1663 states
1664 })
1665 .collect()
1666}
1667
1668fn rank_found_candidate_paths<'a, W>(
1669 mut found: Vec<(Path<'a, W>, BigUint)>,
1670 max_candidates: usize,
1671 order: &Order,
1672) -> Result<Vec<Path<'a, W>>, AlgorithmError> {
1673 found.sort_by(|(_, a), (_, b)| b.cmp(a));
1674 let mut keys = FxHashSet::default();
1675 let mut paths = Vec::new();
1676 let mut scores = Vec::new();
1677
1678 for (path, amount_out) in found {
1679 if !keys.insert(path_key(&path)) {
1680 continue;
1681 }
1682 let idx = paths.len();
1683 paths.push(path);
1684 scores.push((idx, BigInt::from(amount_out)));
1685 if paths.len() >= max_candidates {
1686 break;
1687 }
1688 }
1689
1690 if paths.is_empty() {
1691 return Err(AlgorithmError::NoPath {
1692 from: order.token_in().clone(),
1693 to: order.token_out().clone(),
1694 reason: NoPathReason::NoGraphPath,
1695 });
1696 }
1697 Ok(paths)
1698}
1699
1700#[cfg(test)]
1701mod tests {
1702 use std::time::Duration;
1703
1704 use alloy::primitives::U256;
1705 use num_bigint::BigUint;
1706 use num_traits::ToPrimitive;
1707 use tycho_simulation::evm::protocol::uniswap_v2::state::UniswapV2State;
1708
1709 use super::{super::MostLiquidAlgorithm, *};
1710 use crate::{
1711 algorithm::{
1712 split_test_harness::{
1713 evaluate_scenario, optimal_two_component_output, split_metrics, split_scenarios,
1714 two_equal_weth_usdc, TWO_EQUAL_USDC_RESERVE, TWO_EQUAL_WETH_RESERVE,
1715 },
1716 test_utils::{
1717 addr, setup_market_unweighted_topology, setup_market_weighted_boxed,
1718 token_with_decimals, ConstantProductSim, DivByZeroSim,
1719 },
1720 },
1721 graph::GraphManager,
1722 types::quote::OrderSide,
1723 };
1724
1725 fn config() -> AlgorithmConfig {
1726 config_ms(2000)
1727 }
1728
1729 fn config_ms(ms: u64) -> AlgorithmConfig {
1730 AlgorithmConfig::new(1, 3, Duration::from_millis(ms), None).unwrap()
1731 }
1732
1733 fn whole_weth_order(token_in: &Address, token_out: &Address, weth: u64) -> Order {
1734 Order::new(
1735 token_in.clone(),
1736 token_out.clone(),
1737 BigUint::from(weth) * BigUint::from(10u64).pow(18),
1738 OrderSide::Sell,
1739 addr(0xFF),
1740 )
1741 }
1742
1743 fn v2_component(reserve_a: u128, reserve_b: u128) -> UniswapV2State {
1744 UniswapV2State::new(
1745 U256::from(reserve_a) * U256::from(10u64).pow(U256::from(18u64)),
1746 U256::from(reserve_b) * U256::from(10u64).pow(U256::from(18u64)),
1747 )
1748 }
1749
1750 fn water_fill_default() -> WaterFillAlgorithm {
1753 WaterFillAlgorithm::with_config(
1754 AlgorithmConfig::new(1, 4, Duration::from_millis(5000), None).unwrap(),
1755 )
1756 .unwrap()
1757 }
1758
1759 #[tokio::test]
1762 async fn test_water_fill_all_scenarios() {
1763 let algo = water_fill_default();
1764 for scenario in split_scenarios::all() {
1765 let name = scenario.name;
1766 let (market, gm) = scenario.build_market_weighted();
1767 let result = evaluate_scenario(&algo, &scenario, market, gm).await;
1768
1769 result.assert_passes_lower_bound();
1770 let tolerance_pct = if name == "DOUBLE_SPLIT" { 10 } else { 5 };
1776 assert!(
1777 result.within_pct_of_optimum(tolerance_pct),
1778 "'{name}': not within {tolerance_pct}% of optimum",
1779 );
1780 let route = result
1781 .route
1782 .as_ref()
1783 .unwrap_or_else(|| panic!("'{name}': expected a route"));
1784 assert!(route.validate().is_ok(), "'{name}': route validation failed");
1785 }
1786 }
1787
1788 #[tokio::test]
1791 async fn test_water_fill_gas_kills_split() {
1792 let scenario = split_scenarios::gas_kills_split();
1793 let (market, gm) = scenario.build_market_weighted();
1794 let result = evaluate_scenario(&water_fill_default(), &scenario, market, gm).await;
1795 assert_eq!(result.path_count, 1, "high gas should prevent splitting");
1796 }
1797
1798 #[tokio::test]
1804 async fn test_split_fills_when_no_single_path_can() {
1805 let a = token_with_decimals(0x01, "A", 18);
1806 let b = token_with_decimals(0x02, "B", 18);
1807 let component = || {
1811 Box::new(ConstantProductSim {
1812 reserve_0: BigUint::from(800u64) * BigUint::from(10u64).pow(18),
1813 reserve_1: BigUint::from(10_000u64) * BigUint::from(10u64).pow(18),
1814 gas: 50_000,
1815 }) as Box<dyn ProtocolSim>
1816 };
1817 let (market, gm) = setup_market_weighted_boxed(vec![
1818 ("component_x", &a, &b, component()),
1819 ("component_y", &a, &b, component()),
1820 ]);
1821 let order = Order::new(
1822 a.address.clone(),
1823 b.address.clone(),
1824 BigUint::from(1_000u64) * BigUint::from(10u64).pow(18),
1825 OrderSide::Sell,
1826 addr(0xFF),
1827 );
1828
1829 let single = MostLiquidAlgorithm::with_config(config())
1831 .unwrap()
1832 .find_best_route(gm.graph(), market.clone(), None, None, &order)
1833 .await;
1834 assert!(single.is_err(), "premise: no single path fills the full order");
1835
1836 let split = WaterFillAlgorithm::with_config(config())
1838 .unwrap()
1839 .find_best_route(gm.graph(), market.clone(), None, None, &order)
1840 .await
1841 .expect("water_fill returns a split when no single path fills");
1842 assert!(split.route().swaps().len() >= 2, "expected a split across both components");
1843 }
1844
1845 #[tokio::test]
1849 async fn test_water_fill_contains_simulation_panic() {
1850 let a = token_with_decimals(0x01, "A", 18);
1851 let b = token_with_decimals(0x02, "B", 18);
1852 let healthy = Box::new(ConstantProductSim {
1853 reserve_0: BigUint::from(10_000u64) * BigUint::from(10u64).pow(18),
1854 reserve_1: BigUint::from(10_000u64) * BigUint::from(10u64).pow(18),
1855 gas: 50_000,
1856 }) as Box<dyn ProtocolSim>;
1857 let (market, gm) = setup_market_weighted_boxed(vec![
1858 ("component_ok", &a, &b, healthy),
1859 ("component_panics", &a, &b, Box::new(DivByZeroSim::default())),
1860 ]);
1861 let order = Order::new(
1862 a.address.clone(),
1863 b.address.clone(),
1864 BigUint::from(100u64) * BigUint::from(10u64).pow(18),
1865 OrderSide::Sell,
1866 addr(0xFF),
1867 );
1868
1869 let result = WaterFillAlgorithm::with_config(config())
1870 .unwrap()
1871 .find_best_route(gm.graph(), market.clone(), None, None, &order)
1872 .await
1873 .expect("panicking component is skipped; the healthy component still fills the order");
1874
1875 assert!(
1876 result
1877 .route()
1878 .swaps()
1879 .iter()
1880 .all(|swap| swap.component_id() == "component_ok"),
1881 "route must only use the healthy component",
1882 );
1883 }
1884
1885 #[tokio::test]
1889 async fn test_water_fill_output_near_two_component_optimum() {
1890 let m = two_equal_weth_usdc(1);
1891 let trade = 500u64;
1892 let order = whole_weth_order(&m.weth, &m.usdc, trade);
1893
1894 let result = WaterFillAlgorithm::with_config(config())
1895 .unwrap()
1896 .find_best_route(
1897 m.weighted.graph(),
1898 m.market.clone(),
1899 None,
1900 Some(m.derived.clone()),
1901 &order,
1902 )
1903 .await
1904 .expect("split solves");
1905 let (_, path_count, gross) = split_metrics(&result, &m.weth, &m.usdc);
1906 assert_eq!(path_count, 2, "the optimum uses both components");
1907
1908 let reserve_in = TWO_EQUAL_WETH_RESERVE as f64 * 1e18;
1910 let reserve_out = TWO_EQUAL_USDC_RESERVE as f64 * 1e6;
1911 let trade_amount = trade as f64 * 1e18;
1912 let (_, optimum) = optimal_two_component_output(
1913 reserve_in,
1914 reserve_out,
1915 reserve_in,
1916 reserve_out,
1917 trade_amount,
1918 );
1919
1920 let gross = gross.to_f64().unwrap();
1921 assert!(
1922 gross >= optimum * 0.999 && gross <= optimum * 1.0001,
1923 "split gross {gross} should be within 0.1% of the two-component optimum {optimum}",
1924 );
1925 }
1926
1927 #[tokio::test]
1934 async fn test_water_fill_no_loss_under_tight_timeout() {
1935 for ms in [1u64, 5, 50] {
1936 let m = two_equal_weth_usdc(1_000_000_000);
1937 let order = whole_weth_order(&m.weth, &m.usdc, 500);
1938
1939 let split = WaterFillAlgorithm::with_config(config_ms(ms))
1940 .unwrap()
1941 .find_best_route(
1942 m.weighted.graph(),
1943 m.market.clone(),
1944 None,
1945 Some(m.derived.clone()),
1946 &order,
1947 )
1948 .await;
1949 let single = MostLiquidAlgorithm::with_config(config_ms(ms))
1950 .unwrap()
1951 .find_best_route(
1952 m.weighted.graph(),
1953 m.market.clone(),
1954 None,
1955 Some(m.derived.clone()),
1956 &order,
1957 )
1958 .await;
1959
1960 let (Ok(split), Ok(single)) = (split, single) else {
1961 continue;
1962 };
1963 let (split_net, _, _) = split_metrics(&split, &m.weth, &m.usdc);
1964 let (single_net, _, _) = split_metrics(&single, &m.weth, &m.usdc);
1965 assert!(
1966 split_net >= single_net,
1967 "split lost to single-path under {ms}ms timeout: split={split_net} single={single_net}",
1968 );
1969 }
1970 }
1971
1972 #[tokio::test]
1978 async fn test_discovery_finds_and_ranks_parallel_components() {
1979 let link = token_with_decimals(0x01, "LINK", 18);
1980 let weth = token_with_decimals(0x02, "WETH", 18);
1981 let (market, graph_manager) = setup_market_unweighted_topology(vec![
1982 (
1983 "a_weak_link_weth",
1984 &link,
1985 &weth,
1986 Box::new(v2_component(2_000_000, 264)) as Box<dyn ProtocolSim>,
1987 ),
1988 (
1989 "z_strong_link_weth",
1990 &link,
1991 &weth,
1992 Box::new(v2_component(2_000_000, 5_700)) as Box<dyn ProtocolSim>,
1993 ),
1994 ]);
1995 let order = Order::new(
1996 link.address.clone(),
1997 weth.address.clone(),
1998 BigUint::from(1_000u64) * BigUint::from(10u64).pow(18),
1999 OrderSide::Sell,
2000 addr(0xFF),
2001 );
2002
2003 let start = Instant::now();
2004 let view = market.read().await;
2005 let paths = discover_paths(
2006 graph_manager.graph(),
2007 &view,
2008 &order,
2009 &mut SwapCache::new(),
2010 CandidateSearchConfig {
2011 query: &GraphQueryFilter { min_hops: 1, max_hops: 3, connector_tokens: None },
2012 max_candidates: 128,
2013 anchor_tokens: &FxHashSet::default(),
2014 source_token: order.token_in(),
2015 deadline: Deadline::new(start, Duration::from_millis(2000)),
2016 },
2017 )
2018 .expect("discovery finds candidates");
2019
2020 assert_eq!(paths.len(), 2, "both parallel components should be discovered");
2021 let best_path = &paths[0];
2023 assert_eq!(
2024 best_path.edge_iter()[0].component_id,
2025 "z_strong_link_weth",
2026 "discovery should rank by simulated output, not topology or edge weights",
2027 );
2028 }
2029
2030 fn hop(amount_out: u64, gas: u64) -> SwapResult {
2033 SwapResult { amount_out: BigUint::from(amount_out), gas: BigUint::from(gas) }
2034 }
2035
2036 #[test]
2039 fn test_rank_outcomes_places_unfilled_and_missing_paths() {
2040 let outcomes = vec![
2041 Some(FullAmountOutcome::Filled(hop(1000, 0))),
2042 Some(FullAmountOutcome::Unfilled),
2043 None,
2044 Some(FullAmountOutcome::Filled(hop(3000, 0))),
2045 ];
2046
2047 let ranking = rank_outcomes(outcomes, &BigUint::from(1u64), None, &addr(0x02));
2048
2049 assert_eq!(ranking.by_output, vec![3, 0, 1], "unfilled ranks last, missing is absent");
2050 assert_eq!(ranking.by_output_net_gas, vec![3, 0], "unfilled cannot be the baseline");
2051 }
2052
2053 #[test]
2057 fn test_rank_outcomes_orders_by_output_net_of_gas() {
2058 let token_out = addr(0x02);
2059 let mut token_prices = TokenGasPrices::default();
2060 token_prices.insert(
2061 token_out.clone(),
2062 tycho_simulation::tycho_common::simulation::protocol_sim::Price::new(
2063 BigUint::from(1u64),
2064 BigUint::from(1u64),
2065 ),
2066 );
2067 let outcomes = vec![
2068 Some(FullAmountOutcome::Filled(hop(1000, 500))),
2069 Some(FullAmountOutcome::Filled(hop(900, 10))),
2070 ];
2071
2072 let ranking =
2073 rank_outcomes(outcomes, &BigUint::from(1u64), Some(&token_prices), &token_out);
2074
2075 assert_eq!(ranking.by_output, vec![1, 0], "the cheaper path ranks first");
2076 assert_eq!(ranking.by_output_net_gas, vec![1, 0], "and the baseline ordering agrees");
2077 }
2078
2079 #[test]
2083 fn test_rank_outcomes_places_a_loss_making_path_above_an_unfilled_one() {
2084 let token_out = addr(0x02);
2085 let mut token_prices = TokenGasPrices::default();
2086 token_prices.insert(
2087 token_out.clone(),
2088 tycho_simulation::tycho_common::simulation::protocol_sim::Price::new(
2089 BigUint::from(1u64),
2090 BigUint::from(1u64),
2091 ),
2092 );
2093 let outcomes = vec![
2094 Some(FullAmountOutcome::Unfilled),
2095 Some(FullAmountOutcome::Filled(hop(100, 500))),
2097 Some(FullAmountOutcome::Filled(hop(900, 10))),
2098 ];
2099
2100 let ranking =
2101 rank_outcomes(outcomes, &BigUint::from(1u64), Some(&token_prices), &token_out);
2102
2103 assert_eq!(ranking.by_output, vec![2, 1, 0], "unfilled ranks below a loss-making path");
2104 }
2105
2106 #[test]
2109 fn test_rank_outcomes_ignores_gas_it_cannot_price() {
2110 let outcomes = vec![
2111 Some(FullAmountOutcome::Filled(hop(1000, 500))),
2112 Some(FullAmountOutcome::Filled(hop(900, 10))),
2113 ];
2114
2115 let ranking = rank_outcomes(outcomes, &BigUint::from(1u64), None, &addr(0x02));
2116
2117 assert_eq!(ranking.by_output_net_gas, vec![0, 1]);
2118 }
2119
2120 #[test]
2123 fn test_derive_anchor_tokens_ranks_hub_and_includes_native_sentinel() {
2124 let hub = token_with_decimals(0x01, "HUB", 18);
2125 let a = token_with_decimals(0x02, "A", 18);
2126 let b = token_with_decimals(0x03, "B", 18);
2127 let c = token_with_decimals(0x04, "C", 18);
2128 let (_market, graph_manager) = setup_market_unweighted_topology(vec![
2129 ("hub_a", &hub, &a, Box::new(v2_component(1, 1)) as Box<dyn ProtocolSim>),
2130 ("hub_b", &hub, &b, Box::new(v2_component(1, 1)) as Box<dyn ProtocolSim>),
2131 ("hub_c", &hub, &c, Box::new(v2_component(1, 1)) as Box<dyn ProtocolSim>),
2132 ]);
2133
2134 let anchors = derive_anchor_tokens(graph_manager.graph());
2135
2136 assert!(anchors.contains(&hub.address), "highest-degree token should be anchored");
2137 assert!(
2138 anchors.contains(&Address::from([0u8; 20])),
2139 "native-ETH sentinel should always be anchored",
2140 );
2141 }
2142}