1use std::{str::FromStr, sync::Arc, time::Duration};
14
15use num_cpus;
16use rustc_hash::FxHashSet;
17use serde::{Deserialize, Serialize};
18use tokio::{sync::broadcast, task::JoinHandle};
19use tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry;
20#[cfg(feature = "experimental")]
21use tycho_simulation::evm::stream::BlockStepController;
22#[cfg(feature = "test-utils")]
23use tycho_simulation::tycho_ethereum::gas::{BlockGasPrice, GasPrice};
24use tycho_simulation::{
25 evm::pending::PendingBlockProcessor,
26 tycho_common::{models::Chain, traits::TxDeltaIndexer, Bytes},
27 tycho_core::models::Address,
28 tycho_ethereum::rpc::EthereumRpcClient,
29};
30
31use crate::{
32 algorithm::{AlgorithmConfig, AlgorithmError, AlgorithmRegistry},
33 derived::{ComputationManager, ComputationManagerConfig, SharedDerivedDataRef},
34 encoding::{encoder::Encoder, fee_fetcher::RouterFeeFetcher, router_fees::SharedRouterFees},
35 feed::{
36 events::{MarketEvent, MarketEventHandler},
37 gas::GasPriceFetcher,
38 market_data::MarketData,
39 metrics_sampler::MetricsSampler,
40 tycho_feed::TychoFeed,
41 TychoFeedConfig,
42 },
43 graph::EdgeWeightUpdaterWithDerived,
44 price_guard::{
45 guard::PriceGuard, provider::PriceProvider, provider_registry::PriceProviderRegistry,
46 },
47 simulation::simulator::QuoteSimulator,
48 types::constants::native_token,
49 worker_pool::{
50 pool::{WorkerPool, WorkerPoolBuilder},
51 registry::UnknownAlgorithmError,
52 },
53 worker_pool_router::{
54 config::WorkerPoolRouterConfig, ExclusiveAccess, LiquidityScope, SolverPoolHandle,
55 WorkerPoolRouter,
56 },
57 Algorithm, Quote, QuoteRequest, SolveError,
58};
59
60pub mod defaults {
66 use std::time::Duration;
67
68 pub const MIN_TOKEN_QUALITY: i32 = 100;
70 pub const TRADED_N_DAYS_AGO: u64 = 3;
72 pub const TVL_BUFFER_RATIO: f64 = 1.1;
75 pub const GAS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
77 pub const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_secs(10);
79 pub const ROUTER_FEE_REFRESH_INTERVAL: Duration = Duration::from_secs(300);
81 pub const RECONNECT_DELAY: Duration = Duration::from_secs(5);
83 pub const ROUTER_MIN_RESPONSES: usize = 0;
86 pub const POOL_TASK_QUEUE_CAPACITY: usize = 1000;
88 pub const POOL_MIN_HOPS: usize = 1;
90 pub const POOL_MAX_HOPS: usize = 3;
92 pub const POOL_TIMEOUT_MS: u64 = 100;
94 pub const SIMULATION_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
96 pub const SIMULATION_LAYOUT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
103}
104
105const DEFAULT_TYCHO_USE_TLS: bool = true;
107const DEFAULT_DEPTH_SLIPPAGE_THRESHOLD: f64 = 0.01;
108const DEFAULT_ROUTER_TIMEOUT: Duration = Duration::from_secs(10);
111
112fn default_task_queue_capacity() -> usize {
115 defaults::POOL_TASK_QUEUE_CAPACITY
116}
117
118fn default_min_hops() -> usize {
119 defaults::POOL_MIN_HOPS
120}
121
122fn default_max_hops() -> usize {
123 defaults::POOL_MAX_HOPS
124}
125
126fn default_algo_timeout_ms() -> u64 {
127 defaults::POOL_TIMEOUT_MS
128}
129
130fn pricing_max_hops(pool_max_hops: impl Iterator<Item = usize>) -> usize {
136 pool_max_hops
137 .max()
138 .unwrap_or(defaults::POOL_MAX_HOPS)
139}
140
141fn parse_connector_tokens(
142 raw: Option<&[String]>,
143) -> Result<Option<FxHashSet<Address>>, SolverBuildError> {
144 let Some(strings) = raw else {
145 return Ok(None);
146 };
147 let mut set = FxHashSet::with_capacity_and_hasher(strings.len(), Default::default());
148 for s in strings {
149 let addr = Address::from_str(s).map_err(|e| AlgorithmError::InvalidConfiguration {
150 reason: format!("connector_tokens: invalid address {s:?}: {e}"),
151 })?;
152 set.insert(addr);
153 }
154 Ok(Some(set))
155}
156
157#[must_use]
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct PoolConfig {
161 algorithm: String,
163 #[serde(default = "num_cpus::get")]
165 num_workers: usize,
166 #[serde(default = "default_task_queue_capacity")]
168 task_queue_capacity: usize,
169 #[serde(default = "default_min_hops")]
171 min_hops: usize,
172 #[serde(default = "default_max_hops")]
174 max_hops: usize,
175 #[serde(default = "default_algo_timeout_ms")]
177 timeout_ms: u64,
178 #[serde(default)]
180 max_routes: Option<usize>,
181 #[serde(default)]
184 connector_tokens: Option<Vec<String>>,
185 #[serde(default)]
187 liquidity_scope: Option<LiquidityScope>,
188 #[serde(default)]
191 exclude_protocols: Option<Vec<String>>,
192}
193
194impl PoolConfig {
195 pub fn new(algorithm: impl Into<String>) -> Self {
198 Self {
199 algorithm: algorithm.into(),
200 num_workers: num_cpus::get(),
201 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
202 min_hops: defaults::POOL_MIN_HOPS,
203 max_hops: defaults::POOL_MAX_HOPS,
204 timeout_ms: defaults::POOL_TIMEOUT_MS,
205 max_routes: None,
206 connector_tokens: None,
207 liquidity_scope: None,
208 exclude_protocols: None,
209 }
210 }
211
212 pub fn algorithm(&self) -> &str {
214 &self.algorithm
215 }
216
217 pub fn liquidity_scope(&self) -> Option<LiquidityScope> {
219 self.liquidity_scope
220 }
221
222 pub fn with_liquidity_scope(mut self, scope: LiquidityScope) -> Self {
224 self.liquidity_scope = Some(scope);
225 self
226 }
227
228 pub fn exclude_protocols(&self) -> Option<&[String]> {
230 self.exclude_protocols.as_deref()
231 }
232
233 pub fn with_exclude_protocols(mut self, exclude_protocols: Vec<String>) -> Self {
237 self.exclude_protocols = Some(exclude_protocols);
238 self
239 }
240
241 pub fn num_workers(&self) -> usize {
243 self.num_workers
244 }
245
246 pub fn with_num_workers(mut self, num_workers: usize) -> Self {
248 self.num_workers = num_workers;
249 self
250 }
251
252 pub fn with_task_queue_capacity(mut self, task_queue_capacity: usize) -> Self {
254 self.task_queue_capacity = task_queue_capacity;
255 self
256 }
257
258 pub fn with_min_hops(mut self, min_hops: usize) -> Self {
260 self.min_hops = min_hops;
261 self
262 }
263
264 pub fn with_max_hops(mut self, max_hops: usize) -> Self {
266 self.max_hops = max_hops;
267 self
268 }
269
270 pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
272 self.timeout_ms = timeout_ms;
273 self
274 }
275
276 pub fn with_max_routes(mut self, max_routes: Option<usize>) -> Self {
278 self.max_routes = max_routes;
279 self
280 }
281
282 pub fn task_queue_capacity(&self) -> usize {
284 self.task_queue_capacity
285 }
286
287 pub fn min_hops(&self) -> usize {
289 self.min_hops
290 }
291
292 pub fn max_hops(&self) -> usize {
294 self.max_hops
295 }
296
297 pub fn timeout_ms(&self) -> u64 {
299 self.timeout_ms
300 }
301
302 pub fn max_routes(&self) -> Option<usize> {
304 self.max_routes
305 }
306
307 pub fn with_connector_tokens(mut self, tokens: Vec<String>) -> Self {
310 self.connector_tokens = Some(tokens);
311 self
312 }
313
314 pub fn connector_tokens(&self) -> Option<&[String]> {
316 self.connector_tokens.as_deref()
317 }
318}
319
320#[derive(Debug, thiserror::Error)]
322#[error("timed out after {timeout_ms}ms waiting for market data and derived computations")]
323pub struct WaitReadyError {
324 timeout_ms: u64,
325}
326
327#[non_exhaustive]
329#[derive(Debug, thiserror::Error)]
330pub enum SolverBuildError {
331 #[error("failed to create ethereum RPC client: {0}")]
333 RpcClient(String),
334 #[error(transparent)]
336 AlgorithmConfig(#[from] AlgorithmError),
337 #[error("failed to create computation manager: {0}")]
339 ComputationManager(String),
340 #[error("failed to create encoder: {0}")]
342 Encoder(String),
343 #[error("failed to create router fee fetcher: {0}")]
345 RouterFeeFetcher(String),
346 #[error("failed to create quote simulator: {0}")]
348 QuoteSimulator(String),
349 #[error(transparent)]
351 UnknownAlgorithm(#[from] UnknownAlgorithmError),
352 #[error("gas token not configured for chain")]
354 GasToken,
355 #[error("no worker pools configured")]
357 NoPools,
358 #[error(
362 "every worker pool sets liquidity_scope = \"include_exclusive\"; requests without \
363 exclusive access would be served by no pool. Configure at least one public_only pool"
364 )]
365 NoPublicPool,
366 #[cfg(feature = "test-utils")]
368 #[error("replay failed: {0}")]
369 Replay(String),
370 #[error("feed setup failed before delivering pending processor: {0}")]
375 FeedSetup(String),
376 #[error("pending processor channel closed before processor was delivered")]
379 PendingChannelClosed,
380 #[cfg(feature = "experimental")]
383 #[error("step controller channel closed before controller was delivered")]
384 StepControllerChannelClosed,
385}
386
387enum PoolEntry {
389 BuiltIn {
390 name: String,
391 algorithm: String,
392 num_workers: usize,
393 task_queue_capacity: usize,
394 min_hops: usize,
395 max_hops: usize,
396 timeout_ms: u64,
397 max_routes: Option<usize>,
398 connector_tokens: Option<FxHashSet<Address>>,
399 liquidity_scope: Option<LiquidityScope>,
400 exclude_protocols: Vec<String>,
401 },
402 Custom(CustomPoolEntry),
403}
404
405fn pools_over_router_timeout<'a>(
413 pools: impl IntoIterator<Item = (&'a str, Duration)>,
414 router_timeout: Duration,
415) -> Vec<(&'a str, Duration)> {
416 pools
417 .into_iter()
418 .filter(|(_, timeout)| *timeout > router_timeout)
419 .collect()
420}
421
422impl PoolEntry {
423 fn name(&self) -> &str {
425 match self {
426 PoolEntry::BuiltIn { name, .. } => name,
427 PoolEntry::Custom(custom) => &custom.name,
428 }
429 }
430
431 fn timeout(&self) -> Duration {
433 match self {
434 PoolEntry::BuiltIn { timeout_ms, .. } => Duration::from_millis(*timeout_ms),
435 PoolEntry::Custom(custom) => Duration::from_millis(custom.timeout_ms),
436 }
437 }
438
439 fn liquidity_scope(&self) -> Option<LiquidityScope> {
441 match self {
442 PoolEntry::BuiltIn { liquidity_scope, .. } => *liquidity_scope,
443 PoolEntry::Custom(custom) => custom.liquidity_scope,
444 }
445 }
446
447 fn max_hops(&self) -> usize {
449 match self {
450 PoolEntry::BuiltIn { max_hops, .. } => *max_hops,
451 PoolEntry::Custom(custom) => custom.max_hops,
452 }
453 }
454}
455
456struct CustomPoolEntry {
458 name: String,
459 num_workers: usize,
460 task_queue_capacity: usize,
461 min_hops: usize,
462 max_hops: usize,
463 timeout_ms: u64,
464 max_routes: Option<usize>,
465 liquidity_scope: Option<LiquidityScope>,
466 configure: Box<dyn FnOnce(WorkerPoolBuilder) -> WorkerPoolBuilder + Send>,
468}
469
470struct BuiltComponents {
473 tycho_feed: TychoFeed,
474 gas_price_fetcher: GasPriceFetcher<EthereumRpcClient>,
475 router_fee_fetcher: Option<RouterFeeFetcher>,
476 computation_manager: ComputationManager,
477 computation_event_rx: broadcast::Receiver<MarketEvent>,
478 computation_shutdown_tx: broadcast::Sender<()>,
479 computation_shutdown_rx: broadcast::Receiver<()>,
480 router: WorkerPoolRouter,
481 worker_pools: Vec<WorkerPool>,
482 market_data: MarketData,
483 derived_data: SharedDerivedDataRef,
484 router_fees: SharedRouterFees,
485 chain: Chain,
486 router_address: Option<Bytes>,
487 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
488 market_event_tx: broadcast::Sender<MarketEvent>,
489}
490
491#[must_use = "a builder does nothing until .build() is called"]
496pub struct FyndBuilder {
497 algorithms: AlgorithmRegistry,
499 chain: Chain,
500 tycho_url: String,
501 rpc_url: String,
502 protocols: Vec<String>,
503 min_tvl: f64,
504 tycho_api_key: Option<String>,
505 tycho_use_tls: bool,
506 min_token_quality: i32,
507 traded_n_days_ago: u64,
508 tvl_buffer_ratio: f64,
509 gas_refresh_interval: Duration,
510 reconnect_delay: Duration,
511 blocklisted_components: FxHashSet<String>,
512 partial_blocks: bool,
513 tycho_subscription_buffer_size: Option<usize>,
514 pricing_max_tokens_per_pass: Option<usize>,
517 pricing_min_pass_interval: Option<Duration>,
518 router_timeout: Duration,
519 router_min_responses: usize,
520 encoder: Option<Encoder>,
521 calldata_watermark: Option<Vec<u8>>,
522 pools: Vec<PoolEntry>,
523 price_guard_enabled: bool,
524 simulation_enabled: bool,
525 price_providers: Vec<Box<dyn PriceProvider>>,
526 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
527}
528
529impl FyndBuilder {
530 pub fn new(
532 chain: Chain,
533 tycho_url: impl Into<String>,
534 rpc_url: impl Into<String>,
535 protocols: Vec<String>,
536 min_tvl: f64,
537 ) -> Self {
538 Self {
539 algorithms: AlgorithmRegistry::new(),
540 chain,
541 tycho_url: tycho_url.into(),
542 rpc_url: rpc_url.into(),
543 protocols,
544 min_tvl,
545 tycho_api_key: None,
546 tycho_use_tls: DEFAULT_TYCHO_USE_TLS,
547 min_token_quality: defaults::MIN_TOKEN_QUALITY,
548 traded_n_days_ago: defaults::TRADED_N_DAYS_AGO,
549 tvl_buffer_ratio: defaults::TVL_BUFFER_RATIO,
550 gas_refresh_interval: defaults::GAS_REFRESH_INTERVAL,
551 reconnect_delay: defaults::RECONNECT_DELAY,
552 blocklisted_components: FxHashSet::default(),
553 partial_blocks: false,
554 tycho_subscription_buffer_size: None,
555 pricing_max_tokens_per_pass: None,
556 pricing_min_pass_interval: None,
557 router_timeout: DEFAULT_ROUTER_TIMEOUT,
558 router_min_responses: defaults::ROUTER_MIN_RESPONSES,
559 encoder: None,
560 calldata_watermark: None,
561 pools: Vec::new(),
562 price_guard_enabled: false,
563 simulation_enabled: false,
564 price_providers: Vec::new(),
565 pending_indexers: Vec::new(),
566 }
567 }
568
569 pub fn chain(&self) -> Chain {
571 self.chain
572 }
573
574 pub fn tycho_api_key(mut self, key: impl Into<String>) -> Self {
576 self.tycho_api_key = Some(key.into());
577 self
578 }
579
580 pub fn min_tvl(mut self, min_tvl: f64) -> Self {
582 self.min_tvl = min_tvl;
583 self
584 }
585
586 pub fn tycho_use_tls(mut self, use_tls: bool) -> Self {
588 self.tycho_use_tls = use_tls;
589 self
590 }
591
592 pub fn min_token_quality(mut self, quality: i32) -> Self {
595 self.min_token_quality = quality;
596 self
597 }
598
599 pub fn traded_n_days_ago(mut self, days: u64) -> Self {
601 self.traded_n_days_ago = days;
602 self
603 }
604
605 pub fn tvl_buffer_ratio(mut self, ratio: f64) -> Self {
607 self.tvl_buffer_ratio = ratio;
608 self
609 }
610
611 pub fn gas_refresh_interval(mut self, interval: Duration) -> Self {
613 self.gas_refresh_interval = interval;
614 self
615 }
616
617 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
619 self.reconnect_delay = delay;
620 self
621 }
622
623 pub fn blocklisted_components(mut self, components: impl IntoIterator<Item = String>) -> Self {
625 self.blocklisted_components = components.into_iter().collect();
626 self
627 }
628
629 pub fn partial_blocks(mut self, enabled: bool) -> Self {
635 self.partial_blocks = enabled;
636 self
637 }
638
639 pub fn tycho_subscription_buffer_size(mut self, size: usize) -> Self {
642 self.tycho_subscription_buffer_size = Some(size);
643 self
644 }
645
646 pub fn set_pricing_max_tokens_per_pass(mut self, max_tokens: usize) -> Self {
652 self.pricing_max_tokens_per_pass = Some(max_tokens);
653 self
654 }
655
656 pub fn set_pricing_min_pass_interval(mut self, interval: Duration) -> Self {
662 self.pricing_min_pass_interval = Some(interval);
663 self
664 }
665
666 pub fn worker_router_timeout(mut self, timeout: Duration) -> Self {
668 self.router_timeout = timeout;
669 self
670 }
671
672 pub fn worker_router_min_responses(mut self, min: usize) -> Self {
674 self.router_min_responses = min;
675 self
676 }
677
678 pub fn encoder(mut self, encoder: Encoder) -> Self {
680 self.encoder = Some(encoder);
681 self
682 }
683
684 pub fn calldata_watermark(mut self, watermark: impl Into<Vec<u8>>) -> Self {
688 self.calldata_watermark = Some(watermark.into());
689 self
690 }
691
692 pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
694 self.pools.push(PoolEntry::BuiltIn {
695 name: "default".to_string(),
696 algorithm: algorithm.into(),
697 num_workers: num_cpus::get(),
698 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
699 min_hops: defaults::POOL_MIN_HOPS,
700 max_hops: defaults::POOL_MAX_HOPS,
701 timeout_ms: defaults::POOL_TIMEOUT_MS,
702 max_routes: None,
703 connector_tokens: None,
704 liquidity_scope: None,
705 exclude_protocols: Vec::new(),
706 });
707 self
708 }
709
710 #[deprecated(
714 since = "0.99.23",
715 note = "register the algorithm in an `AlgorithmRegistry` and pass it to \
716 `with_algorithms`, which also serves pools that name it in a configuration \
717 file; this shorthand only ever added one pool"
718 )]
719 pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
720 where
721 A: Algorithm + 'static,
722 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
723 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
724 {
725 let name = name.into();
726 let algo_name = name.clone();
727 let configure =
728 Box::new(move |builder: WorkerPoolBuilder| builder.with_algorithm(algo_name, factory));
729 self.pools
730 .push(PoolEntry::Custom(CustomPoolEntry {
731 name,
732 num_workers: num_cpus::get(),
733 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
734 min_hops: defaults::POOL_MIN_HOPS,
735 max_hops: defaults::POOL_MAX_HOPS,
736 timeout_ms: defaults::POOL_TIMEOUT_MS,
737 max_routes: None,
738 liquidity_scope: None,
739 configure,
740 }));
741 self
742 }
743
744 pub fn with_algorithms(mut self, algorithms: AlgorithmRegistry) -> Self {
750 self.algorithms = algorithms;
751 self
752 }
753
754 pub fn add_default_price_providers(self) -> Self {
761 self.register_price_provider(Box::new(
762 crate::price_guard::hyperliquid::HyperliquidProvider::default(),
763 ))
764 .register_price_provider(Box::new(
765 crate::price_guard::binance_ws::BinanceWsProvider::default(),
766 ))
767 }
768
769 pub fn register_price_provider(mut self, provider: Box<dyn PriceProvider>) -> Self {
774 self.price_providers.push(provider);
775 self
776 }
777
778 pub fn with_pending_indexer(
784 mut self,
785 extractor: impl Into<String>,
786 indexer: Box<dyn TxDeltaIndexer>,
787 ) -> Self {
788 self.pending_indexers
789 .push((extractor.into(), indexer));
790 self
791 }
792
793 pub fn price_guard_enabled(mut self, enabled: bool) -> Self {
800 self.price_guard_enabled = enabled;
801 self
802 }
803
804 pub fn simulation_enabled(mut self, enabled: bool) -> Self {
808 self.simulation_enabled = enabled;
809 self
810 }
811
812 pub fn add_pool(
819 mut self,
820 name: impl Into<String>,
821 config: &PoolConfig,
822 ) -> Result<Self, SolverBuildError> {
823 let connector_tokens = parse_connector_tokens(config.connector_tokens())?;
824 self.pools.push(PoolEntry::BuiltIn {
825 name: name.into(),
826 algorithm: config.algorithm().to_string(),
827 num_workers: config.num_workers(),
828 task_queue_capacity: config.task_queue_capacity(),
829 min_hops: config.min_hops(),
830 max_hops: config.max_hops(),
831 timeout_ms: config.timeout_ms(),
832 max_routes: config.max_routes(),
833 connector_tokens,
834 liquidity_scope: config.liquidity_scope(),
835 exclude_protocols: config
836 .exclude_protocols()
837 .map(<[String]>::to_vec)
838 .unwrap_or_default(),
839 });
840 Ok(self)
841 }
842
843 fn assemble_components(mut self) -> Result<BuiltComponents, SolverBuildError> {
846 if self.pools.is_empty() {
847 return Err(SolverBuildError::NoPools);
848 }
849
850 if self
854 .pools
855 .iter()
856 .all(|p| p.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
857 {
858 return Err(SolverBuildError::NoPublicPool);
859 }
860
861 for (name, timeout) in pools_over_router_timeout(
864 self.pools
865 .iter()
866 .map(|pool| (pool.name(), pool.timeout())),
867 self.router_timeout,
868 ) {
869 tracing::warn!(
870 pool = name,
871 pool_timeout_ms = timeout.as_millis() as u64,
872 router_timeout_ms = self.router_timeout.as_millis() as u64,
873 "worker pool solve budget is longer than the router deadline; the router stops \
874 waiting first, so the extra budget is only reachable for a request that raises \
875 its own timeout_ms"
876 );
877 }
878
879 if self.price_providers.is_empty() {
881 self = self.add_default_price_providers();
882 }
883
884 let market_data = MarketData::new_shared();
885
886 let tycho_feed_config = TychoFeedConfig::new(
887 self.tycho_url,
888 self.chain,
889 self.tycho_api_key,
890 self.tycho_use_tls,
891 self.protocols,
892 self.min_tvl,
893 )
894 .tvl_buffer_ratio(self.tvl_buffer_ratio)
895 .reconnect_delay(self.reconnect_delay)
896 .min_token_quality(self.min_token_quality)
897 .traded_n_days_ago(self.traded_n_days_ago)
898 .blocklisted_components(self.blocklisted_components)
899 .partial_blocks(self.partial_blocks);
900
901 let tycho_feed_config = match self.tycho_subscription_buffer_size {
902 Some(size) => tycho_feed_config.subscription_buffer_size(size),
903 None => tycho_feed_config,
904 };
905
906 let ethereum_client = EthereumRpcClient::new(self.rpc_url.as_str())
907 .map_err(|e| SolverBuildError::RpcClient(e.to_string()))?;
908
909 let gas_price_fetcher =
910 GasPriceFetcher::new(ethereum_client, market_data.clone(), self.gas_refresh_interval);
911
912 let tycho_feed = TychoFeed::new(tycho_feed_config, market_data.clone());
913 let market_event_tx = tycho_feed.event_sender();
914
915 let gas_token = native_token(&self.chain).map_err(|_| SolverBuildError::GasToken)?;
916 let pricing_max_hops = pricing_max_hops(
917 self.pools
918 .iter()
919 .map(PoolEntry::max_hops),
920 );
921 let mut computation_config = ComputationManagerConfig::new()
922 .with_gas_token(gas_token)
923 .with_max_hop(pricing_max_hops)
924 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
925 if let Some(max_tokens) = self.pricing_max_tokens_per_pass {
926 computation_config = computation_config.with_pricing_max_tokens_per_pass(max_tokens);
927 }
928 if let Some(interval) = self.pricing_min_pass_interval {
929 computation_config = computation_config.with_pricing_min_pass_interval(interval);
930 }
931 let (computation_manager, _) =
934 ComputationManager::new(computation_config, market_data.clone())
935 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
936
937 let derived_data: SharedDerivedDataRef = computation_manager.store();
938 let derived_event_tx = computation_manager.event_sender();
939
940 let computation_event_rx = tycho_feed.subscribe();
943 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
944
945 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
946 let mut worker_pools: Vec<WorkerPool> = Vec::new();
947
948 let pools = std::mem::take(&mut self.pools);
949
950 for pool_entry in pools {
951 let pool_event_rx = tycho_feed.subscribe();
952 let derived_rx = derived_event_tx.subscribe();
953
954 let pool_scope = pool_entry
955 .liquidity_scope()
956 .unwrap_or_default();
957
958 let (worker_pool, task_handle) = match pool_entry {
959 PoolEntry::BuiltIn {
960 name,
961 algorithm,
962 num_workers,
963 task_queue_capacity,
964 min_hops,
965 max_hops,
966 timeout_ms,
967 max_routes,
968 connector_tokens,
969 liquidity_scope: _,
970 exclude_protocols,
971 } => {
972 let mut algo_cfg = AlgorithmConfig::new(
973 min_hops,
974 max_hops,
975 Duration::from_millis(timeout_ms),
976 max_routes,
977 )?;
978 if let Some(tokens) = connector_tokens {
979 algo_cfg = algo_cfg.with_connector_tokens(tokens);
980 }
981 let named = WorkerPoolBuilder::new()
982 .name(name)
983 .algorithm_config(algo_cfg)
984 .num_workers(num_workers)
985 .task_queue_capacity(task_queue_capacity)
986 .liquidity_scope(pool_scope)
987 .exclude_protocols(exclude_protocols);
988 let builder = self
989 .algorithms
990 .configure(&algorithm, named)?;
991 builder.build(
992 market_data.clone(),
993 Arc::clone(&derived_data),
994 pool_event_rx,
995 derived_rx,
996 )?
997 }
998 PoolEntry::Custom(custom) => {
999 let algo_cfg = AlgorithmConfig::new(
1000 custom.min_hops,
1001 custom.max_hops,
1002 Duration::from_millis(custom.timeout_ms),
1003 custom.max_routes,
1004 )?;
1005 let builder = WorkerPoolBuilder::new()
1006 .name(custom.name)
1007 .algorithm_config(algo_cfg)
1008 .num_workers(custom.num_workers)
1009 .task_queue_capacity(custom.task_queue_capacity)
1010 .liquidity_scope(pool_scope);
1011 let builder = (custom.configure)(builder);
1012 builder.build(
1013 market_data.clone(),
1014 Arc::clone(&derived_data),
1015 pool_event_rx,
1016 derived_rx,
1017 )?
1018 }
1019 };
1020
1021 solver_pool_handles.push(
1022 SolverPoolHandle::new(worker_pool.name(), task_handle)
1023 .with_liquidity_scope(pool_scope),
1024 );
1025 worker_pools.push(worker_pool);
1026 }
1027
1028 let encoder = match self.encoder {
1029 Some(enc) => enc,
1030 None => {
1031 let registry = SwapEncoderRegistry::new(self.chain)
1032 .add_default_encoders(None)
1033 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1034 Encoder::new(self.chain, registry)
1035 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1036 }
1037 };
1038 let encoder = match self.calldata_watermark {
1039 Some(watermark) => encoder.with_calldata_watermark(watermark),
1040 None => encoder,
1041 };
1042
1043 let chain = self.chain;
1044 let router_address = encoder.router_address().cloned();
1045 let router_fees = encoder.router_fees();
1046
1047 let router_fee_fetcher = match &router_address {
1048 Some(addr) => Some(
1049 RouterFeeFetcher::new(
1050 self.rpc_url.as_str(),
1051 addr,
1052 router_fees.clone(),
1053 defaults::ROUTER_FEE_REFRESH_INTERVAL,
1054 )
1055 .map_err(|e| SolverBuildError::RouterFeeFetcher(e.to_string()))?,
1056 ),
1057 None => {
1058 tracing::warn!(
1059 %chain,
1060 "no Tycho router for this chain; running quote-only (encoding disabled)"
1061 );
1062 None
1063 }
1064 };
1065
1066 let quote_simulator = if self.simulation_enabled {
1067 Some(
1068 QuoteSimulator::new(
1069 self.rpc_url.as_str(),
1070 chain,
1071 defaults::SIMULATION_REQUEST_TIMEOUT,
1072 )
1073 .map_err(|error| SolverBuildError::QuoteSimulator(error.to_string()))?,
1074 )
1075 } else {
1076 None
1077 };
1078
1079 let router_config = WorkerPoolRouterConfig::default()
1082 .with_timeout(self.router_timeout)
1083 .with_min_responses(self.router_min_responses);
1084 let mut router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1085 if let Some(simulator) = quote_simulator {
1086 router = router.with_simulator(simulator);
1087 }
1088
1089 if self.price_guard_enabled {
1090 let mut registry = PriceProviderRegistry::new();
1091 let mut worker_handles = Vec::new();
1092 for mut provider in self.price_providers {
1093 worker_handles.push(provider.start(market_data.clone()));
1094 registry = registry.register(provider);
1095 }
1096 let price_guard = PriceGuard::new(registry, worker_handles);
1097 router = router.with_price_guard(price_guard);
1098 }
1099
1100 Ok(BuiltComponents {
1101 tycho_feed,
1102 gas_price_fetcher,
1103 router_fee_fetcher,
1104 computation_manager,
1105 computation_event_rx,
1106 computation_shutdown_tx,
1107 computation_shutdown_rx,
1108 router,
1109 worker_pools,
1110 market_data,
1111 derived_data,
1112 router_fees,
1113 chain,
1114 router_address,
1115 pending_indexers: self.pending_indexers,
1116 market_event_tx,
1117 })
1118 }
1119
1120 pub fn build(self) -> Result<Solver, SolverBuildError> {
1126 let mut c = self.assemble_components()?;
1127
1128 let feed_handle = tokio::spawn(async move {
1129 if let Err(e) = c.tycho_feed.run().await {
1130 metrics::counter!("tycho_feed_failures_total").increment(1);
1131 tracing::error!(error = %e, "tycho feed error");
1132 }
1133 });
1134 let gas_price_handle = tokio::spawn(async move {
1135 c.gas_price_fetcher.run().await;
1136 });
1137 let metrics_sampler =
1138 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1139 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1140 let router_fee_handle = match c.router_fee_fetcher {
1141 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1142 None => tokio::spawn(async {}),
1143 };
1144 let computation_handle = tokio::spawn(async move {
1145 c.computation_manager
1146 .run(c.computation_event_rx, c.computation_shutdown_rx)
1147 .await;
1148 });
1149
1150 Ok(Solver {
1151 router: c.router,
1152 worker_pools: c.worker_pools,
1153 market_data: c.market_data,
1154 derived_data: c.derived_data,
1155 router_fees: c.router_fees,
1156 feed_handle,
1157 gas_price_handle,
1158 metrics_sampler_handle,
1159 router_fee_handle,
1160 computation_handle,
1161 computation_shutdown_tx: c.computation_shutdown_tx,
1162 chain: c.chain,
1163 router_address: c.router_address,
1164 market_event_tx: c.market_event_tx,
1165 })
1166 }
1167
1168 pub async fn build_with_pending(
1180 self,
1181 ) -> Result<(Solver, PendingBlockProcessor), SolverBuildError> {
1182 let mut c = self.assemble_components()?;
1183
1184 let (pending_tx, pending_rx) =
1185 tokio::sync::oneshot::channel::<Result<PendingBlockProcessor, String>>();
1186
1187 let pending_indexers = c.pending_indexers;
1188 let feed_handle = tokio::spawn(async move {
1189 if let Err(e) = c
1190 .tycho_feed
1191 .run_with_pending(pending_tx, pending_indexers)
1192 .await
1193 {
1194 metrics::counter!("tycho_feed_failures_total").increment(1);
1195 tracing::error!(error = %e, "tycho feed error");
1196 }
1197 });
1198 let gas_price_handle = tokio::spawn(async move {
1199 c.gas_price_fetcher.run().await;
1200 });
1201 let metrics_sampler =
1202 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1203 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1204 let router_fee_handle = match c.router_fee_fetcher {
1205 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1206 None => tokio::spawn(async {}),
1207 };
1208 let computation_handle = tokio::spawn(async move {
1209 c.computation_manager
1210 .run(c.computation_event_rx, c.computation_shutdown_rx)
1211 .await;
1212 });
1213
1214 let pending = pending_rx
1215 .await
1216 .map_err(|_| SolverBuildError::PendingChannelClosed)?
1217 .map_err(SolverBuildError::FeedSetup)?;
1218
1219 Ok((
1220 Solver {
1221 router: c.router,
1222 worker_pools: c.worker_pools,
1223 market_data: c.market_data,
1224 derived_data: c.derived_data,
1225 router_fees: c.router_fees,
1226 feed_handle,
1227 gas_price_handle,
1228 metrics_sampler_handle,
1229 router_fee_handle,
1230 computation_handle,
1231 computation_shutdown_tx: c.computation_shutdown_tx,
1232 chain: c.chain,
1233 router_address: c.router_address,
1234 market_event_tx: c.market_event_tx,
1235 },
1236 pending,
1237 ))
1238 }
1239
1240 #[cfg(feature = "experimental")]
1255 pub async fn build_with_step_controller(
1256 self,
1257 ) -> Result<(Solver, BlockStepController), SolverBuildError> {
1258 let mut c = self.assemble_components()?;
1259
1260 let (controller_tx, controller_rx) =
1261 tokio::sync::oneshot::channel::<Result<BlockStepController, String>>();
1262
1263 let feed_handle = tokio::spawn(async move {
1264 if let Err(e) = c
1265 .tycho_feed
1266 .run_with_step_controller(controller_tx)
1267 .await
1268 {
1269 tracing::error!(error = %e, "tycho feed error");
1270 }
1271 });
1272 let gas_price_handle = tokio::spawn(async move {
1273 c.gas_price_fetcher.run().await;
1274 });
1275 let metrics_sampler =
1276 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1277 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1278 let router_fee_handle = match c.router_fee_fetcher {
1279 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1280 None => tokio::spawn(async {}),
1281 };
1282 let computation_handle = tokio::spawn(async move {
1283 c.computation_manager
1284 .run(c.computation_event_rx, c.computation_shutdown_rx)
1285 .await;
1286 });
1287
1288 let controller = controller_rx
1289 .await
1290 .map_err(|_| SolverBuildError::StepControllerChannelClosed)?
1291 .map_err(SolverBuildError::FeedSetup)?;
1292
1293 Ok((
1294 Solver {
1295 router: c.router,
1296 worker_pools: c.worker_pools,
1297 market_data: c.market_data,
1298 derived_data: c.derived_data,
1299 router_fees: c.router_fees,
1300 feed_handle,
1301 gas_price_handle,
1302 metrics_sampler_handle,
1303 router_fee_handle,
1304 computation_handle,
1305 computation_shutdown_tx: c.computation_shutdown_tx,
1306 chain: c.chain,
1307 router_address: c.router_address,
1308 market_event_tx: c.market_event_tx,
1309 },
1310 controller,
1311 ))
1312 }
1313} pub struct Solver {
1317 router: WorkerPoolRouter,
1318 worker_pools: Vec<WorkerPool>,
1319 market_data: MarketData,
1320 derived_data: SharedDerivedDataRef,
1321 router_fees: SharedRouterFees,
1322 feed_handle: JoinHandle<()>,
1323 gas_price_handle: JoinHandle<()>,
1324 metrics_sampler_handle: JoinHandle<()>,
1325 router_fee_handle: JoinHandle<()>,
1326 computation_handle: JoinHandle<()>,
1327 computation_shutdown_tx: broadcast::Sender<()>,
1328 chain: Chain,
1329 router_address: Option<Bytes>,
1330 market_event_tx: broadcast::Sender<MarketEvent>,
1331}
1332
1333impl Solver {
1334 pub fn market_data(&self) -> MarketData {
1336 self.market_data.clone()
1337 }
1338
1339 pub fn router_address(&self) -> Option<&Bytes> {
1341 self.router_address.as_ref()
1342 }
1343
1344 pub fn derived_data(&self) -> SharedDerivedDataRef {
1346 Arc::clone(&self.derived_data)
1347 }
1348
1349 pub fn subscribe_market_events(&self) -> broadcast::Receiver<crate::feed::events::MarketEvent> {
1354 self.market_event_tx.subscribe()
1355 }
1356
1357 pub async fn quote(&self, request: QuoteRequest) -> Result<Quote, SolveError> {
1367 self.router
1368 .quote(request, ExclusiveAccess::Granted)
1369 .await
1370 }
1371
1372 pub async fn wait_until_ready(&self, timeout: Duration) -> Result<(), WaitReadyError> {
1389 const POLL_INTERVAL: Duration = Duration::from_millis(500);
1390
1391 let deadline = tokio::time::Instant::now() + timeout;
1392
1393 loop {
1394 let market_ready = self
1395 .market_data
1396 .read()
1397 .await
1398 .last_updated()
1399 .is_some();
1400 let derived_ready = self
1401 .derived_data
1402 .read()
1403 .await
1404 .derived_data_ready();
1405
1406 if market_ready && derived_ready {
1407 return Ok(());
1408 }
1409
1410 if tokio::time::Instant::now() >= deadline {
1411 return Err(WaitReadyError { timeout_ms: timeout.as_millis() as u64 });
1412 }
1413
1414 tokio::time::sleep(POLL_INTERVAL).await;
1415 }
1416 }
1417
1418 #[cfg(feature = "test-utils")]
1430 pub async fn from_recording(
1431 chain: Chain,
1432 updates: Vec<tycho_simulation::protocol::models::Update>,
1433 pools: std::collections::HashMap<String, PoolConfig>,
1434 gas_price_wei: Option<num_bigint::BigUint>,
1435 ) -> Result<Self, SolverBuildError> {
1436 Self::from_recording_with(chain, updates, pools, gas_price_wei, &AlgorithmRegistry::new())
1437 .await
1438 }
1439
1440 #[cfg(feature = "test-utils")]
1452 pub async fn from_recording_with(
1453 chain: Chain,
1454 updates: Vec<tycho_simulation::protocol::models::Update>,
1455 pools: std::collections::HashMap<String, PoolConfig>,
1456 gas_price_wei: Option<num_bigint::BigUint>,
1457 algorithms: &AlgorithmRegistry,
1458 ) -> Result<Self, SolverBuildError> {
1459 if pools.is_empty() {
1460 return Err(SolverBuildError::NoPools);
1461 }
1462 if pools
1463 .values()
1464 .all(|pool| pool.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
1465 {
1466 return Err(SolverBuildError::NoPublicPool);
1467 }
1468
1469 let market_data = MarketData::new_shared();
1470
1471 let feed_config =
1473 TychoFeedConfig::new("ws://replay".to_string(), chain, None, false, vec![], 0.0);
1474 let feed = TychoFeed::new(feed_config, market_data.clone());
1475 let market_event_tx = feed.event_sender();
1476 let _feed_rx = feed.subscribe();
1477
1478 for update in updates {
1479 feed.handle_tycho_message(update)
1480 .await
1481 .map_err(|e| SolverBuildError::Replay(e.to_string()))?;
1482 }
1483
1484 let gas_price = match gas_price_wei {
1486 Some(price) => price,
1487 None => {
1488 tracing::warn!("no recorded gas price, defaulting to 10 gwei");
1489 num_bigint::BigUint::from(10_000_000_000u64)
1490 }
1491 };
1492 let block_number = match market_data.read().await.last_updated() {
1493 Some(block) => block.number(),
1494 None => {
1495 tracing::warn!("no block number from replayed updates, defaulting to 0");
1496 0
1497 }
1498 };
1499 {
1500 let mut market = market_data.write().await;
1501 market.update_gas_price(BlockGasPrice {
1502 block_number,
1503 block_hash: Default::default(),
1504 block_timestamp: 0,
1505 pricing: GasPrice::Legacy { gas_price },
1506 });
1507 }
1508
1509 let gas_token = native_token(&chain).map_err(|_| SolverBuildError::GasToken)?;
1510 let pricing_max_hops = pricing_max_hops(
1511 pools
1512 .values()
1513 .map(|pool_cfg| pool_cfg.max_hops()),
1514 );
1515 let computation_config = ComputationManagerConfig::new()
1516 .with_gas_token(gas_token)
1517 .with_max_hop(pricing_max_hops)
1518 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD)
1519 .with_pricing_pass_budget(Duration::from_secs(24 * 60 * 60))
1524 .with_pricing_max_tokens_per_pass(usize::MAX)
1525 .with_pricing_min_pass_interval(Duration::ZERO);
1526 let (computation_manager, _) =
1527 ComputationManager::new(computation_config, market_data.clone())
1528 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
1529
1530 let derived_data: SharedDerivedDataRef = computation_manager.store();
1531 let derived_event_tx = computation_manager.event_sender();
1532
1533 let computation_event_rx = feed.subscribe();
1534 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
1535
1536 let computation_handle = tokio::spawn(async move {
1537 computation_manager
1538 .run(computation_event_rx, computation_shutdown_rx)
1539 .await;
1540 });
1541
1542 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
1544 let mut worker_pools: Vec<WorkerPool> = Vec::new();
1545 let mut max_timeout_ms = 0u64;
1546
1547 for (name, pool_cfg) in &pools {
1548 let mut algo_cfg = AlgorithmConfig::new(
1549 pool_cfg.min_hops(),
1550 pool_cfg.max_hops(),
1551 Duration::from_millis(pool_cfg.timeout_ms()),
1552 pool_cfg.max_routes(),
1553 )?;
1554 if let Some(tokens) = parse_connector_tokens(pool_cfg.connector_tokens())? {
1555 algo_cfg = algo_cfg.with_connector_tokens(tokens);
1556 }
1557
1558 let pool_event_rx = feed.subscribe();
1559 let derived_rx = derived_event_tx.subscribe();
1560
1561 let named = WorkerPoolBuilder::new()
1562 .name(name.clone())
1563 .algorithm_config(algo_cfg)
1564 .num_workers(pool_cfg.num_workers())
1565 .task_queue_capacity(pool_cfg.task_queue_capacity())
1566 .liquidity_scope(
1567 pool_cfg
1568 .liquidity_scope()
1569 .unwrap_or_default(),
1570 );
1571 let (worker_pool, task_handle) = algorithms
1572 .configure(pool_cfg.algorithm(), named)?
1573 .build(market_data.clone(), Arc::clone(&derived_data), pool_event_rx, derived_rx)?;
1574
1575 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
1576 max_timeout_ms = max_timeout_ms.max(pool_cfg.timeout_ms());
1577 worker_pools.push(worker_pool);
1578 }
1579
1580 let encoder = {
1582 let registry = SwapEncoderRegistry::new(chain)
1583 .add_default_encoders(None)
1584 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1585 Encoder::new(chain, registry).map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1586 };
1587
1588 let router_address = encoder.router_address().cloned();
1589 let router_fees = encoder.router_fees();
1593 router_fees.set(crate::encoding::router_fees::RouterFees::new(
1594 100_000_000,
1595 0,
1596 0,
1597 rustc_hash::FxHashMap::default(),
1598 ));
1599 let router_config = WorkerPoolRouterConfig::default()
1600 .with_timeout(Duration::from_millis(max_timeout_ms.max(5000)))
1601 .with_min_responses(defaults::ROUTER_MIN_RESPONSES);
1602 let router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1603
1604 let market_read = market_data.read().await;
1606 let added = market_read.component_topology();
1607 drop(market_read);
1608
1609 if market_event_tx
1610 .send(MarketEvent::MarketUpdated {
1611 added_components: added,
1612 removed_components: vec![],
1613 updated_components: vec![],
1614 })
1615 .is_err()
1616 {
1617 tracing::warn!("no receivers for initial MarketUpdated broadcast");
1618 }
1619
1620 let feed_handle = tokio::spawn(futures::future::pending::<()>());
1623 let gas_price_handle = tokio::spawn(async { });
1624 let metrics_sampler_handle = tokio::spawn(async { });
1625 let router_fee_handle = tokio::spawn(async { });
1626
1627 Ok(Solver {
1628 router,
1629 worker_pools,
1630 market_data,
1631 derived_data,
1632 router_fees,
1633 feed_handle,
1634 gas_price_handle,
1635 metrics_sampler_handle,
1636 router_fee_handle,
1637 computation_handle,
1638 computation_shutdown_tx,
1639 chain,
1640 router_address,
1641 market_event_tx,
1642 })
1643 }
1644
1645 pub fn shutdown(self) {
1647 let _ = self.computation_shutdown_tx.send(());
1648 for pool in self.worker_pools {
1649 pool.shutdown();
1650 }
1651 self.feed_handle.abort();
1652 self.gas_price_handle.abort();
1653 self.metrics_sampler_handle.abort();
1654 self.router_fee_handle.abort();
1655 }
1656
1657 pub fn into_parts(self) -> SolverParts {
1659 SolverParts {
1660 router: self.router,
1661 worker_pools: self.worker_pools,
1662 market_data: self.market_data,
1663 derived_data: self.derived_data,
1664 router_fees: self.router_fees,
1665 feed_handle: self.feed_handle,
1666 gas_price_handle: self.gas_price_handle,
1667 metrics_sampler_handle: self.metrics_sampler_handle,
1668 router_fee_handle: self.router_fee_handle,
1669 computation_handle: self.computation_handle,
1670 computation_shutdown_tx: self.computation_shutdown_tx,
1671 chain: self.chain,
1672 router_address: self.router_address,
1673 }
1674 }
1675}
1676
1677pub struct SolverParts {
1681 router: WorkerPoolRouter,
1683 worker_pools: Vec<WorkerPool>,
1685 market_data: MarketData,
1687 derived_data: SharedDerivedDataRef,
1689 router_fees: SharedRouterFees,
1691 feed_handle: JoinHandle<()>,
1693 gas_price_handle: JoinHandle<()>,
1695 metrics_sampler_handle: JoinHandle<()>,
1697 router_fee_handle: JoinHandle<()>,
1699 computation_handle: JoinHandle<()>,
1701 computation_shutdown_tx: broadcast::Sender<()>,
1703 chain: Chain,
1705 router_address: Option<Bytes>,
1707}
1708
1709impl SolverParts {
1710 pub fn chain(&self) -> Chain {
1712 self.chain
1713 }
1714
1715 pub fn router_address(&self) -> Option<&Bytes> {
1717 self.router_address.as_ref()
1718 }
1719
1720 pub fn worker_pools(&self) -> &[WorkerPool] {
1722 &self.worker_pools
1723 }
1724
1725 pub fn market_data(&self) -> &MarketData {
1727 &self.market_data
1728 }
1729
1730 pub fn derived_data(&self) -> &SharedDerivedDataRef {
1732 &self.derived_data
1733 }
1734
1735 pub fn router_fees(&self) -> &SharedRouterFees {
1737 &self.router_fees
1738 }
1739
1740 pub fn into_router(self) -> WorkerPoolRouter {
1742 self.router
1743 }
1744
1745 #[allow(clippy::type_complexity)]
1747 pub fn into_components(
1748 self,
1749 ) -> (
1750 WorkerPoolRouter,
1751 Vec<WorkerPool>,
1752 MarketData,
1753 SharedDerivedDataRef,
1754 JoinHandle<()>,
1755 JoinHandle<()>,
1756 JoinHandle<()>,
1757 JoinHandle<()>,
1758 JoinHandle<()>,
1759 broadcast::Sender<()>,
1760 ) {
1761 (
1762 self.router,
1763 self.worker_pools,
1764 self.market_data,
1765 self.derived_data,
1766 self.feed_handle,
1767 self.gas_price_handle,
1768 self.metrics_sampler_handle,
1769 self.router_fee_handle,
1770 self.computation_handle,
1771 self.computation_shutdown_tx,
1772 )
1773 }
1774}
1775
1776#[cfg(test)]
1777mod tests {
1778 use super::*;
1779
1780 #[test]
1783 fn test_unscoped_pool_resolves_to_public_only() {
1784 let config = PoolConfig::new("most_liquid");
1785 assert_eq!(config.liquidity_scope(), None);
1786 assert_eq!(
1787 config
1788 .liquidity_scope()
1789 .unwrap_or_default(),
1790 LiquidityScope::PublicOnly
1791 );
1792 }
1793
1794 #[test]
1797 fn test_build_all_exclusive_pools() {
1798 let config =
1799 PoolConfig::new("most_liquid").with_liquidity_scope(LiquidityScope::IncludeExclusive);
1800 let result = FyndBuilder::new(
1801 Chain::Ethereum,
1802 "wss://example.invalid",
1803 "https://example.invalid",
1804 vec!["uniswap_v2".to_string()],
1805 100.0,
1806 )
1807 .add_pool("exclusive", &config)
1808 .expect("add_pool should accept the config")
1809 .build();
1810
1811 assert!(matches!(result, Err(SolverBuildError::NoPublicPool)));
1812 }
1813
1814 #[test]
1817 fn test_pools_over_router_timeout_names_the_offenders() {
1818 let over = pools_over_router_timeout(
1819 [("deep", Duration::from_millis(1000)), ("shallow", Duration::from_millis(50))],
1820 Duration::from_millis(100),
1821 );
1822 assert_eq!(over, vec![("deep", Duration::from_millis(1000))]);
1823 }
1824
1825 #[test]
1827 fn test_pool_budget_equal_to_the_router_timeout_is_not_reported() {
1828 let over = pools_over_router_timeout(
1829 [("exact", Duration::from_millis(100))],
1830 Duration::from_millis(100),
1831 );
1832 assert!(over.is_empty());
1833 }
1834}