1use std::{str::FromStr, sync::Arc, time::Duration};
14
15use num_cpus;
16use rustc_hash::FxHashSet;
17use serde::{Deserialize, Serialize};
18use tokio::{
19 sync::broadcast,
20 task::{AbortHandle, JoinHandle},
21};
22use tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry;
23#[cfg(feature = "experimental")]
24use tycho_simulation::evm::stream::BlockStepController;
25#[cfg(feature = "test-utils")]
26use tycho_simulation::tycho_ethereum::gas::{BlockGasPrice, GasPrice};
27use tycho_simulation::{
28 evm::pending::PendingBlockProcessor,
29 tycho_common::{models::Chain, traits::TxDeltaIndexer, Bytes},
30 tycho_core::models::Address,
31 tycho_ethereum::rpc::EthereumRpcClient,
32};
33
34use crate::{
35 algorithm::{AlgorithmConfig, AlgorithmError, AlgorithmRegistry},
36 derived::{ComputationManager, ComputationManagerConfig, SharedDerivedDataRef},
37 encoding::{encoder::Encoder, fee_fetcher::RouterFeeFetcher, router_fees::SharedRouterFees},
38 feed::{
39 events::{MarketEvent, MarketEventHandler},
40 gas::GasPriceFetcher,
41 market_data::MarketData,
42 metrics_sampler::MetricsSampler,
43 tycho_feed::TychoFeed,
44 TychoFeedConfig,
45 },
46 graph::EdgeWeightUpdaterWithDerived,
47 price_guard::{
48 guard::PriceGuard, provider::PriceProvider, provider_registry::PriceProviderRegistry,
49 },
50 propamm_fallback::{
51 fee_tier_fetcher::FeeTierFetcher, SharedFeeTiers, PROPAMM_ROUTER_ADDRESS, PROPAMM_VENUES,
52 },
53 simulation::simulator::QuoteSimulator,
54 types::constants::native_token,
55 worker_pool::{
56 pool::{WorkerPool, WorkerPoolBuilder},
57 registry::UnknownAlgorithmError,
58 },
59 worker_pool_router::{
60 config::WorkerPoolRouterConfig, ExclusiveAccess, LiquidityScope, SolverPoolHandle,
61 WorkerPoolRouter,
62 },
63 Algorithm, Quote, QuoteRequest, SolveError,
64};
65
66pub mod defaults {
72 use std::time::Duration;
73
74 pub const MIN_TOKEN_QUALITY: i32 = 100;
76 pub const TRADED_N_DAYS_AGO: u64 = 3;
78 pub const TVL_BUFFER_RATIO: f64 = 1.1;
81 pub const GAS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
83 pub const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_secs(10);
85 pub const ROUTER_FEE_REFRESH_INTERVAL: Duration = Duration::from_secs(300);
87 pub const FALLBACK_FEE_TIER_REFRESH_INTERVAL: Duration = Duration::from_secs(600);
90 pub const RECONNECT_DELAY: Duration = Duration::from_secs(5);
92 pub const ROUTER_MIN_RESPONSES: usize = 0;
95 pub const POOL_TASK_QUEUE_CAPACITY: usize = 1000;
97 pub const POOL_MIN_HOPS: usize = 1;
99 pub const POOL_MAX_HOPS: usize = 3;
101 pub const POOL_TIMEOUT_MS: u64 = 100;
103 pub const SIMULATION_REQUEST_TIMEOUT: Duration = Duration::from_secs(3);
105 pub const SIMULATION_LAYOUT_DISCOVERY_TIMEOUT: Duration = Duration::from_secs(5);
112}
113
114const DEFAULT_TYCHO_USE_TLS: bool = true;
116const DEFAULT_DEPTH_SLIPPAGE_THRESHOLD: f64 = 0.01;
117const DEFAULT_ROUTER_TIMEOUT: Duration = Duration::from_secs(10);
120
121fn default_task_queue_capacity() -> usize {
124 defaults::POOL_TASK_QUEUE_CAPACITY
125}
126
127fn default_min_hops() -> usize {
128 defaults::POOL_MIN_HOPS
129}
130
131fn default_max_hops() -> usize {
132 defaults::POOL_MAX_HOPS
133}
134
135fn default_algo_timeout_ms() -> u64 {
136 defaults::POOL_TIMEOUT_MS
137}
138
139fn propamm_fee_tier_fetcher(
151 rpc_url: &str,
152 fallback_fee_tiers: SharedFeeTiers,
153) -> Result<FeeTierFetcher, SolverBuildError> {
154 let router = Bytes::from_str(PROPAMM_ROUTER_ADDRESS).map_err(|e| {
155 SolverBuildError::FeeTierFetcher(format!(
156 "PropAMMRouter address {PROPAMM_ROUTER_ADDRESS}: {e}"
157 ))
158 })?;
159 let venues = PROPAMM_VENUES
160 .iter()
161 .map(|venue| {
162 Bytes::from_str(venue)
163 .map_err(|e| SolverBuildError::FeeTierFetcher(format!("pAMM venue {venue}: {e}")))
164 })
165 .collect::<Result<Vec<Bytes>, _>>()?;
166 FeeTierFetcher::new(
167 rpc_url,
168 &router,
169 &venues,
170 fallback_fee_tiers,
171 defaults::FALLBACK_FEE_TIER_REFRESH_INTERVAL,
172 )
173 .map_err(|e| SolverBuildError::FeeTierFetcher(e.to_string()))
174}
175
176fn pricing_max_hops(pool_max_hops: impl Iterator<Item = usize>) -> usize {
182 pool_max_hops
183 .max()
184 .unwrap_or(defaults::POOL_MAX_HOPS)
185}
186
187fn parse_connector_tokens(
188 raw: Option<&[String]>,
189) -> Result<Option<FxHashSet<Address>>, SolverBuildError> {
190 let Some(strings) = raw else {
191 return Ok(None);
192 };
193 let mut set = FxHashSet::with_capacity_and_hasher(strings.len(), Default::default());
194 for s in strings {
195 let addr = Address::from_str(s).map_err(|e| AlgorithmError::InvalidConfiguration {
196 reason: format!("connector_tokens: invalid address {s:?}: {e}"),
197 })?;
198 set.insert(addr);
199 }
200 Ok(Some(set))
201}
202
203#[must_use]
205#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct PoolConfig {
207 algorithm: String,
209 #[serde(default = "num_cpus::get")]
211 num_workers: usize,
212 #[serde(default = "default_task_queue_capacity")]
214 task_queue_capacity: usize,
215 #[serde(default = "default_min_hops")]
217 min_hops: usize,
218 #[serde(default = "default_max_hops")]
220 max_hops: usize,
221 #[serde(default = "default_algo_timeout_ms")]
223 timeout_ms: u64,
224 #[serde(default)]
226 max_routes: Option<usize>,
227 #[serde(default)]
230 connector_tokens: Option<Vec<String>>,
231 #[serde(default)]
233 liquidity_scope: Option<LiquidityScope>,
234 #[serde(default)]
237 exclude_protocols: Option<Vec<String>>,
238}
239
240impl PoolConfig {
241 pub fn new(algorithm: impl Into<String>) -> Self {
244 Self {
245 algorithm: algorithm.into(),
246 num_workers: num_cpus::get(),
247 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
248 min_hops: defaults::POOL_MIN_HOPS,
249 max_hops: defaults::POOL_MAX_HOPS,
250 timeout_ms: defaults::POOL_TIMEOUT_MS,
251 max_routes: None,
252 connector_tokens: None,
253 liquidity_scope: None,
254 exclude_protocols: None,
255 }
256 }
257
258 pub fn algorithm(&self) -> &str {
260 &self.algorithm
261 }
262
263 pub fn liquidity_scope(&self) -> Option<LiquidityScope> {
265 self.liquidity_scope
266 }
267
268 pub fn with_liquidity_scope(mut self, scope: LiquidityScope) -> Self {
270 self.liquidity_scope = Some(scope);
271 self
272 }
273
274 pub fn exclude_protocols(&self) -> Option<&[String]> {
276 self.exclude_protocols.as_deref()
277 }
278
279 pub fn with_exclude_protocols(mut self, exclude_protocols: Vec<String>) -> Self {
283 self.exclude_protocols = Some(exclude_protocols);
284 self
285 }
286
287 pub fn num_workers(&self) -> usize {
289 self.num_workers
290 }
291
292 pub fn with_num_workers(mut self, num_workers: usize) -> Self {
294 self.num_workers = num_workers;
295 self
296 }
297
298 pub fn with_task_queue_capacity(mut self, task_queue_capacity: usize) -> Self {
300 self.task_queue_capacity = task_queue_capacity;
301 self
302 }
303
304 pub fn with_min_hops(mut self, min_hops: usize) -> Self {
306 self.min_hops = min_hops;
307 self
308 }
309
310 pub fn with_max_hops(mut self, max_hops: usize) -> Self {
312 self.max_hops = max_hops;
313 self
314 }
315
316 pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
318 self.timeout_ms = timeout_ms;
319 self
320 }
321
322 pub fn with_max_routes(mut self, max_routes: Option<usize>) -> Self {
324 self.max_routes = max_routes;
325 self
326 }
327
328 pub fn task_queue_capacity(&self) -> usize {
330 self.task_queue_capacity
331 }
332
333 pub fn min_hops(&self) -> usize {
335 self.min_hops
336 }
337
338 pub fn max_hops(&self) -> usize {
340 self.max_hops
341 }
342
343 pub fn timeout_ms(&self) -> u64 {
345 self.timeout_ms
346 }
347
348 pub fn max_routes(&self) -> Option<usize> {
350 self.max_routes
351 }
352
353 pub fn with_connector_tokens(mut self, tokens: Vec<String>) -> Self {
356 self.connector_tokens = Some(tokens);
357 self
358 }
359
360 pub fn connector_tokens(&self) -> Option<&[String]> {
362 self.connector_tokens.as_deref()
363 }
364}
365
366#[derive(Debug, thiserror::Error)]
368#[error("timed out after {timeout_ms}ms waiting for market data and derived computations")]
369pub struct WaitReadyError {
370 timeout_ms: u64,
371}
372
373#[non_exhaustive]
375#[derive(Debug, thiserror::Error)]
376pub enum SolverBuildError {
377 #[error("failed to create ethereum RPC client: {0}")]
379 RpcClient(String),
380 #[error(transparent)]
382 AlgorithmConfig(#[from] AlgorithmError),
383 #[error("failed to create computation manager: {0}")]
385 ComputationManager(String),
386 #[error("failed to create encoder: {0}")]
388 Encoder(String),
389 #[error("failed to create router fee fetcher: {0}")]
391 RouterFeeFetcher(String),
392 #[error("failed to create quote simulator: {0}")]
394 QuoteSimulator(String),
395 #[error("failed to create fallback fee tier fetcher: {0}")]
398 FeeTierFetcher(String),
399 #[error(transparent)]
401 UnknownAlgorithm(#[from] UnknownAlgorithmError),
402 #[error("gas token not configured for chain")]
404 GasToken,
405 #[error("no worker pools configured")]
407 NoPools,
408 #[error(
412 "every worker pool sets liquidity_scope = \"include_exclusive\"; requests without \
413 exclusive access would be served by no pool. Configure at least one public_only pool"
414 )]
415 NoPublicPool,
416 #[cfg(feature = "test-utils")]
418 #[error("replay failed: {0}")]
419 Replay(String),
420 #[error("feed setup failed before delivering pending processor: {0}")]
425 FeedSetup(String),
426 #[error("pending processor channel closed before processor was delivered")]
429 PendingChannelClosed,
430 #[cfg(feature = "experimental")]
433 #[error("step controller channel closed before controller was delivered")]
434 StepControllerChannelClosed,
435}
436
437enum PoolEntry {
439 BuiltIn {
440 name: String,
441 algorithm: String,
442 num_workers: usize,
443 task_queue_capacity: usize,
444 min_hops: usize,
445 max_hops: usize,
446 timeout_ms: u64,
447 max_routes: Option<usize>,
448 connector_tokens: Option<FxHashSet<Address>>,
449 liquidity_scope: Option<LiquidityScope>,
450 exclude_protocols: Vec<String>,
451 },
452 Custom(CustomPoolEntry),
453}
454
455impl PoolEntry {
456 fn liquidity_scope(&self) -> Option<LiquidityScope> {
458 match self {
459 PoolEntry::BuiltIn { liquidity_scope, .. } => *liquidity_scope,
460 PoolEntry::Custom(custom) => custom.liquidity_scope,
461 }
462 }
463
464 fn max_hops(&self) -> usize {
466 match self {
467 PoolEntry::BuiltIn { max_hops, .. } => *max_hops,
468 PoolEntry::Custom(custom) => custom.max_hops,
469 }
470 }
471}
472
473struct CustomPoolEntry {
475 name: String,
476 num_workers: usize,
477 task_queue_capacity: usize,
478 min_hops: usize,
479 max_hops: usize,
480 timeout_ms: u64,
481 max_routes: Option<usize>,
482 liquidity_scope: Option<LiquidityScope>,
483 configure: Box<dyn FnOnce(WorkerPoolBuilder) -> WorkerPoolBuilder + Send>,
485}
486
487struct BuiltComponents {
490 tycho_feed: TychoFeed,
491 gas_price_fetcher: GasPriceFetcher<EthereumRpcClient>,
492 router_fee_fetcher: Option<RouterFeeFetcher>,
493 fee_tier_fetcher: Option<FeeTierFetcher>,
494 computation_manager: ComputationManager,
495 computation_event_rx: broadcast::Receiver<MarketEvent>,
496 computation_shutdown_tx: broadcast::Sender<()>,
497 computation_shutdown_rx: broadcast::Receiver<()>,
498 router: WorkerPoolRouter,
499 worker_pools: Vec<WorkerPool>,
500 market_data: MarketData,
501 derived_data: SharedDerivedDataRef,
502 router_fees: SharedRouterFees,
503 chain: Chain,
504 router_address: Option<Bytes>,
505 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
506 market_event_tx: broadcast::Sender<MarketEvent>,
507}
508
509#[must_use = "a builder does nothing until .build() is called"]
514pub struct FyndBuilder {
515 algorithms: AlgorithmRegistry,
517 chain: Chain,
518 tycho_url: String,
519 rpc_url: String,
520 protocols: Vec<String>,
521 min_tvl: f64,
522 tycho_api_key: Option<String>,
523 tycho_use_tls: bool,
524 min_token_quality: i32,
525 traded_n_days_ago: u64,
526 tvl_buffer_ratio: f64,
527 gas_refresh_interval: Duration,
528 reconnect_delay: Duration,
529 blocklisted_components: FxHashSet<String>,
530 partial_blocks: bool,
531 tycho_subscription_buffer_size: Option<usize>,
532 router_timeout: Duration,
533 router_min_responses: usize,
534 encoder: Option<Encoder>,
535 calldata_watermark: Option<Vec<u8>>,
536 pools: Vec<PoolEntry>,
537 price_guard_enabled: bool,
538 simulation_enabled: bool,
539 price_providers: Vec<Box<dyn PriceProvider>>,
540 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
541}
542
543impl FyndBuilder {
544 pub fn new(
546 chain: Chain,
547 tycho_url: impl Into<String>,
548 rpc_url: impl Into<String>,
549 protocols: Vec<String>,
550 min_tvl: f64,
551 ) -> Self {
552 Self {
553 algorithms: AlgorithmRegistry::new(),
554 chain,
555 tycho_url: tycho_url.into(),
556 rpc_url: rpc_url.into(),
557 protocols,
558 min_tvl,
559 tycho_api_key: None,
560 tycho_use_tls: DEFAULT_TYCHO_USE_TLS,
561 min_token_quality: defaults::MIN_TOKEN_QUALITY,
562 traded_n_days_ago: defaults::TRADED_N_DAYS_AGO,
563 tvl_buffer_ratio: defaults::TVL_BUFFER_RATIO,
564 gas_refresh_interval: defaults::GAS_REFRESH_INTERVAL,
565 reconnect_delay: defaults::RECONNECT_DELAY,
566 blocklisted_components: FxHashSet::default(),
567 partial_blocks: false,
568 tycho_subscription_buffer_size: None,
569 router_timeout: DEFAULT_ROUTER_TIMEOUT,
570 router_min_responses: defaults::ROUTER_MIN_RESPONSES,
571 encoder: None,
572 calldata_watermark: None,
573 pools: Vec::new(),
574 price_guard_enabled: false,
575 simulation_enabled: false,
576 price_providers: Vec::new(),
577 pending_indexers: Vec::new(),
578 }
579 }
580
581 pub fn chain(&self) -> Chain {
583 self.chain
584 }
585
586 pub fn tycho_api_key(mut self, key: impl Into<String>) -> Self {
588 self.tycho_api_key = Some(key.into());
589 self
590 }
591
592 pub fn min_tvl(mut self, min_tvl: f64) -> Self {
594 self.min_tvl = min_tvl;
595 self
596 }
597
598 pub fn tycho_use_tls(mut self, use_tls: bool) -> Self {
600 self.tycho_use_tls = use_tls;
601 self
602 }
603
604 pub fn min_token_quality(mut self, quality: i32) -> Self {
607 self.min_token_quality = quality;
608 self
609 }
610
611 pub fn traded_n_days_ago(mut self, days: u64) -> Self {
613 self.traded_n_days_ago = days;
614 self
615 }
616
617 pub fn tvl_buffer_ratio(mut self, ratio: f64) -> Self {
619 self.tvl_buffer_ratio = ratio;
620 self
621 }
622
623 pub fn gas_refresh_interval(mut self, interval: Duration) -> Self {
625 self.gas_refresh_interval = interval;
626 self
627 }
628
629 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
631 self.reconnect_delay = delay;
632 self
633 }
634
635 pub fn blocklisted_components(mut self, components: impl IntoIterator<Item = String>) -> Self {
637 self.blocklisted_components = components.into_iter().collect();
638 self
639 }
640
641 pub fn partial_blocks(mut self, enabled: bool) -> Self {
647 self.partial_blocks = enabled;
648 self
649 }
650
651 pub fn tycho_subscription_buffer_size(mut self, size: usize) -> Self {
654 self.tycho_subscription_buffer_size = Some(size);
655 self
656 }
657
658 pub fn worker_router_timeout(mut self, timeout: Duration) -> Self {
660 self.router_timeout = timeout;
661 self
662 }
663
664 pub fn worker_router_min_responses(mut self, min: usize) -> Self {
666 self.router_min_responses = min;
667 self
668 }
669
670 pub fn encoder(mut self, encoder: Encoder) -> Self {
672 self.encoder = Some(encoder);
673 self
674 }
675
676 pub fn calldata_watermark(mut self, watermark: impl Into<Vec<u8>>) -> Self {
680 self.calldata_watermark = Some(watermark.into());
681 self
682 }
683
684 pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
686 self.pools.push(PoolEntry::BuiltIn {
687 name: "default".to_string(),
688 algorithm: algorithm.into(),
689 num_workers: num_cpus::get(),
690 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
691 min_hops: defaults::POOL_MIN_HOPS,
692 max_hops: defaults::POOL_MAX_HOPS,
693 timeout_ms: defaults::POOL_TIMEOUT_MS,
694 max_routes: None,
695 connector_tokens: None,
696 liquidity_scope: None,
697 exclude_protocols: Vec::new(),
698 });
699 self
700 }
701
702 #[deprecated(
706 since = "0.99.23",
707 note = "register the algorithm in an `AlgorithmRegistry` and pass it to \
708 `with_algorithms`, which also serves pools that name it in a configuration \
709 file; this shorthand only ever added one pool"
710 )]
711 pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
712 where
713 A: Algorithm + 'static,
714 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
715 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
716 {
717 let name = name.into();
718 let algo_name = name.clone();
719 let configure =
720 Box::new(move |builder: WorkerPoolBuilder| builder.with_algorithm(algo_name, factory));
721 self.pools
722 .push(PoolEntry::Custom(CustomPoolEntry {
723 name,
724 num_workers: num_cpus::get(),
725 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
726 min_hops: defaults::POOL_MIN_HOPS,
727 max_hops: defaults::POOL_MAX_HOPS,
728 timeout_ms: defaults::POOL_TIMEOUT_MS,
729 max_routes: None,
730 liquidity_scope: None,
731 configure,
732 }));
733 self
734 }
735
736 pub fn with_algorithms(mut self, algorithms: AlgorithmRegistry) -> Self {
742 self.algorithms = algorithms;
743 self
744 }
745
746 pub fn add_default_price_providers(self) -> Self {
753 self.register_price_provider(Box::new(
754 crate::price_guard::hyperliquid::HyperliquidProvider::default(),
755 ))
756 .register_price_provider(Box::new(
757 crate::price_guard::binance_ws::BinanceWsProvider::default(),
758 ))
759 }
760
761 pub fn register_price_provider(mut self, provider: Box<dyn PriceProvider>) -> Self {
766 self.price_providers.push(provider);
767 self
768 }
769
770 pub fn with_pending_indexer(
776 mut self,
777 extractor: impl Into<String>,
778 indexer: Box<dyn TxDeltaIndexer>,
779 ) -> Self {
780 self.pending_indexers
781 .push((extractor.into(), indexer));
782 self
783 }
784
785 pub fn price_guard_enabled(mut self, enabled: bool) -> Self {
792 self.price_guard_enabled = enabled;
793 self
794 }
795
796 pub fn simulation_enabled(mut self, enabled: bool) -> Self {
800 self.simulation_enabled = enabled;
801 self
802 }
803
804 pub fn add_pool(
811 mut self,
812 name: impl Into<String>,
813 config: &PoolConfig,
814 ) -> Result<Self, SolverBuildError> {
815 let connector_tokens = parse_connector_tokens(config.connector_tokens())?;
816 self.pools.push(PoolEntry::BuiltIn {
817 name: name.into(),
818 algorithm: config.algorithm().to_string(),
819 num_workers: config.num_workers(),
820 task_queue_capacity: config.task_queue_capacity(),
821 min_hops: config.min_hops(),
822 max_hops: config.max_hops(),
823 timeout_ms: config.timeout_ms(),
824 max_routes: config.max_routes(),
825 connector_tokens,
826 liquidity_scope: config.liquidity_scope(),
827 exclude_protocols: config
828 .exclude_protocols()
829 .map(<[String]>::to_vec)
830 .unwrap_or_default(),
831 });
832 Ok(self)
833 }
834
835 fn assemble_components(mut self) -> Result<BuiltComponents, SolverBuildError> {
838 if self.pools.is_empty() {
839 return Err(SolverBuildError::NoPools);
840 }
841
842 if self
846 .pools
847 .iter()
848 .all(|p| p.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
849 {
850 return Err(SolverBuildError::NoPublicPool);
851 }
852
853 if self.price_providers.is_empty() {
855 self = self.add_default_price_providers();
856 }
857
858 let market_data = MarketData::new_shared();
859
860 let tycho_feed_config = TychoFeedConfig::new(
861 self.tycho_url,
862 self.chain,
863 self.tycho_api_key,
864 self.tycho_use_tls,
865 self.protocols,
866 self.min_tvl,
867 )
868 .tvl_buffer_ratio(self.tvl_buffer_ratio)
869 .reconnect_delay(self.reconnect_delay)
870 .min_token_quality(self.min_token_quality)
871 .traded_n_days_ago(self.traded_n_days_ago)
872 .blocklisted_components(self.blocklisted_components)
873 .partial_blocks(self.partial_blocks);
874
875 let tycho_feed_config = match self.tycho_subscription_buffer_size {
876 Some(size) => tycho_feed_config.subscription_buffer_size(size),
877 None => tycho_feed_config,
878 };
879
880 let ethereum_client = EthereumRpcClient::new(self.rpc_url.as_str())
881 .map_err(|e| SolverBuildError::RpcClient(e.to_string()))?;
882
883 let gas_price_fetcher =
884 GasPriceFetcher::new(ethereum_client, market_data.clone(), self.gas_refresh_interval);
885
886 let tycho_feed = TychoFeed::new(tycho_feed_config, market_data.clone());
887 let market_event_tx = tycho_feed.event_sender();
888
889 let gas_token = native_token(&self.chain).map_err(|_| SolverBuildError::GasToken)?;
890 let pricing_max_hops = pricing_max_hops(
891 self.pools
892 .iter()
893 .map(PoolEntry::max_hops),
894 );
895 let computation_config = ComputationManagerConfig::new()
896 .with_gas_token(gas_token)
897 .with_max_hop(pricing_max_hops)
898 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
899 let (computation_manager, _) =
902 ComputationManager::new(computation_config, market_data.clone())
903 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
904
905 let derived_data: SharedDerivedDataRef = computation_manager.store();
906 let derived_event_tx = computation_manager.event_sender();
907
908 let computation_event_rx = tycho_feed.subscribe();
911 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
912
913 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
914 let mut worker_pools: Vec<WorkerPool> = Vec::new();
915 let fallback_fee_tiers = SharedFeeTiers::default();
917
918 let pools = std::mem::take(&mut self.pools);
919
920 for pool_entry in pools {
921 let pool_event_rx = tycho_feed.subscribe();
922 let derived_rx = derived_event_tx.subscribe();
923
924 let pool_scope = pool_entry
925 .liquidity_scope()
926 .unwrap_or_default();
927
928 let (worker_pool, task_handle) = match pool_entry {
929 PoolEntry::BuiltIn {
930 name,
931 algorithm,
932 num_workers,
933 task_queue_capacity,
934 min_hops,
935 max_hops,
936 timeout_ms,
937 max_routes,
938 connector_tokens,
939 liquidity_scope: _,
940 exclude_protocols,
941 } => {
942 let mut algo_cfg = AlgorithmConfig::new(
943 min_hops,
944 max_hops,
945 Duration::from_millis(timeout_ms),
946 max_routes,
947 )?;
948 if let Some(tokens) = connector_tokens {
949 algo_cfg = algo_cfg.with_connector_tokens(tokens);
950 }
951 let named = WorkerPoolBuilder::new()
952 .name(name)
953 .algorithm_config(algo_cfg)
954 .num_workers(num_workers)
955 .task_queue_capacity(task_queue_capacity)
956 .liquidity_scope(pool_scope)
957 .exclude_protocols(exclude_protocols)
958 .fallback_fee_tiers(fallback_fee_tiers.clone());
959 let builder = self
960 .algorithms
961 .configure(&algorithm, named)?;
962 builder.build(
963 market_data.clone(),
964 Arc::clone(&derived_data),
965 pool_event_rx,
966 derived_rx,
967 )?
968 }
969 PoolEntry::Custom(custom) => {
970 let algo_cfg = AlgorithmConfig::new(
971 custom.min_hops,
972 custom.max_hops,
973 Duration::from_millis(custom.timeout_ms),
974 custom.max_routes,
975 )?;
976 let builder = WorkerPoolBuilder::new()
977 .name(custom.name)
978 .algorithm_config(algo_cfg)
979 .num_workers(custom.num_workers)
980 .task_queue_capacity(custom.task_queue_capacity)
981 .liquidity_scope(pool_scope)
982 .fallback_fee_tiers(fallback_fee_tiers.clone());
983 let builder = (custom.configure)(builder);
984 builder.build(
985 market_data.clone(),
986 Arc::clone(&derived_data),
987 pool_event_rx,
988 derived_rx,
989 )?
990 }
991 };
992
993 solver_pool_handles.push(
994 SolverPoolHandle::new(worker_pool.name(), task_handle)
995 .with_liquidity_scope(pool_scope),
996 );
997 worker_pools.push(worker_pool);
998 }
999
1000 let encoder = match self.encoder {
1001 Some(enc) => enc,
1002 None => {
1003 let registry = SwapEncoderRegistry::new(self.chain)
1004 .add_default_encoders(None)
1005 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1006 Encoder::new(self.chain, registry)
1007 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1008 }
1009 };
1010 let encoder = match self.calldata_watermark {
1011 Some(watermark) => encoder.with_calldata_watermark(watermark),
1012 None => encoder,
1013 };
1014
1015 let chain = self.chain;
1016 let router_address = encoder.router_address().cloned();
1017 let router_fees = encoder.router_fees();
1018
1019 let router_fee_fetcher = match &router_address {
1020 Some(addr) => Some(
1021 RouterFeeFetcher::new(
1022 self.rpc_url.as_str(),
1023 addr,
1024 router_fees.clone(),
1025 defaults::ROUTER_FEE_REFRESH_INTERVAL,
1026 )
1027 .map_err(|e| SolverBuildError::RouterFeeFetcher(e.to_string()))?,
1028 ),
1029 None => {
1030 tracing::warn!(
1031 %chain,
1032 "no Tycho router for this chain; running quote-only (encoding disabled)"
1033 );
1034 None
1035 }
1036 };
1037
1038 let quote_simulator = if self.simulation_enabled {
1039 Some(
1040 QuoteSimulator::new(
1041 self.rpc_url.as_str(),
1042 chain,
1043 defaults::SIMULATION_REQUEST_TIMEOUT,
1044 )
1045 .map_err(|error| SolverBuildError::QuoteSimulator(error.to_string()))?,
1046 )
1047 } else {
1048 None
1049 };
1050
1051 let fee_tier_fetcher = if chain == Chain::Ethereum {
1057 Some(propamm_fee_tier_fetcher(self.rpc_url.as_str(), fallback_fee_tiers.clone())?)
1058 } else {
1059 None
1060 };
1061
1062 let router_config = WorkerPoolRouterConfig::default()
1065 .with_timeout(self.router_timeout)
1066 .with_min_responses(self.router_min_responses);
1067 let mut router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1068 if let Some(simulator) = quote_simulator {
1069 router = router.with_simulator(simulator);
1070 }
1071
1072 if self.price_guard_enabled {
1073 let mut registry = PriceProviderRegistry::new();
1074 let mut worker_handles = Vec::new();
1075 for mut provider in self.price_providers {
1076 worker_handles.push(provider.start(market_data.clone()));
1077 registry = registry.register(provider);
1078 }
1079 let price_guard = PriceGuard::new(registry, worker_handles);
1080 router = router.with_price_guard(price_guard);
1081 }
1082
1083 Ok(BuiltComponents {
1084 tycho_feed,
1085 gas_price_fetcher,
1086 router_fee_fetcher,
1087 fee_tier_fetcher,
1088 computation_manager,
1089 computation_event_rx,
1090 computation_shutdown_tx,
1091 computation_shutdown_rx,
1092 router,
1093 worker_pools,
1094 market_data,
1095 derived_data,
1096 router_fees,
1097 chain,
1098 router_address,
1099 pending_indexers: self.pending_indexers,
1100 market_event_tx,
1101 })
1102 }
1103
1104 pub fn build(self) -> Result<Solver, SolverBuildError> {
1110 let mut c = self.assemble_components()?;
1111
1112 let feed_handle = tokio::spawn(async move {
1113 if let Err(e) = c.tycho_feed.run().await {
1114 metrics::counter!("tycho_feed_failures_total").increment(1);
1115 tracing::error!(error = %e, "tycho feed error");
1116 }
1117 });
1118 let gas_price_handle = tokio::spawn(async move {
1119 c.gas_price_fetcher.run().await;
1120 });
1121 let metrics_sampler =
1122 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1123 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1124 let router_fee_handle = match c.router_fee_fetcher {
1125 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1126 None => tokio::spawn(async {}),
1127 };
1128 let fee_tier_handle = match c.fee_tier_fetcher {
1129 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1130 None => tokio::spawn(async {}),
1131 };
1132 let computation_handle = tokio::spawn(async move {
1133 c.computation_manager
1134 .run(c.computation_event_rx, c.computation_shutdown_rx)
1135 .await;
1136 });
1137
1138 Ok(Solver {
1139 router: c.router,
1140 worker_pools: c.worker_pools,
1141 market_data: c.market_data,
1142 derived_data: c.derived_data,
1143 router_fees: c.router_fees,
1144 feed_handle,
1145 gas_price_handle,
1146 metrics_sampler_handle,
1147 router_fee_handle,
1148 fee_tier_handle,
1149 computation_handle,
1150 computation_shutdown_tx: c.computation_shutdown_tx,
1151 chain: c.chain,
1152 router_address: c.router_address,
1153 market_event_tx: c.market_event_tx,
1154 })
1155 }
1156
1157 pub async fn build_with_pending(
1169 self,
1170 ) -> Result<(Solver, PendingBlockProcessor), SolverBuildError> {
1171 let mut c = self.assemble_components()?;
1172
1173 let (pending_tx, pending_rx) =
1174 tokio::sync::oneshot::channel::<Result<PendingBlockProcessor, String>>();
1175
1176 let pending_indexers = c.pending_indexers;
1177 let feed_handle = tokio::spawn(async move {
1178 if let Err(e) = c
1179 .tycho_feed
1180 .run_with_pending(pending_tx, pending_indexers)
1181 .await
1182 {
1183 metrics::counter!("tycho_feed_failures_total").increment(1);
1184 tracing::error!(error = %e, "tycho feed error");
1185 }
1186 });
1187 let gas_price_handle = tokio::spawn(async move {
1188 c.gas_price_fetcher.run().await;
1189 });
1190 let metrics_sampler =
1191 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1192 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1193 let router_fee_handle = match c.router_fee_fetcher {
1194 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1195 None => tokio::spawn(async {}),
1196 };
1197 let fee_tier_handle = match c.fee_tier_fetcher {
1198 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1199 None => tokio::spawn(async {}),
1200 };
1201 let computation_handle = tokio::spawn(async move {
1202 c.computation_manager
1203 .run(c.computation_event_rx, c.computation_shutdown_rx)
1204 .await;
1205 });
1206
1207 let pending = pending_rx
1208 .await
1209 .map_err(|_| SolverBuildError::PendingChannelClosed)?
1210 .map_err(SolverBuildError::FeedSetup)?;
1211
1212 Ok((
1213 Solver {
1214 router: c.router,
1215 worker_pools: c.worker_pools,
1216 market_data: c.market_data,
1217 derived_data: c.derived_data,
1218 router_fees: c.router_fees,
1219 feed_handle,
1220 gas_price_handle,
1221 metrics_sampler_handle,
1222 router_fee_handle,
1223 fee_tier_handle,
1224 computation_handle,
1225 computation_shutdown_tx: c.computation_shutdown_tx,
1226 chain: c.chain,
1227 router_address: c.router_address,
1228 market_event_tx: c.market_event_tx,
1229 },
1230 pending,
1231 ))
1232 }
1233
1234 #[cfg(feature = "experimental")]
1249 pub async fn build_with_step_controller(
1250 self,
1251 ) -> Result<(Solver, BlockStepController), SolverBuildError> {
1252 let mut c = self.assemble_components()?;
1253
1254 let (controller_tx, controller_rx) =
1255 tokio::sync::oneshot::channel::<Result<BlockStepController, String>>();
1256
1257 let feed_handle = tokio::spawn(async move {
1258 if let Err(e) = c
1259 .tycho_feed
1260 .run_with_step_controller(controller_tx)
1261 .await
1262 {
1263 tracing::error!(error = %e, "tycho feed error");
1264 }
1265 });
1266 let gas_price_handle = tokio::spawn(async move {
1267 c.gas_price_fetcher.run().await;
1268 });
1269 let metrics_sampler =
1270 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1271 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1272 let router_fee_handle = match c.router_fee_fetcher {
1273 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1274 None => tokio::spawn(async {}),
1275 };
1276 let fee_tier_handle = match c.fee_tier_fetcher {
1277 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1278 None => tokio::spawn(async {}),
1279 };
1280 let computation_handle = tokio::spawn(async move {
1281 c.computation_manager
1282 .run(c.computation_event_rx, c.computation_shutdown_rx)
1283 .await;
1284 });
1285
1286 let controller = controller_rx
1287 .await
1288 .map_err(|_| SolverBuildError::StepControllerChannelClosed)?
1289 .map_err(SolverBuildError::FeedSetup)?;
1290
1291 Ok((
1292 Solver {
1293 router: c.router,
1294 worker_pools: c.worker_pools,
1295 market_data: c.market_data,
1296 derived_data: c.derived_data,
1297 router_fees: c.router_fees,
1298 feed_handle,
1299 gas_price_handle,
1300 metrics_sampler_handle,
1301 router_fee_handle,
1302 fee_tier_handle,
1303 computation_handle,
1304 computation_shutdown_tx: c.computation_shutdown_tx,
1305 chain: c.chain,
1306 router_address: c.router_address,
1307 market_event_tx: c.market_event_tx,
1308 },
1309 controller,
1310 ))
1311 }
1312} pub struct Solver {
1316 router: WorkerPoolRouter,
1317 worker_pools: Vec<WorkerPool>,
1318 market_data: MarketData,
1319 derived_data: SharedDerivedDataRef,
1320 router_fees: SharedRouterFees,
1321 feed_handle: JoinHandle<()>,
1322 gas_price_handle: JoinHandle<()>,
1323 metrics_sampler_handle: JoinHandle<()>,
1324 router_fee_handle: JoinHandle<()>,
1325 fee_tier_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")]
1436 pub async fn from_recording(
1437 chain: Chain,
1438 updates: Vec<tycho_simulation::protocol::models::Update>,
1439 pools: std::collections::HashMap<String, PoolConfig>,
1440 gas_price_wei: Option<num_bigint::BigUint>,
1441 rpc_url: Option<&str>,
1442 ) -> Result<Self, SolverBuildError> {
1443 Self::from_recording_with(
1444 chain,
1445 updates,
1446 pools,
1447 gas_price_wei,
1448 rpc_url,
1449 &AlgorithmRegistry::new(),
1450 )
1451 .await
1452 }
1453
1454 #[cfg(feature = "test-utils")]
1466 pub async fn from_recording_with(
1467 chain: Chain,
1468 updates: Vec<tycho_simulation::protocol::models::Update>,
1469 pools: std::collections::HashMap<String, PoolConfig>,
1470 gas_price_wei: Option<num_bigint::BigUint>,
1471 rpc_url: Option<&str>,
1472 algorithms: &AlgorithmRegistry,
1473 ) -> Result<Self, SolverBuildError> {
1474 if pools.is_empty() {
1475 return Err(SolverBuildError::NoPools);
1476 }
1477 if pools
1478 .values()
1479 .all(|pool| pool.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
1480 {
1481 return Err(SolverBuildError::NoPublicPool);
1482 }
1483
1484 let market_data = MarketData::new_shared();
1485
1486 let feed_config =
1488 TychoFeedConfig::new("ws://replay".to_string(), chain, None, false, vec![], 0.0);
1489 let feed = TychoFeed::new(feed_config, market_data.clone());
1490 let market_event_tx = feed.event_sender();
1491 let _feed_rx = feed.subscribe();
1492
1493 for update in updates {
1494 feed.handle_tycho_message(update)
1495 .await
1496 .map_err(|e| SolverBuildError::Replay(e.to_string()))?;
1497 }
1498
1499 let gas_price = match gas_price_wei {
1501 Some(price) => price,
1502 None => {
1503 tracing::warn!("no recorded gas price, defaulting to 10 gwei");
1504 num_bigint::BigUint::from(10_000_000_000u64)
1505 }
1506 };
1507 let block_number = match market_data.read().await.last_updated() {
1508 Some(block) => block.number(),
1509 None => {
1510 tracing::warn!("no block number from replayed updates, defaulting to 0");
1511 0
1512 }
1513 };
1514 {
1515 let mut market = market_data.write().await;
1516 market.update_gas_price(BlockGasPrice {
1517 block_number,
1518 block_hash: Default::default(),
1519 block_timestamp: 0,
1520 pricing: GasPrice::Legacy { gas_price },
1521 });
1522 }
1523
1524 let gas_token = native_token(&chain).map_err(|_| SolverBuildError::GasToken)?;
1525 let pricing_max_hops = pricing_max_hops(
1526 pools
1527 .values()
1528 .map(|pool_cfg| pool_cfg.max_hops()),
1529 );
1530 let computation_config = ComputationManagerConfig::new()
1531 .with_gas_token(gas_token)
1532 .with_max_hop(pricing_max_hops)
1533 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD)
1534 .with_pricing_pass_budget(Duration::from_secs(24 * 60 * 60));
1538 let (computation_manager, _) =
1539 ComputationManager::new(computation_config, market_data.clone())
1540 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
1541
1542 let derived_data: SharedDerivedDataRef = computation_manager.store();
1543 let derived_event_tx = computation_manager.event_sender();
1544
1545 let computation_event_rx = feed.subscribe();
1546 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
1547
1548 let computation_handle = tokio::spawn(async move {
1549 computation_manager
1550 .run(computation_event_rx, computation_shutdown_rx)
1551 .await;
1552 });
1553
1554 let fallback_fee_tiers = SharedFeeTiers::default();
1557 if let Some(rpc_url) = rpc_url.filter(|_| chain == Chain::Ethereum) {
1558 propamm_fee_tier_fetcher(rpc_url, fallback_fee_tiers.clone())?
1559 .refresh_once()
1560 .await
1561 .map_err(|e| SolverBuildError::FeeTierFetcher(e.to_string()))?;
1562 }
1563
1564 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
1566 let mut worker_pools: Vec<WorkerPool> = Vec::new();
1567 let mut max_timeout_ms = 0u64;
1568
1569 for (name, pool_cfg) in &pools {
1570 let mut algo_cfg = AlgorithmConfig::new(
1571 pool_cfg.min_hops(),
1572 pool_cfg.max_hops(),
1573 Duration::from_millis(pool_cfg.timeout_ms()),
1574 pool_cfg.max_routes(),
1575 )?;
1576 if let Some(tokens) = parse_connector_tokens(pool_cfg.connector_tokens())? {
1577 algo_cfg = algo_cfg.with_connector_tokens(tokens);
1578 }
1579
1580 let pool_event_rx = feed.subscribe();
1581 let derived_rx = derived_event_tx.subscribe();
1582
1583 let named = WorkerPoolBuilder::new()
1584 .name(name.clone())
1585 .algorithm_config(algo_cfg)
1586 .num_workers(pool_cfg.num_workers())
1587 .task_queue_capacity(pool_cfg.task_queue_capacity())
1588 .liquidity_scope(
1589 pool_cfg
1590 .liquidity_scope()
1591 .unwrap_or_default(),
1592 )
1593 .fallback_fee_tiers(fallback_fee_tiers.clone());
1594 let (worker_pool, task_handle) = algorithms
1595 .configure(pool_cfg.algorithm(), named)?
1596 .build(market_data.clone(), Arc::clone(&derived_data), pool_event_rx, derived_rx)?;
1597
1598 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
1599 max_timeout_ms = max_timeout_ms.max(pool_cfg.timeout_ms());
1600 worker_pools.push(worker_pool);
1601 }
1602
1603 let encoder = {
1605 let registry = SwapEncoderRegistry::new(chain)
1606 .add_default_encoders(None)
1607 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1608 Encoder::new(chain, registry).map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1609 };
1610
1611 let router_address = encoder.router_address().cloned();
1612 let router_fees = encoder.router_fees();
1616 router_fees.set(crate::encoding::router_fees::RouterFees::new(
1617 100_000_000,
1618 0,
1619 0,
1620 rustc_hash::FxHashMap::default(),
1621 ));
1622 let router_config = WorkerPoolRouterConfig::default()
1623 .with_timeout(Duration::from_millis(max_timeout_ms.max(5000)))
1624 .with_min_responses(defaults::ROUTER_MIN_RESPONSES);
1625 let router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1626
1627 let market_read = market_data.read().await;
1629 let added = market_read.component_topology();
1630 drop(market_read);
1631
1632 if market_event_tx
1633 .send(MarketEvent::MarketUpdated {
1634 added_components: added,
1635 removed_components: vec![],
1636 updated_components: vec![],
1637 })
1638 .is_err()
1639 {
1640 tracing::warn!("no receivers for initial MarketUpdated broadcast");
1641 }
1642
1643 let feed_handle = tokio::spawn(futures::future::pending::<()>());
1646 let gas_price_handle = tokio::spawn(async { });
1647 let metrics_sampler_handle = tokio::spawn(async { });
1648 let router_fee_handle = tokio::spawn(async { });
1649 let fee_tier_handle = tokio::spawn(async { });
1650
1651 Ok(Solver {
1652 router,
1653 worker_pools,
1654 market_data,
1655 derived_data,
1656 router_fees,
1657 feed_handle,
1658 gas_price_handle,
1659 metrics_sampler_handle,
1660 router_fee_handle,
1661 fee_tier_handle,
1662 computation_handle,
1663 computation_shutdown_tx,
1664 chain,
1665 router_address,
1666 market_event_tx,
1667 })
1668 }
1669
1670 pub fn shutdown(self) {
1672 let _ = self.computation_shutdown_tx.send(());
1673 for pool in self.worker_pools {
1674 pool.shutdown();
1675 }
1676 self.feed_handle.abort();
1677 self.gas_price_handle.abort();
1678 self.metrics_sampler_handle.abort();
1679 self.router_fee_handle.abort();
1680 self.fee_tier_handle.abort();
1681 }
1682
1683 pub fn into_parts(self) -> SolverParts {
1685 SolverParts {
1686 router: self.router,
1687 worker_pools: self.worker_pools,
1688 market_data: self.market_data,
1689 derived_data: self.derived_data,
1690 router_fees: self.router_fees,
1691 feed_handle: self.feed_handle,
1692 gas_price_handle: self.gas_price_handle,
1693 metrics_sampler_handle: self.metrics_sampler_handle,
1694 router_fee_handle: self.router_fee_handle,
1695 fee_tier_handle: self.fee_tier_handle,
1696 computation_handle: self.computation_handle,
1697 computation_shutdown_tx: self.computation_shutdown_tx,
1698 chain: self.chain,
1699 router_address: self.router_address,
1700 }
1701 }
1702}
1703
1704pub struct SolverParts {
1708 router: WorkerPoolRouter,
1710 worker_pools: Vec<WorkerPool>,
1712 market_data: MarketData,
1714 derived_data: SharedDerivedDataRef,
1716 router_fees: SharedRouterFees,
1718 feed_handle: JoinHandle<()>,
1720 gas_price_handle: JoinHandle<()>,
1722 metrics_sampler_handle: JoinHandle<()>,
1724 router_fee_handle: JoinHandle<()>,
1726 fee_tier_handle: JoinHandle<()>,
1731 computation_handle: JoinHandle<()>,
1733 computation_shutdown_tx: broadcast::Sender<()>,
1735 chain: Chain,
1737 router_address: Option<Bytes>,
1739}
1740
1741impl SolverParts {
1742 pub fn chain(&self) -> Chain {
1744 self.chain
1745 }
1746
1747 pub fn router_address(&self) -> Option<&Bytes> {
1749 self.router_address.as_ref()
1750 }
1751
1752 pub fn worker_pools(&self) -> &[WorkerPool] {
1754 &self.worker_pools
1755 }
1756
1757 pub fn market_data(&self) -> &MarketData {
1759 &self.market_data
1760 }
1761
1762 pub fn derived_data(&self) -> &SharedDerivedDataRef {
1764 &self.derived_data
1765 }
1766
1767 pub fn router_fees(&self) -> &SharedRouterFees {
1769 &self.router_fees
1770 }
1771
1772 pub fn fee_tier_abort_handle(&self) -> AbortHandle {
1778 self.fee_tier_handle.abort_handle()
1779 }
1780
1781 pub fn into_router(self) -> WorkerPoolRouter {
1783 self.router
1784 }
1785
1786 #[allow(clippy::type_complexity)]
1788 pub fn into_components(
1789 self,
1790 ) -> (
1791 WorkerPoolRouter,
1792 Vec<WorkerPool>,
1793 MarketData,
1794 SharedDerivedDataRef,
1795 JoinHandle<()>,
1796 JoinHandle<()>,
1797 JoinHandle<()>,
1798 JoinHandle<()>,
1799 JoinHandle<()>,
1800 broadcast::Sender<()>,
1801 ) {
1802 (
1803 self.router,
1804 self.worker_pools,
1805 self.market_data,
1806 self.derived_data,
1807 self.feed_handle,
1808 self.gas_price_handle,
1809 self.metrics_sampler_handle,
1810 self.router_fee_handle,
1811 self.computation_handle,
1812 self.computation_shutdown_tx,
1813 )
1814 }
1815}
1816
1817#[cfg(test)]
1818mod tests {
1819 use super::*;
1820
1821 #[test]
1824 fn test_unscoped_pool_resolves_to_public_only() {
1825 let config = PoolConfig::new("most_liquid");
1826 assert_eq!(config.liquidity_scope(), None);
1827 assert_eq!(
1828 config
1829 .liquidity_scope()
1830 .unwrap_or_default(),
1831 LiquidityScope::PublicOnly
1832 );
1833 }
1834
1835 #[test]
1838 fn test_build_all_exclusive_pools() {
1839 let config =
1840 PoolConfig::new("most_liquid").with_liquidity_scope(LiquidityScope::IncludeExclusive);
1841 let result = FyndBuilder::new(
1842 Chain::Ethereum,
1843 "wss://example.invalid",
1844 "https://example.invalid",
1845 vec!["uniswap_v2".to_string()],
1846 100.0,
1847 )
1848 .add_pool("exclusive", &config)
1849 .expect("add_pool should accept the config")
1850 .build();
1851
1852 assert!(matches!(result, Err(SolverBuildError::NoPublicPool)));
1853 }
1854}