1pub mod config;
25
26use std::{
27 collections::HashSet,
28 time::{Duration, Instant},
29};
30
31use config::WorkerPoolRouterConfig;
32use futures::stream::{FuturesUnordered, StreamExt};
33use metrics::{counter, histogram};
34use num_bigint::BigUint;
35use tracing::{debug, warn};
36use tycho_execution::encoding::{
37 evm::gas_estimator::estimate_gas_usage,
38 models::{Solution, Strategy},
39};
40use tycho_simulation::tycho_common::Bytes;
41
42use crate::{
43 encoding::encoder::Encoder, price_guard::guard::PriceGuard,
44 worker_pool::task_queue::TaskQueueHandle, BlockInfo, EncodingOptions, Order, OrderQuote, Quote,
45 QuoteOptions, QuoteRequest, QuoteStatus, SolveError, SolveParams,
46};
47
48#[derive(Clone)]
50pub struct SolverPoolHandle {
51 name: String,
53 queue: TaskQueueHandle,
55}
56
57impl SolverPoolHandle {
58 pub fn new(name: impl Into<String>, queue: TaskQueueHandle) -> Self {
60 Self { name: name.into(), queue }
61 }
62
63 pub fn name(&self) -> &str {
65 &self.name
66 }
67
68 pub fn queue(&self) -> &TaskQueueHandle {
70 &self.queue
71 }
72}
73
74#[derive(Debug)]
76pub(crate) struct OrderResponses {
77 order_id: String,
79 quotes: Vec<(String, OrderQuote)>,
81 failed_solvers: Vec<(String, SolveError)>,
84}
85
86pub struct WorkerPoolRouter {
88 solver_pools: Vec<SolverPoolHandle>,
90 config: WorkerPoolRouterConfig,
92 encoder: Encoder,
94 price_guard: Option<PriceGuard>,
97}
98
99impl WorkerPoolRouter {
100 pub fn new(
102 solver_pools: Vec<SolverPoolHandle>,
103 config: WorkerPoolRouterConfig,
104 encoder: Encoder,
105 ) -> Self {
106 Self { solver_pools, config, encoder, price_guard: None }
107 }
108
109 pub fn with_price_guard(mut self, price_guard: PriceGuard) -> Self {
114 self.price_guard = Some(price_guard);
115 self
116 }
117
118 pub fn num_pools(&self) -> usize {
120 self.solver_pools.len()
121 }
122
123 pub async fn quote(&self, request: QuoteRequest) -> Result<Quote, SolveError> {
132 let start = Instant::now();
133 let deadline = start + self.effective_timeout(request.options());
134 let min_responses = request
135 .options()
136 .min_responses()
137 .unwrap_or(self.config.min_responses());
138
139 if self.solver_pools.is_empty() {
140 return Err(SolveError::Internal("no solver pools configured".to_string()));
141 }
142
143 let params = match request.options().state_label().cloned() {
144 Some(label) => SolveParams::default().with_state_label(label),
145 None => SolveParams::default(),
146 };
147
148 let order_futures: Vec<_> = request
150 .orders()
151 .iter()
152 .map(|order| self.solve_order(order.clone(), params.clone(), deadline, min_responses))
153 .collect();
154
155 let mut order_responses = futures::future::join_all(order_futures).await;
156
157 if let Some(encoding_options) = request.options().encoding_options() {
160 refine_gas_estimates(&mut order_responses, encoding_options)?;
161 }
162
163 let ranked_quotes: Vec<Vec<OrderQuote>> = order_responses
165 .into_iter()
166 .map(|responses| self.rank_quotes(&responses, request.options()))
167 .collect();
168
169 let price_guard_config = request
171 .options()
172 .encoding_options()
173 .map(|e| e.price_guard())
174 .filter(|c| c.enabled());
175
176 let mut order_quotes: Vec<OrderQuote> = match (&self.price_guard, price_guard_config) {
177 (Some(guard), Some(config)) => guard
178 .validate(ranked_quotes, config)
179 .map_err(|e| {
180 warn!(error = %e, "price guard validation error");
181 SolveError::Internal(e.to_string())
182 })?,
183 (None, Some(_)) => {
184 return Err(SolveError::Internal(
185 "price guard config provided but price guard is not enabled on this server"
186 .to_string(),
187 ));
188 }
189 _ => ranked_quotes
190 .into_iter()
191 .filter_map(|candidates| candidates.into_iter().next())
192 .collect(),
193 };
194
195 if let Some(encoding_options) = request.options().encoding_options() {
197 order_quotes = self
198 .encoder
199 .encode(order_quotes, encoding_options.clone())
200 .await?;
201 }
202
203 let total_gas_estimate = order_quotes
205 .iter()
206 .map(|o| o.gas_estimate())
207 .fold(BigUint::ZERO, |acc, g| acc + g);
208
209 let solve_time_ms = start.elapsed().as_millis() as u64;
210
211 Ok(Quote::new(order_quotes, total_gas_estimate, solve_time_ms))
212 }
213
214 async fn solve_order(
216 &self,
217 order: Order,
218 params: SolveParams,
219 deadline: Instant,
220 min_responses: usize,
221 ) -> OrderResponses {
222 let start_time = Instant::now();
223 let order_id = order.id().to_string();
224
225 let mut pending: FuturesUnordered<_> = self
229 .solver_pools
230 .iter()
231 .map(|pool| {
232 let order_clone = order.clone();
233 let pool_name = pool.name().to_string();
234 let queue = pool.queue().clone();
235 let task_params = params.clone();
236
237 async move {
238 let result = queue
239 .enqueue(order_clone, task_params)
240 .await;
241 (pool_name, result)
242 }
243 })
244 .collect();
245
246 let mut quotes = Vec::new();
247 let mut failed_solvers: Vec<(String, SolveError)> = Vec::new();
248 let mut remaining_pools: HashSet<String> = self
249 .solver_pools
250 .iter()
251 .map(|p| p.name().to_string())
252 .collect();
253
254 loop {
256 let deadline_instant = tokio::time::Instant::from_std(deadline);
257
258 tokio::select! {
259 biased;
261
262 _ = tokio::time::sleep_until(deadline_instant) => {
264 let elapsed_ms = deadline.saturating_duration_since(Instant::now())
266 .as_millis() as u64;
267 for pool_name in remaining_pools.drain() {
268 failed_solvers.push((
269 pool_name,
270 SolveError::Timeout { elapsed_ms },
271 ));
272 }
273 break;
274 }
275
276 result = pending.next() => {
278 match result {
279 Some((pool_name, Ok(single_quote))) => {
280 remaining_pools.remove(&pool_name);
282
283 quotes.push((pool_name.clone(), single_quote.order().clone()));
285
286 if min_responses > 0 && quotes.len() >= min_responses {
288 debug!(
289 order_id = %order_id,
290 responses = quotes.len(),
291 min_responses,
292 "early return: min_responses reached"
293 );
294 counter!("worker_router_early_returns_total").increment(1);
295 break;
296 }
297 }
298 Some((pool_name, Err(e))) => {
299 remaining_pools.remove(&pool_name);
300 debug!(
301 pool = %pool_name,
302 order_id = %order_id,
303 error = %e,
304 "solver pool failed"
305 );
306 failed_solvers.push((pool_name, e));
307 }
308 None => {
309 break;
311 }
312 }
313 }
314 }
315 }
316
317 let duration = start_time.elapsed().as_secs_f64();
319 histogram!("worker_router_solve_duration_seconds").record(duration);
320 histogram!("worker_router_solver_responses").record(quotes.len() as f64);
321
322 for (pool_name, error) in &failed_solvers {
324 let error_type = match error {
325 SolveError::Timeout { .. } => "timeout",
326 SolveError::NoRouteFound { .. } => "no_route",
327 SolveError::QueueFull => "queue_full",
328 SolveError::Internal(_) => "internal",
329 SolveError::PriceCheckFailed { .. } => "price_check_failed",
330 _ => "other",
331 };
332 counter!("worker_router_solver_failures_total", "pool" => pool_name.clone(), "error_type" => error_type).increment(1);
333 }
334
335 if !failed_solvers.is_empty() {
336 let timeout_count = failed_solvers
337 .iter()
338 .filter(|(_, e)| matches!(e, SolveError::Timeout { .. }))
339 .count();
340 let other_count = failed_solvers.len() - timeout_count;
341 warn!(
342 order_id = %order_id,
343 timeout_count,
344 other_failures = other_count,
345 "some solver pools failed"
346 );
347 }
348
349 OrderResponses { order_id, quotes, failed_solvers }
350 }
351
352 fn rank_quotes(&self, responses: &OrderResponses, options: &QuoteOptions) -> Vec<OrderQuote> {
358 let mut valid_quotes: Vec<_> = responses
359 .quotes
360 .iter()
361 .filter(|(_, q)| q.status() == QuoteStatus::Success)
362 .filter(|(_, q)| {
363 options
364 .max_gas()
365 .map(|max| q.gas_estimate() <= max)
366 .unwrap_or(true)
367 })
368 .collect();
369
370 valid_quotes.sort_by(|(_, a), (_, b)| {
372 b.amount_out_net_gas()
373 .cmp(a.amount_out_net_gas())
374 });
375
376 if !valid_quotes.is_empty() {
377 counter!("worker_router_orders_total", "status" => "success").increment(1);
378 let (pool_name, best) = valid_quotes[0];
379 counter!("worker_router_best_quote_pool", "pool" => pool_name.clone()).increment(1);
380 debug!(
381 order_id = %best.order_id(),
382 number_of_candidates = valid_quotes.len(),
383 "ranked quotes"
384 );
385 return valid_quotes
386 .into_iter()
387 .map(|(_, q)| q.clone())
388 .collect();
389 }
390
391 let fallback = if let Some((_, any_q)) = responses.quotes.first() {
394 counter!("worker_router_orders_total", "status" => "no_route").increment(1);
395 OrderQuote::new(
396 responses.order_id.clone(),
397 QuoteStatus::NoRouteFound,
398 any_q.amount_in().clone(),
399 BigUint::ZERO,
400 BigUint::ZERO,
401 BigUint::ZERO,
402 any_q.block().clone(),
403 String::new(),
404 any_q.sender().clone(),
405 any_q.receiver().clone(),
406 any_q.solved_against().clone(),
407 )
408 } else {
409 let status = if responses.failed_solvers.is_empty() {
411 QuoteStatus::NoRouteFound
412 } else {
413 let all_timeouts = responses
416 .failed_solvers
417 .iter()
418 .all(|(_, e)| matches!(e, SolveError::Timeout { .. }));
419 let all_not_ready = responses
420 .failed_solvers
421 .iter()
422 .all(|(_, e)| matches!(e, SolveError::NotReady(_)));
423 if all_timeouts {
424 QuoteStatus::Timeout
425 } else if all_not_ready {
426 QuoteStatus::NotReady
427 } else {
428 QuoteStatus::NoRouteFound
429 }
430 };
431
432 let status_label = match status {
434 QuoteStatus::Timeout => "timeout",
435 QuoteStatus::NotReady => "not_ready",
436 _ => "no_route",
437 };
438 counter!("worker_router_orders_total", "status" => status_label).increment(1);
439
440 let label = options
443 .state_label()
444 .cloned()
445 .unwrap_or_else(|| "0".to_string());
446 OrderQuote::new(
447 responses.order_id.clone(),
448 status,
449 BigUint::ZERO,
450 BigUint::ZERO,
451 BigUint::ZERO,
452 BigUint::ZERO,
453 BlockInfo::new(0, String::new(), 0),
454 String::new(),
455 Bytes::default(),
456 Bytes::default(),
457 label,
458 )
459 };
460 vec![fallback]
461 }
462
463 fn effective_timeout(&self, options: &QuoteOptions) -> Duration {
465 options
466 .timeout_ms()
467 .map(Duration::from_millis)
468 .unwrap_or(self.config.default_timeout())
469 }
470}
471
472fn refine_gas_estimates(
473 order_responses: &mut Vec<OrderResponses>,
474 encoding_options: &EncodingOptions,
475) -> Result<(), SolveError> {
476 for responses in order_responses {
477 for (_, quote) in &mut responses.quotes {
478 if quote.status() != QuoteStatus::Success {
479 continue;
480 }
481 let solution = Solution::try_from(&*quote)?
482 .with_user_transfer_type(encoding_options.transfer_type().clone());
483 let refined_gas = estimate_gas_usage(&solution, derive_strategy(quote));
484 let naive_gas = quote.gas_estimate().clone();
485 if naive_gas > BigUint::ZERO {
486 let gas_cost_in_token_out = quote.amount_out() - quote.amount_out_net_gas();
487 let new_gas_cost = &gas_cost_in_token_out * &refined_gas / &naive_gas;
488 let new_net = if new_gas_cost <= *quote.amount_out() {
489 quote.amount_out() - &new_gas_cost
490 } else {
491 BigUint::ZERO
492 };
493 quote.set_amount_out_net_gas(new_net);
494 quote.set_gas_estimate(refined_gas);
495 }
496 }
497 }
498 Ok(())
499}
500
501fn derive_strategy(quote: &OrderQuote) -> Strategy {
502 let Some(route) = quote.route() else { return Strategy::Single };
503 let swaps = route.swaps();
504 if swaps.len() == 1 {
505 Strategy::Single
506 } else if swaps.iter().any(|s| *s.split() > 0.0) {
507 Strategy::Split
508 } else {
509 Strategy::Sequential
510 }
511}
512
513#[cfg(test)]
514mod tests {
515 use std::collections::HashMap;
516
517 use rstest::rstest;
518 use tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry;
519 use tycho_simulation::{
520 tycho_common::models::Chain,
521 tycho_core::{
522 models::{token::Token, Address, Chain as SimChain},
523 Bytes,
524 },
525 };
526
527 use super::*;
528 use crate::{
529 algorithm::test_utils::{component, MockProtocolSim},
530 types::internal::SolveTask,
531 EncodingOptions, OrderSide, Route, SingleOrderQuote, Swap,
532 };
533
534 fn default_encoder() -> Encoder {
535 let registry = SwapEncoderRegistry::new(Chain::Ethereum)
536 .add_default_encoders(None)
537 .expect("default encoders should always succeed");
538 let encoder =
539 Encoder::new(Chain::Ethereum, registry).expect("encoder creation should succeed");
540 encoder
542 .router_fees()
543 .set(crate::encoding::router_fees::RouterFees::new(
544 100_000_000,
545 100_000,
546 20_000_000,
547 std::collections::HashMap::new(),
548 ));
549 encoder
550 }
551
552 fn make_address(byte: u8) -> Address {
553 Address::from([byte; 20])
554 }
555
556 fn make_order() -> Order {
557 Order::new(
558 make_address(0x01),
559 make_address(0x02),
560 BigUint::from(1000u64),
561 OrderSide::Sell,
562 make_address(0xAA),
563 )
564 .with_id("test-order".to_string())
565 }
566
567 fn make_single_quote(amount_out_net_gas: u64) -> SingleOrderQuote {
568 let make_token = |addr: Address| Token {
569 address: addr,
570 symbol: "T".to_string(),
571 decimals: 18,
572 tax: Default::default(),
573 gas: vec![],
574 chain: SimChain::Ethereum,
575 quality: 100,
576 };
577 let tin = make_address(0x01);
578 let tout = make_address(0x02);
579 let tin_token = make_token(tin.clone());
580 let tout_token = make_token(tout.clone());
581 let swap = Swap::new(
582 "pool-1".to_string(),
583 "uniswap_v2".to_string(),
584 tin.clone(),
585 tout.clone(),
586 BigUint::from(1000u64),
587 BigUint::from(990u64),
588 BigUint::from(50_000u64),
589 component(
590 "0x0000000000000000000000000000000000000001",
591 &[tin_token.clone(), tout_token.clone()],
592 ),
593 Box::new(MockProtocolSim::default()),
594 );
595 let mut tokens = HashMap::new();
596 tokens.insert(tin, tin_token);
597 tokens.insert(tout, tout_token);
598 let quote = OrderQuote::new(
599 "test-order".to_string(),
600 QuoteStatus::Success,
601 BigUint::from(1000u64),
602 BigUint::from(990u64),
603 BigUint::from(100_000u64),
604 BigUint::from(amount_out_net_gas),
605 BlockInfo::new(1, "0x123".to_string(), 1000),
606 "test".to_string(),
607 Bytes::from(make_address(0xAA).as_ref()),
608 Bytes::from(make_address(0xAA).as_ref()),
609 "1".to_string(),
610 )
611 .with_route(Route::new(vec![swap], tokens).expect("non-empty route"));
612 SingleOrderQuote::new(quote, 5)
613 }
614
615 fn create_mock_pool(
617 name: &str,
618 response: Result<SingleOrderQuote, SolveError>,
619 delay_ms: u64,
620 ) -> (SolverPoolHandle, tokio::task::JoinHandle<()>) {
621 let (tx, rx) = async_channel::bounded::<SolveTask>(10);
622 let handle = TaskQueueHandle::from_sender(tx);
623
624 let worker = tokio::spawn(async move {
625 while let Ok(task) = rx.recv().await {
626 if delay_ms > 0 {
627 tokio::time::sleep(Duration::from_millis(delay_ms)).await;
628 }
629 task.respond(response.clone());
630 }
631 });
632
633 (SolverPoolHandle::new(name, handle), worker)
634 }
635
636 #[test]
637 fn test_config_default() {
638 let config = WorkerPoolRouterConfig::default();
639 assert_eq!(config.default_timeout(), Duration::from_secs(1));
640 assert_eq!(config.min_responses(), 1);
641 }
642
643 #[test]
644 fn test_config_builder() {
645 let config = WorkerPoolRouterConfig::default()
646 .with_timeout(Duration::from_millis(500))
647 .with_min_responses(2);
648 assert_eq!(config.default_timeout(), Duration::from_millis(500));
649 assert_eq!(config.min_responses(), 2);
650 }
651
652 #[tokio::test]
653 async fn test_router_no_pools() {
654 let worker_router =
655 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
656 let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
657
658 let result = worker_router.quote(request).await;
659 assert!(matches!(result, Err(SolveError::Internal(_))));
660 }
661
662 #[tokio::test]
663 async fn test_router_single_pool_success() {
664 let (pool, worker) = create_mock_pool("pool_a", Ok(make_single_quote(900)), 0);
665
666 let worker_router =
667 WorkerPoolRouter::new(vec![pool], WorkerPoolRouterConfig::default(), default_encoder());
668 let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
669 let request = QuoteRequest::new(vec![make_order()], options);
670
671 let result = worker_router.quote(request).await;
672 assert!(result.is_ok());
673
674 let quote = result.unwrap();
675 assert_eq!(quote.orders().len(), 1);
676 assert_eq!(quote.orders()[0].status(), QuoteStatus::Success);
677 assert_eq!(*quote.orders()[0].amount_out_net_gas(), BigUint::from(873u64));
679 assert!(!quote.orders()[0]
680 .transaction()
681 .unwrap()
682 .data()
683 .is_empty());
684
685 drop(worker_router);
686 worker.abort();
687 }
688
689 #[tokio::test]
690 async fn test_router_selects_best_of_two() {
691 let (pool_a, worker_a) = create_mock_pool("pool_a", Ok(make_single_quote(800)), 0);
693 let (pool_b, worker_b) = create_mock_pool("pool_b", Ok(make_single_quote(950)), 0);
695
696 let config = WorkerPoolRouterConfig::default().with_min_responses(2);
698 let worker_router = WorkerPoolRouter::new(vec![pool_a, pool_b], config, default_encoder());
699 let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
700 let request = QuoteRequest::new(vec![make_order()], options);
701
702 let result = worker_router.quote(request).await;
703 assert!(result.is_ok());
704
705 let quote = result.unwrap();
706 assert_eq!(quote.orders().len(), 1);
707 assert_eq!(*quote.orders()[0].amount_out_net_gas(), BigUint::from(938u64));
709 assert!(!quote.orders()[0]
710 .transaction()
711 .unwrap()
712 .data()
713 .is_empty());
714
715 drop(worker_router);
716 worker_a.abort();
717 worker_b.abort();
718 }
719
720 #[tokio::test]
721 async fn test_router_timeout() {
722 let (pool, worker) = create_mock_pool("slow_pool", Ok(make_single_quote(900)), 500);
724
725 let config = WorkerPoolRouterConfig::default().with_timeout(Duration::from_millis(50));
726 let worker_router = WorkerPoolRouter::new(vec![pool], config, default_encoder());
727 let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
728
729 let result = worker_router.quote(request).await;
730 assert!(result.is_ok());
731
732 let quote = result.unwrap();
733 assert_eq!(quote.orders().len(), 1);
735 assert!(matches!(
736 quote.orders()[0].status(),
737 QuoteStatus::Timeout | QuoteStatus::NoRouteFound
738 ));
739
740 drop(worker_router);
741 worker.abort();
742 }
743
744 #[tokio::test]
745 async fn test_router_early_return_on_min_responses() {
746 let (pool_a, worker_a) = create_mock_pool("fast_pool", Ok(make_single_quote(800)), 0);
748 let (pool_b, worker_b) = create_mock_pool("slow_pool", Ok(make_single_quote(950)), 500);
750
751 let config = WorkerPoolRouterConfig::default()
752 .with_timeout(Duration::from_millis(1000))
753 .with_min_responses(1);
754 let worker_router = WorkerPoolRouter::new(vec![pool_a, pool_b], config, default_encoder());
755
756 let start = Instant::now();
757 let options = QuoteOptions::default().with_encoding_options(EncodingOptions::new(0.01));
758 let request = QuoteRequest::new(vec![make_order()], options);
759
760 let result = worker_router.quote(request).await;
761 let elapsed = start.elapsed();
762
763 assert!(result.is_ok());
764 assert!(elapsed < Duration::from_millis(200));
766
767 let quote = result.unwrap();
769 assert_eq!(quote.orders().len(), 1);
770 assert_eq!(quote.orders()[0].status(), QuoteStatus::Success);
771 assert!(!quote.orders()[0]
773 .transaction()
774 .unwrap()
775 .data()
776 .is_empty());
777
778 drop(worker_router);
779 worker_a.abort();
780 worker_b.abort();
781 }
782
783 #[rstest]
784 #[case::under_limit(100, Some(200), true)]
785 #[case::at_limit(200, Some(200), true)]
786 #[case::over_limit(300, Some(200), false)]
787 #[case::no_limit(500, None, true)]
788 fn test_max_gas_constraint(
789 #[case] gas_estimate: u64,
790 #[case] max_gas: Option<u64>,
791 #[case] should_pass: bool,
792 ) {
793 let responses = OrderResponses {
794 order_id: "test".to_string(),
795 quotes: vec![(
796 "pool".to_string(),
797 OrderQuote::new(
798 "test".to_string(),
799 QuoteStatus::Success,
800 BigUint::from(1000u64),
801 BigUint::from(990u64),
802 BigUint::from(gas_estimate),
803 BigUint::from(900u64),
804 BlockInfo::new(1, "0x123".to_string(), 1000),
805 "test".to_string(),
806 Bytes::from(make_address(0xAA).as_ref()),
807 Bytes::from(make_address(0xAA).as_ref()),
808 "1".to_string(),
809 ),
810 )],
811 failed_solvers: vec![],
812 };
813
814 let options = match max_gas {
815 Some(gas) => QuoteOptions::default().with_max_gas(BigUint::from(gas)),
816 None => QuoteOptions::default(),
817 };
818
819 let worker_router =
820 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
821 let result = worker_router.rank_quotes(&responses, &options);
822
823 if should_pass {
824 assert_eq!(result[0].status(), QuoteStatus::Success);
825 } else {
826 assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
827 }
828 }
829
830 #[tokio::test]
831 async fn test_router_captures_solver_errors() {
832 let (pool, worker) = create_mock_pool(
834 "error_pool",
835 Err(SolveError::NoRouteFound { order_id: "test-order".to_string() }),
836 0,
837 );
838
839 let worker_router =
840 WorkerPoolRouter::new(vec![pool], WorkerPoolRouterConfig::default(), default_encoder());
841 let request = QuoteRequest::new(vec![make_order()], QuoteOptions::default());
842
843 let result = worker_router.quote(request).await;
844 assert!(result.is_ok());
845
846 let quote = result.unwrap();
847 assert_eq!(quote.orders().len(), 1);
848 assert_eq!(quote.orders()[0].status(), QuoteStatus::NoRouteFound);
850
851 drop(worker_router);
852 worker.abort();
853 }
854
855 #[test]
856 fn test_rank_quotes_all_timeouts_returns_timeout_status() {
857 let responses = OrderResponses {
858 order_id: "test".to_string(),
859 quotes: vec![],
860 failed_solvers: vec![
861 ("pool_a".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
862 ("pool_b".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
863 ],
864 };
865
866 let worker_router =
867 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
868 let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
869
870 assert_eq!(result.len(), 1);
871 assert_eq!(result[0].status(), QuoteStatus::Timeout);
872 }
873
874 #[test]
875 fn test_rank_quotes_mixed_failures_returns_no_route_found() {
876 let responses = OrderResponses {
877 order_id: "test".to_string(),
878 quotes: vec![],
879 failed_solvers: vec![
880 ("pool_a".to_string(), SolveError::Timeout { elapsed_ms: 100 }),
881 ("pool_b".to_string(), SolveError::NoRouteFound { order_id: "test".to_string() }),
882 ],
883 };
884
885 let worker_router =
886 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
887 let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
888
889 assert_eq!(result.len(), 1);
890 assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
891 }
892
893 #[test]
894 fn test_rank_quotes_no_failures_returns_no_route_found() {
895 let responses =
896 OrderResponses { order_id: "test".to_string(), quotes: vec![], failed_solvers: vec![] };
897
898 let worker_router =
899 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
900 let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
901
902 assert_eq!(result.len(), 1);
903 assert_eq!(result[0].status(), QuoteStatus::NoRouteFound);
904 }
905
906 #[test]
907 fn test_rank_quotes_returns_sorted_candidates() {
908 let responses = OrderResponses {
909 order_id: "test".to_string(),
910 quotes: vec![
911 (
912 "pool_a".to_string(),
913 OrderQuote::new(
914 "test".to_string(),
915 QuoteStatus::Success,
916 BigUint::from(1000u64),
917 BigUint::from(800u64),
918 BigUint::from(100_000u64),
919 BigUint::from(800u64),
920 BlockInfo::new(1, "0x123".to_string(), 1000),
921 "test".to_string(),
922 Bytes::from(make_address(0xAA).as_ref()),
923 Bytes::from(make_address(0xAA).as_ref()),
924 "1".to_string(),
925 ),
926 ),
927 (
928 "pool_b".to_string(),
929 OrderQuote::new(
930 "test".to_string(),
931 QuoteStatus::Success,
932 BigUint::from(1000u64),
933 BigUint::from(950u64),
934 BigUint::from(100_000u64),
935 BigUint::from(950u64),
936 BlockInfo::new(1, "0x123".to_string(), 1000),
937 "test".to_string(),
938 Bytes::from(make_address(0xAA).as_ref()),
939 Bytes::from(make_address(0xAA).as_ref()),
940 "1".to_string(),
941 ),
942 ),
943 ],
944 failed_solvers: vec![],
945 };
946
947 let worker_router =
948 WorkerPoolRouter::new(vec![], WorkerPoolRouterConfig::default(), default_encoder());
949 let result = worker_router.rank_quotes(&responses, &QuoteOptions::default());
950
951 assert_eq!(result.len(), 2);
952 assert_eq!(*result[0].amount_out_net_gas(), BigUint::from(950u64));
953 assert_eq!(*result[1].amount_out_net_gas(), BigUint::from(800u64));
954 }
955}