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 router_timeout: Duration,
532 router_min_responses: usize,
533 encoder: Option<Encoder>,
534 calldata_watermark: Option<Vec<u8>>,
535 pools: Vec<PoolEntry>,
536 price_guard_enabled: bool,
537 simulation_enabled: bool,
538 price_providers: Vec<Box<dyn PriceProvider>>,
539 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
540}
541
542impl FyndBuilder {
543 pub fn new(
545 chain: Chain,
546 tycho_url: impl Into<String>,
547 rpc_url: impl Into<String>,
548 protocols: Vec<String>,
549 min_tvl: f64,
550 ) -> Self {
551 Self {
552 algorithms: AlgorithmRegistry::new(),
553 chain,
554 tycho_url: tycho_url.into(),
555 rpc_url: rpc_url.into(),
556 protocols,
557 min_tvl,
558 tycho_api_key: None,
559 tycho_use_tls: DEFAULT_TYCHO_USE_TLS,
560 min_token_quality: defaults::MIN_TOKEN_QUALITY,
561 traded_n_days_ago: defaults::TRADED_N_DAYS_AGO,
562 tvl_buffer_ratio: defaults::TVL_BUFFER_RATIO,
563 gas_refresh_interval: defaults::GAS_REFRESH_INTERVAL,
564 reconnect_delay: defaults::RECONNECT_DELAY,
565 blocklisted_components: FxHashSet::default(),
566 partial_blocks: false,
567 router_timeout: DEFAULT_ROUTER_TIMEOUT,
568 router_min_responses: defaults::ROUTER_MIN_RESPONSES,
569 encoder: None,
570 calldata_watermark: None,
571 pools: Vec::new(),
572 price_guard_enabled: false,
573 simulation_enabled: false,
574 price_providers: Vec::new(),
575 pending_indexers: Vec::new(),
576 }
577 }
578
579 pub fn chain(&self) -> Chain {
581 self.chain
582 }
583
584 pub fn tycho_api_key(mut self, key: impl Into<String>) -> Self {
586 self.tycho_api_key = Some(key.into());
587 self
588 }
589
590 pub fn min_tvl(mut self, min_tvl: f64) -> Self {
592 self.min_tvl = min_tvl;
593 self
594 }
595
596 pub fn tycho_use_tls(mut self, use_tls: bool) -> Self {
598 self.tycho_use_tls = use_tls;
599 self
600 }
601
602 pub fn min_token_quality(mut self, quality: i32) -> Self {
605 self.min_token_quality = quality;
606 self
607 }
608
609 pub fn traded_n_days_ago(mut self, days: u64) -> Self {
611 self.traded_n_days_ago = days;
612 self
613 }
614
615 pub fn tvl_buffer_ratio(mut self, ratio: f64) -> Self {
617 self.tvl_buffer_ratio = ratio;
618 self
619 }
620
621 pub fn gas_refresh_interval(mut self, interval: Duration) -> Self {
623 self.gas_refresh_interval = interval;
624 self
625 }
626
627 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
629 self.reconnect_delay = delay;
630 self
631 }
632
633 pub fn blocklisted_components(mut self, components: impl IntoIterator<Item = String>) -> Self {
635 self.blocklisted_components = components.into_iter().collect();
636 self
637 }
638
639 pub fn partial_blocks(mut self, enabled: bool) -> Self {
645 self.partial_blocks = enabled;
646 self
647 }
648
649 pub fn worker_router_timeout(mut self, timeout: Duration) -> Self {
651 self.router_timeout = timeout;
652 self
653 }
654
655 pub fn worker_router_min_responses(mut self, min: usize) -> Self {
657 self.router_min_responses = min;
658 self
659 }
660
661 pub fn encoder(mut self, encoder: Encoder) -> Self {
663 self.encoder = Some(encoder);
664 self
665 }
666
667 pub fn calldata_watermark(mut self, watermark: impl Into<Vec<u8>>) -> Self {
671 self.calldata_watermark = Some(watermark.into());
672 self
673 }
674
675 pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
677 self.pools.push(PoolEntry::BuiltIn {
678 name: "default".to_string(),
679 algorithm: algorithm.into(),
680 num_workers: num_cpus::get(),
681 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
682 min_hops: defaults::POOL_MIN_HOPS,
683 max_hops: defaults::POOL_MAX_HOPS,
684 timeout_ms: defaults::POOL_TIMEOUT_MS,
685 max_routes: None,
686 connector_tokens: None,
687 liquidity_scope: None,
688 exclude_protocols: Vec::new(),
689 });
690 self
691 }
692
693 #[deprecated(
697 since = "0.99.23",
698 note = "register the algorithm in an `AlgorithmRegistry` and pass it to \
699 `with_algorithms`, which also serves pools that name it in a configuration \
700 file; this shorthand only ever added one pool"
701 )]
702 pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
703 where
704 A: Algorithm + 'static,
705 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
706 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
707 {
708 let name = name.into();
709 let algo_name = name.clone();
710 let configure =
711 Box::new(move |builder: WorkerPoolBuilder| builder.with_algorithm(algo_name, factory));
712 self.pools
713 .push(PoolEntry::Custom(CustomPoolEntry {
714 name,
715 num_workers: num_cpus::get(),
716 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
717 min_hops: defaults::POOL_MIN_HOPS,
718 max_hops: defaults::POOL_MAX_HOPS,
719 timeout_ms: defaults::POOL_TIMEOUT_MS,
720 max_routes: None,
721 liquidity_scope: None,
722 configure,
723 }));
724 self
725 }
726
727 pub fn with_algorithms(mut self, algorithms: AlgorithmRegistry) -> Self {
733 self.algorithms = algorithms;
734 self
735 }
736
737 pub fn add_default_price_providers(self) -> Self {
744 self.register_price_provider(Box::new(
745 crate::price_guard::hyperliquid::HyperliquidProvider::default(),
746 ))
747 .register_price_provider(Box::new(
748 crate::price_guard::binance_ws::BinanceWsProvider::default(),
749 ))
750 }
751
752 pub fn register_price_provider(mut self, provider: Box<dyn PriceProvider>) -> Self {
757 self.price_providers.push(provider);
758 self
759 }
760
761 pub fn with_pending_indexer(
767 mut self,
768 extractor: impl Into<String>,
769 indexer: Box<dyn TxDeltaIndexer>,
770 ) -> Self {
771 self.pending_indexers
772 .push((extractor.into(), indexer));
773 self
774 }
775
776 pub fn price_guard_enabled(mut self, enabled: bool) -> Self {
783 self.price_guard_enabled = enabled;
784 self
785 }
786
787 pub fn simulation_enabled(mut self, enabled: bool) -> Self {
791 self.simulation_enabled = enabled;
792 self
793 }
794
795 pub fn add_pool(
802 mut self,
803 name: impl Into<String>,
804 config: &PoolConfig,
805 ) -> Result<Self, SolverBuildError> {
806 let connector_tokens = parse_connector_tokens(config.connector_tokens())?;
807 self.pools.push(PoolEntry::BuiltIn {
808 name: name.into(),
809 algorithm: config.algorithm().to_string(),
810 num_workers: config.num_workers(),
811 task_queue_capacity: config.task_queue_capacity(),
812 min_hops: config.min_hops(),
813 max_hops: config.max_hops(),
814 timeout_ms: config.timeout_ms(),
815 max_routes: config.max_routes(),
816 connector_tokens,
817 liquidity_scope: config.liquidity_scope(),
818 exclude_protocols: config
819 .exclude_protocols()
820 .map(<[String]>::to_vec)
821 .unwrap_or_default(),
822 });
823 Ok(self)
824 }
825
826 fn assemble_components(mut self) -> Result<BuiltComponents, SolverBuildError> {
829 if self.pools.is_empty() {
830 return Err(SolverBuildError::NoPools);
831 }
832
833 if self
837 .pools
838 .iter()
839 .all(|p| p.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
840 {
841 return Err(SolverBuildError::NoPublicPool);
842 }
843
844 if self.price_providers.is_empty() {
846 self = self.add_default_price_providers();
847 }
848
849 let market_data = MarketData::new_shared();
850
851 let tycho_feed_config = TychoFeedConfig::new(
852 self.tycho_url,
853 self.chain,
854 self.tycho_api_key,
855 self.tycho_use_tls,
856 self.protocols,
857 self.min_tvl,
858 )
859 .tvl_buffer_ratio(self.tvl_buffer_ratio)
860 .reconnect_delay(self.reconnect_delay)
861 .min_token_quality(self.min_token_quality)
862 .traded_n_days_ago(self.traded_n_days_ago)
863 .blocklisted_components(self.blocklisted_components)
864 .partial_blocks(self.partial_blocks);
865
866 let ethereum_client = EthereumRpcClient::new(self.rpc_url.as_str())
867 .map_err(|e| SolverBuildError::RpcClient(e.to_string()))?;
868
869 let gas_price_fetcher =
870 GasPriceFetcher::new(ethereum_client, market_data.clone(), self.gas_refresh_interval);
871
872 let tycho_feed = TychoFeed::new(tycho_feed_config, market_data.clone());
873 let market_event_tx = tycho_feed.event_sender();
874
875 let gas_token = native_token(&self.chain).map_err(|_| SolverBuildError::GasToken)?;
876 let pricing_max_hops = pricing_max_hops(
877 self.pools
878 .iter()
879 .map(PoolEntry::max_hops),
880 );
881 let computation_config = ComputationManagerConfig::new()
882 .with_gas_token(gas_token)
883 .with_max_hop(pricing_max_hops)
884 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
885 let (computation_manager, _) =
888 ComputationManager::new(computation_config, market_data.clone())
889 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
890
891 let derived_data: SharedDerivedDataRef = computation_manager.store();
892 let derived_event_tx = computation_manager.event_sender();
893
894 let computation_event_rx = tycho_feed.subscribe();
897 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
898
899 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
900 let mut worker_pools: Vec<WorkerPool> = Vec::new();
901 let fallback_fee_tiers = SharedFeeTiers::default();
903
904 let pools = std::mem::take(&mut self.pools);
905
906 for pool_entry in pools {
907 let pool_event_rx = tycho_feed.subscribe();
908 let derived_rx = derived_event_tx.subscribe();
909
910 let pool_scope = pool_entry
911 .liquidity_scope()
912 .unwrap_or_default();
913
914 let (worker_pool, task_handle) = match pool_entry {
915 PoolEntry::BuiltIn {
916 name,
917 algorithm,
918 num_workers,
919 task_queue_capacity,
920 min_hops,
921 max_hops,
922 timeout_ms,
923 max_routes,
924 connector_tokens,
925 liquidity_scope: _,
926 exclude_protocols,
927 } => {
928 let mut algo_cfg = AlgorithmConfig::new(
929 min_hops,
930 max_hops,
931 Duration::from_millis(timeout_ms),
932 max_routes,
933 )?;
934 if let Some(tokens) = connector_tokens {
935 algo_cfg = algo_cfg.with_connector_tokens(tokens);
936 }
937 let named = WorkerPoolBuilder::new()
938 .name(name)
939 .algorithm_config(algo_cfg)
940 .num_workers(num_workers)
941 .task_queue_capacity(task_queue_capacity)
942 .liquidity_scope(pool_scope)
943 .exclude_protocols(exclude_protocols)
944 .fallback_fee_tiers(fallback_fee_tiers.clone());
945 let builder = self
946 .algorithms
947 .configure(&algorithm, named)?;
948 builder.build(
949 market_data.clone(),
950 Arc::clone(&derived_data),
951 pool_event_rx,
952 derived_rx,
953 )?
954 }
955 PoolEntry::Custom(custom) => {
956 let algo_cfg = AlgorithmConfig::new(
957 custom.min_hops,
958 custom.max_hops,
959 Duration::from_millis(custom.timeout_ms),
960 custom.max_routes,
961 )?;
962 let builder = WorkerPoolBuilder::new()
963 .name(custom.name)
964 .algorithm_config(algo_cfg)
965 .num_workers(custom.num_workers)
966 .task_queue_capacity(custom.task_queue_capacity)
967 .liquidity_scope(pool_scope)
968 .fallback_fee_tiers(fallback_fee_tiers.clone());
969 let builder = (custom.configure)(builder);
970 builder.build(
971 market_data.clone(),
972 Arc::clone(&derived_data),
973 pool_event_rx,
974 derived_rx,
975 )?
976 }
977 };
978
979 solver_pool_handles.push(
980 SolverPoolHandle::new(worker_pool.name(), task_handle)
981 .with_liquidity_scope(pool_scope),
982 );
983 worker_pools.push(worker_pool);
984 }
985
986 let encoder = match self.encoder {
987 Some(enc) => enc,
988 None => {
989 let registry = SwapEncoderRegistry::new(self.chain)
990 .add_default_encoders(None)
991 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
992 Encoder::new(self.chain, registry)
993 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?
994 }
995 };
996 let encoder = match self.calldata_watermark {
997 Some(watermark) => encoder.with_calldata_watermark(watermark),
998 None => encoder,
999 };
1000
1001 let chain = self.chain;
1002 let router_address = encoder.router_address().cloned();
1003 let router_fees = encoder.router_fees();
1004
1005 let router_fee_fetcher = match &router_address {
1006 Some(addr) => Some(
1007 RouterFeeFetcher::new(
1008 self.rpc_url.as_str(),
1009 addr,
1010 router_fees.clone(),
1011 defaults::ROUTER_FEE_REFRESH_INTERVAL,
1012 )
1013 .map_err(|e| SolverBuildError::RouterFeeFetcher(e.to_string()))?,
1014 ),
1015 None => {
1016 tracing::warn!(
1017 %chain,
1018 "no Tycho router for this chain; running quote-only (encoding disabled)"
1019 );
1020 None
1021 }
1022 };
1023
1024 let quote_simulator = if self.simulation_enabled {
1025 Some(
1026 QuoteSimulator::new(
1027 self.rpc_url.as_str(),
1028 chain,
1029 defaults::SIMULATION_REQUEST_TIMEOUT,
1030 )
1031 .map_err(|error| SolverBuildError::QuoteSimulator(error.to_string()))?,
1032 )
1033 } else {
1034 None
1035 };
1036
1037 let fee_tier_fetcher = if chain == Chain::Ethereum {
1043 Some(propamm_fee_tier_fetcher(self.rpc_url.as_str(), fallback_fee_tiers.clone())?)
1044 } else {
1045 None
1046 };
1047
1048 let router_config = WorkerPoolRouterConfig::default()
1051 .with_timeout(self.router_timeout)
1052 .with_min_responses(self.router_min_responses);
1053 let mut router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1054 if let Some(simulator) = quote_simulator {
1055 router = router.with_simulator(simulator);
1056 }
1057
1058 if self.price_guard_enabled {
1059 let mut registry = PriceProviderRegistry::new();
1060 let mut worker_handles = Vec::new();
1061 for mut provider in self.price_providers {
1062 worker_handles.push(provider.start(market_data.clone()));
1063 registry = registry.register(provider);
1064 }
1065 let price_guard = PriceGuard::new(registry, worker_handles);
1066 router = router.with_price_guard(price_guard);
1067 }
1068
1069 Ok(BuiltComponents {
1070 tycho_feed,
1071 gas_price_fetcher,
1072 router_fee_fetcher,
1073 fee_tier_fetcher,
1074 computation_manager,
1075 computation_event_rx,
1076 computation_shutdown_tx,
1077 computation_shutdown_rx,
1078 router,
1079 worker_pools,
1080 market_data,
1081 derived_data,
1082 router_fees,
1083 chain,
1084 router_address,
1085 pending_indexers: self.pending_indexers,
1086 market_event_tx,
1087 })
1088 }
1089
1090 pub fn build(self) -> Result<Solver, SolverBuildError> {
1096 let mut c = self.assemble_components()?;
1097
1098 let feed_handle = tokio::spawn(async move {
1099 if let Err(e) = c.tycho_feed.run().await {
1100 metrics::counter!("tycho_feed_failures_total").increment(1);
1101 tracing::error!(error = %e, "tycho feed error");
1102 }
1103 });
1104 let gas_price_handle = tokio::spawn(async move {
1105 c.gas_price_fetcher.run().await;
1106 });
1107 let metrics_sampler =
1108 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1109 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1110 let router_fee_handle = match c.router_fee_fetcher {
1111 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1112 None => tokio::spawn(async {}),
1113 };
1114 let fee_tier_handle = match c.fee_tier_fetcher {
1115 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1116 None => tokio::spawn(async {}),
1117 };
1118 let computation_handle = tokio::spawn(async move {
1119 c.computation_manager
1120 .run(c.computation_event_rx, c.computation_shutdown_rx)
1121 .await;
1122 });
1123
1124 Ok(Solver {
1125 router: c.router,
1126 worker_pools: c.worker_pools,
1127 market_data: c.market_data,
1128 derived_data: c.derived_data,
1129 router_fees: c.router_fees,
1130 feed_handle,
1131 gas_price_handle,
1132 metrics_sampler_handle,
1133 router_fee_handle,
1134 fee_tier_handle,
1135 computation_handle,
1136 computation_shutdown_tx: c.computation_shutdown_tx,
1137 chain: c.chain,
1138 router_address: c.router_address,
1139 market_event_tx: c.market_event_tx,
1140 })
1141 }
1142
1143 pub async fn build_with_pending(
1155 self,
1156 ) -> Result<(Solver, PendingBlockProcessor), SolverBuildError> {
1157 let mut c = self.assemble_components()?;
1158
1159 let (pending_tx, pending_rx) =
1160 tokio::sync::oneshot::channel::<Result<PendingBlockProcessor, String>>();
1161
1162 let pending_indexers = c.pending_indexers;
1163 let feed_handle = tokio::spawn(async move {
1164 if let Err(e) = c
1165 .tycho_feed
1166 .run_with_pending(pending_tx, pending_indexers)
1167 .await
1168 {
1169 metrics::counter!("tycho_feed_failures_total").increment(1);
1170 tracing::error!(error = %e, "tycho feed error");
1171 }
1172 });
1173 let gas_price_handle = tokio::spawn(async move {
1174 c.gas_price_fetcher.run().await;
1175 });
1176 let metrics_sampler =
1177 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1178 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1179 let router_fee_handle = match c.router_fee_fetcher {
1180 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1181 None => tokio::spawn(async {}),
1182 };
1183 let fee_tier_handle = match c.fee_tier_fetcher {
1184 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1185 None => tokio::spawn(async {}),
1186 };
1187 let computation_handle = tokio::spawn(async move {
1188 c.computation_manager
1189 .run(c.computation_event_rx, c.computation_shutdown_rx)
1190 .await;
1191 });
1192
1193 let pending = pending_rx
1194 .await
1195 .map_err(|_| SolverBuildError::PendingChannelClosed)?
1196 .map_err(SolverBuildError::FeedSetup)?;
1197
1198 Ok((
1199 Solver {
1200 router: c.router,
1201 worker_pools: c.worker_pools,
1202 market_data: c.market_data,
1203 derived_data: c.derived_data,
1204 router_fees: c.router_fees,
1205 feed_handle,
1206 gas_price_handle,
1207 metrics_sampler_handle,
1208 router_fee_handle,
1209 fee_tier_handle,
1210 computation_handle,
1211 computation_shutdown_tx: c.computation_shutdown_tx,
1212 chain: c.chain,
1213 router_address: c.router_address,
1214 market_event_tx: c.market_event_tx,
1215 },
1216 pending,
1217 ))
1218 }
1219
1220 #[cfg(feature = "experimental")]
1235 pub async fn build_with_step_controller(
1236 self,
1237 ) -> Result<(Solver, BlockStepController), SolverBuildError> {
1238 let mut c = self.assemble_components()?;
1239
1240 let (controller_tx, controller_rx) =
1241 tokio::sync::oneshot::channel::<Result<BlockStepController, String>>();
1242
1243 let feed_handle = tokio::spawn(async move {
1244 if let Err(e) = c
1245 .tycho_feed
1246 .run_with_step_controller(controller_tx)
1247 .await
1248 {
1249 tracing::error!(error = %e, "tycho feed error");
1250 }
1251 });
1252 let gas_price_handle = tokio::spawn(async move {
1253 c.gas_price_fetcher.run().await;
1254 });
1255 let metrics_sampler =
1256 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1257 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1258 let router_fee_handle = match c.router_fee_fetcher {
1259 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1260 None => tokio::spawn(async {}),
1261 };
1262 let fee_tier_handle = match c.fee_tier_fetcher {
1263 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1264 None => tokio::spawn(async {}),
1265 };
1266 let computation_handle = tokio::spawn(async move {
1267 c.computation_manager
1268 .run(c.computation_event_rx, c.computation_shutdown_rx)
1269 .await;
1270 });
1271
1272 let controller = controller_rx
1273 .await
1274 .map_err(|_| SolverBuildError::StepControllerChannelClosed)?
1275 .map_err(SolverBuildError::FeedSetup)?;
1276
1277 Ok((
1278 Solver {
1279 router: c.router,
1280 worker_pools: c.worker_pools,
1281 market_data: c.market_data,
1282 derived_data: c.derived_data,
1283 router_fees: c.router_fees,
1284 feed_handle,
1285 gas_price_handle,
1286 metrics_sampler_handle,
1287 router_fee_handle,
1288 fee_tier_handle,
1289 computation_handle,
1290 computation_shutdown_tx: c.computation_shutdown_tx,
1291 chain: c.chain,
1292 router_address: c.router_address,
1293 market_event_tx: c.market_event_tx,
1294 },
1295 controller,
1296 ))
1297 }
1298} pub struct Solver {
1302 router: WorkerPoolRouter,
1303 worker_pools: Vec<WorkerPool>,
1304 market_data: MarketData,
1305 derived_data: SharedDerivedDataRef,
1306 router_fees: SharedRouterFees,
1307 feed_handle: JoinHandle<()>,
1308 gas_price_handle: JoinHandle<()>,
1309 metrics_sampler_handle: JoinHandle<()>,
1310 router_fee_handle: JoinHandle<()>,
1311 fee_tier_handle: JoinHandle<()>,
1312 computation_handle: JoinHandle<()>,
1313 computation_shutdown_tx: broadcast::Sender<()>,
1314 chain: Chain,
1315 router_address: Option<Bytes>,
1316 market_event_tx: broadcast::Sender<MarketEvent>,
1317}
1318
1319impl Solver {
1320 pub fn market_data(&self) -> MarketData {
1322 self.market_data.clone()
1323 }
1324
1325 pub fn router_address(&self) -> Option<&Bytes> {
1327 self.router_address.as_ref()
1328 }
1329
1330 pub fn derived_data(&self) -> SharedDerivedDataRef {
1332 Arc::clone(&self.derived_data)
1333 }
1334
1335 pub fn subscribe_market_events(&self) -> broadcast::Receiver<crate::feed::events::MarketEvent> {
1340 self.market_event_tx.subscribe()
1341 }
1342
1343 pub async fn quote(&self, request: QuoteRequest) -> Result<Quote, SolveError> {
1353 self.router
1354 .quote(request, ExclusiveAccess::Granted)
1355 .await
1356 }
1357
1358 pub async fn wait_until_ready(&self, timeout: Duration) -> Result<(), WaitReadyError> {
1375 const POLL_INTERVAL: Duration = Duration::from_millis(500);
1376
1377 let deadline = tokio::time::Instant::now() + timeout;
1378
1379 loop {
1380 let market_ready = self
1381 .market_data
1382 .read()
1383 .await
1384 .last_updated()
1385 .is_some();
1386 let derived_ready = self
1387 .derived_data
1388 .read()
1389 .await
1390 .derived_data_ready();
1391
1392 if market_ready && derived_ready {
1393 return Ok(());
1394 }
1395
1396 if tokio::time::Instant::now() >= deadline {
1397 return Err(WaitReadyError { timeout_ms: timeout.as_millis() as u64 });
1398 }
1399
1400 tokio::time::sleep(POLL_INTERVAL).await;
1401 }
1402 }
1403
1404 #[cfg(feature = "test-utils")]
1422 pub async fn from_recording(
1423 chain: Chain,
1424 updates: Vec<tycho_simulation::protocol::models::Update>,
1425 pools: std::collections::HashMap<String, PoolConfig>,
1426 gas_price_wei: Option<num_bigint::BigUint>,
1427 rpc_url: Option<&str>,
1428 ) -> Result<Self, SolverBuildError> {
1429 Self::from_recording_with(
1430 chain,
1431 updates,
1432 pools,
1433 gas_price_wei,
1434 rpc_url,
1435 &AlgorithmRegistry::new(),
1436 )
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 rpc_url: Option<&str>,
1458 algorithms: &AlgorithmRegistry,
1459 ) -> Result<Self, SolverBuildError> {
1460 if pools.is_empty() {
1461 return Err(SolverBuildError::NoPools);
1462 }
1463 if pools
1464 .values()
1465 .all(|pool| pool.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
1466 {
1467 return Err(SolverBuildError::NoPublicPool);
1468 }
1469
1470 let market_data = MarketData::new_shared();
1471
1472 let feed_config =
1474 TychoFeedConfig::new("ws://replay".to_string(), chain, None, false, vec![], 0.0);
1475 let feed = TychoFeed::new(feed_config, market_data.clone());
1476 let market_event_tx = feed.event_sender();
1477 let _feed_rx = feed.subscribe();
1478
1479 for update in updates {
1480 feed.handle_tycho_message(update)
1481 .await
1482 .map_err(|e| SolverBuildError::Replay(e.to_string()))?;
1483 }
1484
1485 let gas_price = match gas_price_wei {
1487 Some(price) => price,
1488 None => {
1489 tracing::warn!("no recorded gas price, defaulting to 10 gwei");
1490 num_bigint::BigUint::from(10_000_000_000u64)
1491 }
1492 };
1493 let block_number = match market_data.read().await.last_updated() {
1494 Some(block) => block.number(),
1495 None => {
1496 tracing::warn!("no block number from replayed updates, defaulting to 0");
1497 0
1498 }
1499 };
1500 {
1501 let mut market = market_data.write().await;
1502 market.update_gas_price(BlockGasPrice {
1503 block_number,
1504 block_hash: Default::default(),
1505 block_timestamp: 0,
1506 pricing: GasPrice::Legacy { gas_price },
1507 });
1508 }
1509
1510 let gas_token = native_token(&chain).map_err(|_| SolverBuildError::GasToken)?;
1511 let pricing_max_hops = pricing_max_hops(
1512 pools
1513 .values()
1514 .map(|pool_cfg| pool_cfg.max_hops()),
1515 );
1516 let computation_config = ComputationManagerConfig::new()
1517 .with_gas_token(gas_token)
1518 .with_max_hop(pricing_max_hops)
1519 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD)
1520 .with_pricing_pass_budget(Duration::from_secs(24 * 60 * 60));
1524 let (computation_manager, _) =
1525 ComputationManager::new(computation_config, market_data.clone())
1526 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
1527
1528 let derived_data: SharedDerivedDataRef = computation_manager.store();
1529 let derived_event_tx = computation_manager.event_sender();
1530
1531 let computation_event_rx = feed.subscribe();
1532 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
1533
1534 let computation_handle = tokio::spawn(async move {
1535 computation_manager
1536 .run(computation_event_rx, computation_shutdown_rx)
1537 .await;
1538 });
1539
1540 let fallback_fee_tiers = SharedFeeTiers::default();
1543 if let Some(rpc_url) = rpc_url.filter(|_| chain == Chain::Ethereum) {
1544 propamm_fee_tier_fetcher(rpc_url, fallback_fee_tiers.clone())?
1545 .refresh_once()
1546 .await
1547 .map_err(|e| SolverBuildError::FeeTierFetcher(e.to_string()))?;
1548 }
1549
1550 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
1552 let mut worker_pools: Vec<WorkerPool> = Vec::new();
1553 let mut max_timeout_ms = 0u64;
1554
1555 for (name, pool_cfg) in &pools {
1556 let mut algo_cfg = AlgorithmConfig::new(
1557 pool_cfg.min_hops(),
1558 pool_cfg.max_hops(),
1559 Duration::from_millis(pool_cfg.timeout_ms()),
1560 pool_cfg.max_routes(),
1561 )?;
1562 if let Some(tokens) = parse_connector_tokens(pool_cfg.connector_tokens())? {
1563 algo_cfg = algo_cfg.with_connector_tokens(tokens);
1564 }
1565
1566 let pool_event_rx = feed.subscribe();
1567 let derived_rx = derived_event_tx.subscribe();
1568
1569 let named = WorkerPoolBuilder::new()
1570 .name(name.clone())
1571 .algorithm_config(algo_cfg)
1572 .num_workers(pool_cfg.num_workers())
1573 .task_queue_capacity(pool_cfg.task_queue_capacity())
1574 .liquidity_scope(
1575 pool_cfg
1576 .liquidity_scope()
1577 .unwrap_or_default(),
1578 )
1579 .fallback_fee_tiers(fallback_fee_tiers.clone());
1580 let (worker_pool, task_handle) = algorithms
1581 .configure(pool_cfg.algorithm(), named)?
1582 .build(market_data.clone(), Arc::clone(&derived_data), pool_event_rx, derived_rx)?;
1583
1584 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
1585 max_timeout_ms = max_timeout_ms.max(pool_cfg.timeout_ms());
1586 worker_pools.push(worker_pool);
1587 }
1588
1589 let encoder = {
1591 let registry = SwapEncoderRegistry::new(chain)
1592 .add_default_encoders(None)
1593 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1594 Encoder::new(chain, registry).map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1595 };
1596
1597 let router_address = encoder.router_address().cloned();
1598 let router_fees = encoder.router_fees();
1602 router_fees.set(crate::encoding::router_fees::RouterFees::new(
1603 100_000_000,
1604 0,
1605 0,
1606 rustc_hash::FxHashMap::default(),
1607 ));
1608 let router_config = WorkerPoolRouterConfig::default()
1609 .with_timeout(Duration::from_millis(max_timeout_ms.max(5000)))
1610 .with_min_responses(defaults::ROUTER_MIN_RESPONSES);
1611 let router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1612
1613 let market_read = market_data.read().await;
1615 let added = market_read.component_topology();
1616 drop(market_read);
1617
1618 if market_event_tx
1619 .send(MarketEvent::MarketUpdated {
1620 added_components: added,
1621 removed_components: vec![],
1622 updated_components: vec![],
1623 })
1624 .is_err()
1625 {
1626 tracing::warn!("no receivers for initial MarketUpdated broadcast");
1627 }
1628
1629 let feed_handle = tokio::spawn(futures::future::pending::<()>());
1632 let gas_price_handle = tokio::spawn(async { });
1633 let metrics_sampler_handle = tokio::spawn(async { });
1634 let router_fee_handle = tokio::spawn(async { });
1635 let fee_tier_handle = tokio::spawn(async { });
1636
1637 Ok(Solver {
1638 router,
1639 worker_pools,
1640 market_data,
1641 derived_data,
1642 router_fees,
1643 feed_handle,
1644 gas_price_handle,
1645 metrics_sampler_handle,
1646 router_fee_handle,
1647 fee_tier_handle,
1648 computation_handle,
1649 computation_shutdown_tx,
1650 chain,
1651 router_address,
1652 market_event_tx,
1653 })
1654 }
1655
1656 pub fn shutdown(self) {
1658 let _ = self.computation_shutdown_tx.send(());
1659 for pool in self.worker_pools {
1660 pool.shutdown();
1661 }
1662 self.feed_handle.abort();
1663 self.gas_price_handle.abort();
1664 self.metrics_sampler_handle.abort();
1665 self.router_fee_handle.abort();
1666 self.fee_tier_handle.abort();
1667 }
1668
1669 pub fn into_parts(self) -> SolverParts {
1671 SolverParts {
1672 router: self.router,
1673 worker_pools: self.worker_pools,
1674 market_data: self.market_data,
1675 derived_data: self.derived_data,
1676 router_fees: self.router_fees,
1677 feed_handle: self.feed_handle,
1678 gas_price_handle: self.gas_price_handle,
1679 metrics_sampler_handle: self.metrics_sampler_handle,
1680 router_fee_handle: self.router_fee_handle,
1681 fee_tier_handle: self.fee_tier_handle,
1682 computation_handle: self.computation_handle,
1683 computation_shutdown_tx: self.computation_shutdown_tx,
1684 chain: self.chain,
1685 router_address: self.router_address,
1686 }
1687 }
1688}
1689
1690pub struct SolverParts {
1694 router: WorkerPoolRouter,
1696 worker_pools: Vec<WorkerPool>,
1698 market_data: MarketData,
1700 derived_data: SharedDerivedDataRef,
1702 router_fees: SharedRouterFees,
1704 feed_handle: JoinHandle<()>,
1706 gas_price_handle: JoinHandle<()>,
1708 metrics_sampler_handle: JoinHandle<()>,
1710 router_fee_handle: JoinHandle<()>,
1712 fee_tier_handle: JoinHandle<()>,
1717 computation_handle: JoinHandle<()>,
1719 computation_shutdown_tx: broadcast::Sender<()>,
1721 chain: Chain,
1723 router_address: Option<Bytes>,
1725}
1726
1727impl SolverParts {
1728 pub fn chain(&self) -> Chain {
1730 self.chain
1731 }
1732
1733 pub fn router_address(&self) -> Option<&Bytes> {
1735 self.router_address.as_ref()
1736 }
1737
1738 pub fn worker_pools(&self) -> &[WorkerPool] {
1740 &self.worker_pools
1741 }
1742
1743 pub fn market_data(&self) -> &MarketData {
1745 &self.market_data
1746 }
1747
1748 pub fn derived_data(&self) -> &SharedDerivedDataRef {
1750 &self.derived_data
1751 }
1752
1753 pub fn router_fees(&self) -> &SharedRouterFees {
1755 &self.router_fees
1756 }
1757
1758 pub fn fee_tier_abort_handle(&self) -> AbortHandle {
1764 self.fee_tier_handle.abort_handle()
1765 }
1766
1767 pub fn into_router(self) -> WorkerPoolRouter {
1769 self.router
1770 }
1771
1772 #[allow(clippy::type_complexity)]
1774 pub fn into_components(
1775 self,
1776 ) -> (
1777 WorkerPoolRouter,
1778 Vec<WorkerPool>,
1779 MarketData,
1780 SharedDerivedDataRef,
1781 JoinHandle<()>,
1782 JoinHandle<()>,
1783 JoinHandle<()>,
1784 JoinHandle<()>,
1785 JoinHandle<()>,
1786 broadcast::Sender<()>,
1787 ) {
1788 (
1789 self.router,
1790 self.worker_pools,
1791 self.market_data,
1792 self.derived_data,
1793 self.feed_handle,
1794 self.gas_price_handle,
1795 self.metrics_sampler_handle,
1796 self.router_fee_handle,
1797 self.computation_handle,
1798 self.computation_shutdown_tx,
1799 )
1800 }
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805 use super::*;
1806
1807 #[test]
1810 fn test_unscoped_pool_resolves_to_public_only() {
1811 let config = PoolConfig::new("most_liquid");
1812 assert_eq!(config.liquidity_scope(), None);
1813 assert_eq!(
1814 config
1815 .liquidity_scope()
1816 .unwrap_or_default(),
1817 LiquidityScope::PublicOnly
1818 );
1819 }
1820
1821 #[test]
1824 fn test_build_all_exclusive_pools() {
1825 let config =
1826 PoolConfig::new("most_liquid").with_liquidity_scope(LiquidityScope::IncludeExclusive);
1827 let result = FyndBuilder::new(
1828 Chain::Ethereum,
1829 "wss://example.invalid",
1830 "https://example.invalid",
1831 vec!["uniswap_v2".to_string()],
1832 100.0,
1833 )
1834 .add_pool("exclusive", &config)
1835 .expect("add_pool should accept the config")
1836 .build();
1837
1838 assert!(matches!(result, Err(SolverBuildError::NoPublicPool)));
1839 }
1840}