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},
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 types::constants::native_token,
48 worker_pool::{
49 pool::{WorkerPool, WorkerPoolBuilder},
50 registry::UnknownAlgorithmError,
51 },
52 worker_pool_router::{
53 config::WorkerPoolRouterConfig, ExclusiveAccess, LiquidityScope, SolverPoolHandle,
54 WorkerPoolRouter,
55 },
56 Algorithm, Quote, QuoteRequest, SolveError,
57};
58
59pub mod defaults {
65 use std::time::Duration;
66
67 pub const MIN_TOKEN_QUALITY: i32 = 100;
69 pub const TRADED_N_DAYS_AGO: u64 = 3;
71 pub const TVL_BUFFER_RATIO: f64 = 1.1;
74 pub const GAS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
76 pub const METRICS_SAMPLE_INTERVAL: Duration = Duration::from_secs(10);
78 pub const ROUTER_FEE_REFRESH_INTERVAL: Duration = Duration::from_secs(300);
80 pub const RECONNECT_DELAY: Duration = Duration::from_secs(5);
82 pub const ROUTER_MIN_RESPONSES: usize = 0;
85 pub const POOL_TASK_QUEUE_CAPACITY: usize = 1000;
87 pub const POOL_MIN_HOPS: usize = 1;
89 pub const POOL_MAX_HOPS: usize = 3;
91 pub const POOL_TIMEOUT_MS: u64 = 100;
93}
94
95const DEFAULT_TYCHO_USE_TLS: bool = true;
97const DEFAULT_DEPTH_SLIPPAGE_THRESHOLD: f64 = 0.01;
98const DEFAULT_ROUTER_TIMEOUT: Duration = Duration::from_secs(10);
101
102fn default_task_queue_capacity() -> usize {
105 defaults::POOL_TASK_QUEUE_CAPACITY
106}
107
108fn default_min_hops() -> usize {
109 defaults::POOL_MIN_HOPS
110}
111
112fn default_max_hops() -> usize {
113 defaults::POOL_MAX_HOPS
114}
115
116fn default_algo_timeout_ms() -> u64 {
117 defaults::POOL_TIMEOUT_MS
118}
119
120fn parse_connector_tokens(
121 raw: Option<&[String]>,
122) -> Result<Option<FxHashSet<Address>>, SolverBuildError> {
123 let Some(strings) = raw else {
124 return Ok(None);
125 };
126 let mut set = FxHashSet::with_capacity_and_hasher(strings.len(), Default::default());
127 for s in strings {
128 let addr = Address::from_str(s).map_err(|e| AlgorithmError::InvalidConfiguration {
129 reason: format!("connector_tokens: invalid address {s:?}: {e}"),
130 })?;
131 set.insert(addr);
132 }
133 Ok(Some(set))
134}
135
136#[must_use]
138#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct PoolConfig {
140 algorithm: String,
142 #[serde(default = "num_cpus::get")]
144 num_workers: usize,
145 #[serde(default = "default_task_queue_capacity")]
147 task_queue_capacity: usize,
148 #[serde(default = "default_min_hops")]
150 min_hops: usize,
151 #[serde(default = "default_max_hops")]
153 max_hops: usize,
154 #[serde(default = "default_algo_timeout_ms")]
156 timeout_ms: u64,
157 #[serde(default)]
159 max_routes: Option<usize>,
160 #[serde(default)]
163 connector_tokens: Option<Vec<String>>,
164 #[serde(default)]
166 liquidity_scope: Option<LiquidityScope>,
167}
168
169impl PoolConfig {
170 pub fn new(algorithm: impl Into<String>) -> Self {
173 Self {
174 algorithm: algorithm.into(),
175 num_workers: num_cpus::get(),
176 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
177 min_hops: defaults::POOL_MIN_HOPS,
178 max_hops: defaults::POOL_MAX_HOPS,
179 timeout_ms: defaults::POOL_TIMEOUT_MS,
180 max_routes: None,
181 connector_tokens: None,
182 liquidity_scope: None,
183 }
184 }
185
186 pub fn algorithm(&self) -> &str {
188 &self.algorithm
189 }
190
191 pub fn liquidity_scope(&self) -> Option<LiquidityScope> {
193 self.liquidity_scope
194 }
195
196 pub fn with_liquidity_scope(mut self, scope: LiquidityScope) -> Self {
198 self.liquidity_scope = Some(scope);
199 self
200 }
201
202 pub fn num_workers(&self) -> usize {
204 self.num_workers
205 }
206
207 pub fn with_num_workers(mut self, num_workers: usize) -> Self {
209 self.num_workers = num_workers;
210 self
211 }
212
213 pub fn with_task_queue_capacity(mut self, task_queue_capacity: usize) -> Self {
215 self.task_queue_capacity = task_queue_capacity;
216 self
217 }
218
219 pub fn with_min_hops(mut self, min_hops: usize) -> Self {
221 self.min_hops = min_hops;
222 self
223 }
224
225 pub fn with_max_hops(mut self, max_hops: usize) -> Self {
227 self.max_hops = max_hops;
228 self
229 }
230
231 pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
233 self.timeout_ms = timeout_ms;
234 self
235 }
236
237 pub fn with_max_routes(mut self, max_routes: Option<usize>) -> Self {
239 self.max_routes = max_routes;
240 self
241 }
242
243 pub fn task_queue_capacity(&self) -> usize {
245 self.task_queue_capacity
246 }
247
248 pub fn min_hops(&self) -> usize {
250 self.min_hops
251 }
252
253 pub fn max_hops(&self) -> usize {
255 self.max_hops
256 }
257
258 pub fn timeout_ms(&self) -> u64 {
260 self.timeout_ms
261 }
262
263 pub fn max_routes(&self) -> Option<usize> {
265 self.max_routes
266 }
267
268 pub fn with_connector_tokens(mut self, tokens: Vec<String>) -> Self {
271 self.connector_tokens = Some(tokens);
272 self
273 }
274
275 pub fn connector_tokens(&self) -> Option<&[String]> {
277 self.connector_tokens.as_deref()
278 }
279}
280
281#[derive(Debug, thiserror::Error)]
283#[error("timed out after {timeout_ms}ms waiting for market data and derived computations")]
284pub struct WaitReadyError {
285 timeout_ms: u64,
286}
287
288#[non_exhaustive]
290#[derive(Debug, thiserror::Error)]
291pub enum SolverBuildError {
292 #[error("failed to create ethereum RPC client: {0}")]
294 RpcClient(String),
295 #[error(transparent)]
297 AlgorithmConfig(#[from] AlgorithmError),
298 #[error("failed to create computation manager: {0}")]
300 ComputationManager(String),
301 #[error("failed to create encoder: {0}")]
303 Encoder(String),
304 #[error("failed to create router fee fetcher: {0}")]
306 RouterFeeFetcher(String),
307 #[error(transparent)]
309 UnknownAlgorithm(#[from] UnknownAlgorithmError),
310 #[error("gas token not configured for chain")]
312 GasToken,
313 #[error("no worker pools configured")]
315 NoPools,
316 #[error(
320 "every worker pool sets liquidity_scope = \"include_exclusive\"; requests without \
321 exclusive access would be served by no pool. Configure at least one public_only pool"
322 )]
323 NoPublicPool,
324 #[cfg(feature = "test-utils")]
326 #[error("replay failed: {0}")]
327 Replay(String),
328 #[error("feed setup failed before delivering pending processor: {0}")]
333 FeedSetup(String),
334 #[error("pending processor channel closed before processor was delivered")]
337 PendingChannelClosed,
338 #[cfg(feature = "experimental")]
341 #[error("step controller channel closed before controller was delivered")]
342 StepControllerChannelClosed,
343}
344
345enum PoolEntry {
347 BuiltIn {
348 name: String,
349 algorithm: String,
350 num_workers: usize,
351 task_queue_capacity: usize,
352 min_hops: usize,
353 max_hops: usize,
354 timeout_ms: u64,
355 max_routes: Option<usize>,
356 connector_tokens: Option<FxHashSet<Address>>,
357 liquidity_scope: Option<LiquidityScope>,
358 },
359 Custom(CustomPoolEntry),
360}
361
362impl PoolEntry {
363 fn liquidity_scope(&self) -> Option<LiquidityScope> {
365 match self {
366 PoolEntry::BuiltIn { liquidity_scope, .. } => *liquidity_scope,
367 PoolEntry::Custom(custom) => custom.liquidity_scope,
368 }
369 }
370}
371
372struct CustomPoolEntry {
374 name: String,
375 num_workers: usize,
376 task_queue_capacity: usize,
377 min_hops: usize,
378 max_hops: usize,
379 timeout_ms: u64,
380 max_routes: Option<usize>,
381 liquidity_scope: Option<LiquidityScope>,
382 configure: Box<dyn FnOnce(WorkerPoolBuilder) -> WorkerPoolBuilder + Send>,
384}
385
386struct BuiltComponents {
389 tycho_feed: TychoFeed,
390 gas_price_fetcher: GasPriceFetcher<EthereumRpcClient>,
391 router_fee_fetcher: Option<RouterFeeFetcher>,
392 computation_manager: ComputationManager,
393 computation_event_rx: broadcast::Receiver<MarketEvent>,
394 computation_shutdown_tx: broadcast::Sender<()>,
395 computation_shutdown_rx: broadcast::Receiver<()>,
396 router: WorkerPoolRouter,
397 worker_pools: Vec<WorkerPool>,
398 market_data: MarketData,
399 derived_data: SharedDerivedDataRef,
400 router_fees: SharedRouterFees,
401 chain: Chain,
402 router_address: Option<Bytes>,
403 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
404 market_event_tx: broadcast::Sender<MarketEvent>,
405}
406
407#[must_use = "a builder does nothing until .build() is called"]
412pub struct FyndBuilder {
413 chain: Chain,
414 tycho_url: String,
415 rpc_url: String,
416 protocols: Vec<String>,
417 min_tvl: f64,
418 tycho_api_key: Option<String>,
419 tycho_use_tls: bool,
420 min_token_quality: i32,
421 traded_n_days_ago: u64,
422 tvl_buffer_ratio: f64,
423 gas_refresh_interval: Duration,
424 reconnect_delay: Duration,
425 blocklisted_components: FxHashSet<String>,
426 partial_blocks: bool,
427 router_timeout: Duration,
428 router_min_responses: usize,
429 encoder: Option<Encoder>,
430 calldata_watermark: Option<Vec<u8>>,
431 pools: Vec<PoolEntry>,
432 price_guard_enabled: bool,
433 price_providers: Vec<Box<dyn PriceProvider>>,
434 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
435}
436
437impl FyndBuilder {
438 pub fn new(
440 chain: Chain,
441 tycho_url: impl Into<String>,
442 rpc_url: impl Into<String>,
443 protocols: Vec<String>,
444 min_tvl: f64,
445 ) -> Self {
446 Self {
447 chain,
448 tycho_url: tycho_url.into(),
449 rpc_url: rpc_url.into(),
450 protocols,
451 min_tvl,
452 tycho_api_key: None,
453 tycho_use_tls: DEFAULT_TYCHO_USE_TLS,
454 min_token_quality: defaults::MIN_TOKEN_QUALITY,
455 traded_n_days_ago: defaults::TRADED_N_DAYS_AGO,
456 tvl_buffer_ratio: defaults::TVL_BUFFER_RATIO,
457 gas_refresh_interval: defaults::GAS_REFRESH_INTERVAL,
458 reconnect_delay: defaults::RECONNECT_DELAY,
459 blocklisted_components: FxHashSet::default(),
460 partial_blocks: false,
461 router_timeout: DEFAULT_ROUTER_TIMEOUT,
462 router_min_responses: defaults::ROUTER_MIN_RESPONSES,
463 encoder: None,
464 calldata_watermark: None,
465 pools: Vec::new(),
466 price_guard_enabled: false,
467 price_providers: Vec::new(),
468 pending_indexers: Vec::new(),
469 }
470 }
471
472 pub fn chain(&self) -> Chain {
474 self.chain
475 }
476
477 pub fn tycho_api_key(mut self, key: impl Into<String>) -> Self {
479 self.tycho_api_key = Some(key.into());
480 self
481 }
482
483 pub fn min_tvl(mut self, min_tvl: f64) -> Self {
485 self.min_tvl = min_tvl;
486 self
487 }
488
489 pub fn tycho_use_tls(mut self, use_tls: bool) -> Self {
491 self.tycho_use_tls = use_tls;
492 self
493 }
494
495 pub fn min_token_quality(mut self, quality: i32) -> Self {
498 self.min_token_quality = quality;
499 self
500 }
501
502 pub fn traded_n_days_ago(mut self, days: u64) -> Self {
504 self.traded_n_days_ago = days;
505 self
506 }
507
508 pub fn tvl_buffer_ratio(mut self, ratio: f64) -> Self {
510 self.tvl_buffer_ratio = ratio;
511 self
512 }
513
514 pub fn gas_refresh_interval(mut self, interval: Duration) -> Self {
516 self.gas_refresh_interval = interval;
517 self
518 }
519
520 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
522 self.reconnect_delay = delay;
523 self
524 }
525
526 pub fn blocklisted_components(mut self, components: impl IntoIterator<Item = String>) -> Self {
528 self.blocklisted_components = components.into_iter().collect();
529 self
530 }
531
532 pub fn partial_blocks(mut self, enabled: bool) -> Self {
538 self.partial_blocks = enabled;
539 self
540 }
541
542 pub fn worker_router_timeout(mut self, timeout: Duration) -> Self {
544 self.router_timeout = timeout;
545 self
546 }
547
548 pub fn worker_router_min_responses(mut self, min: usize) -> Self {
550 self.router_min_responses = min;
551 self
552 }
553
554 pub fn encoder(mut self, encoder: Encoder) -> Self {
556 self.encoder = Some(encoder);
557 self
558 }
559
560 pub fn calldata_watermark(mut self, watermark: impl Into<Vec<u8>>) -> Self {
564 self.calldata_watermark = Some(watermark.into());
565 self
566 }
567
568 pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
570 self.pools.push(PoolEntry::BuiltIn {
571 name: "default".to_string(),
572 algorithm: algorithm.into(),
573 num_workers: num_cpus::get(),
574 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
575 min_hops: defaults::POOL_MIN_HOPS,
576 max_hops: defaults::POOL_MAX_HOPS,
577 timeout_ms: defaults::POOL_TIMEOUT_MS,
578 max_routes: None,
579 connector_tokens: None,
580 liquidity_scope: None,
581 });
582 self
583 }
584
585 pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
589 where
590 A: Algorithm + 'static,
591 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
592 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
593 {
594 let name = name.into();
595 let algo_name = name.clone();
596 let configure =
597 Box::new(move |builder: WorkerPoolBuilder| builder.with_algorithm(algo_name, factory));
598 self.pools
599 .push(PoolEntry::Custom(CustomPoolEntry {
600 name,
601 num_workers: num_cpus::get(),
602 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
603 min_hops: defaults::POOL_MIN_HOPS,
604 max_hops: defaults::POOL_MAX_HOPS,
605 timeout_ms: defaults::POOL_TIMEOUT_MS,
606 max_routes: None,
607 liquidity_scope: None,
608 configure,
609 }));
610 self
611 }
612
613 pub fn add_default_price_providers(self) -> Self {
620 self.register_price_provider(Box::new(
621 crate::price_guard::hyperliquid::HyperliquidProvider::default(),
622 ))
623 .register_price_provider(Box::new(
624 crate::price_guard::binance_ws::BinanceWsProvider::default(),
625 ))
626 }
627
628 pub fn register_price_provider(mut self, provider: Box<dyn PriceProvider>) -> Self {
633 self.price_providers.push(provider);
634 self
635 }
636
637 pub fn with_pending_indexer(
643 mut self,
644 extractor: impl Into<String>,
645 indexer: Box<dyn TxDeltaIndexer>,
646 ) -> Self {
647 self.pending_indexers
648 .push((extractor.into(), indexer));
649 self
650 }
651
652 pub fn price_guard_enabled(mut self, enabled: bool) -> Self {
659 self.price_guard_enabled = enabled;
660 self
661 }
662
663 pub fn add_pool(
670 mut self,
671 name: impl Into<String>,
672 config: &PoolConfig,
673 ) -> Result<Self, SolverBuildError> {
674 let connector_tokens = parse_connector_tokens(config.connector_tokens())?;
675 self.pools.push(PoolEntry::BuiltIn {
676 name: name.into(),
677 algorithm: config.algorithm().to_string(),
678 num_workers: config.num_workers(),
679 task_queue_capacity: config.task_queue_capacity(),
680 min_hops: config.min_hops(),
681 max_hops: config.max_hops(),
682 timeout_ms: config.timeout_ms(),
683 max_routes: config.max_routes(),
684 connector_tokens,
685 liquidity_scope: config.liquidity_scope(),
686 });
687 Ok(self)
688 }
689
690 fn assemble_components(mut self) -> Result<BuiltComponents, SolverBuildError> {
693 if self.pools.is_empty() {
694 return Err(SolverBuildError::NoPools);
695 }
696
697 if self
701 .pools
702 .iter()
703 .all(|p| p.liquidity_scope() == Some(LiquidityScope::IncludeExclusive))
704 {
705 return Err(SolverBuildError::NoPublicPool);
706 }
707
708 if self.price_providers.is_empty() {
710 self = self.add_default_price_providers();
711 }
712
713 let market_data = MarketData::new_shared();
714
715 let tycho_feed_config = TychoFeedConfig::new(
716 self.tycho_url,
717 self.chain,
718 self.tycho_api_key,
719 self.tycho_use_tls,
720 self.protocols,
721 self.min_tvl,
722 )
723 .tvl_buffer_ratio(self.tvl_buffer_ratio)
724 .reconnect_delay(self.reconnect_delay)
725 .min_token_quality(self.min_token_quality)
726 .traded_n_days_ago(self.traded_n_days_ago)
727 .blocklisted_components(self.blocklisted_components)
728 .partial_blocks(self.partial_blocks);
729
730 let ethereum_client = EthereumRpcClient::new(self.rpc_url.as_str())
731 .map_err(|e| SolverBuildError::RpcClient(e.to_string()))?;
732
733 let gas_price_fetcher =
734 GasPriceFetcher::new(ethereum_client, market_data.clone(), self.gas_refresh_interval);
735
736 let tycho_feed = TychoFeed::new(tycho_feed_config, market_data.clone());
737 let market_event_tx = tycho_feed.event_sender();
738
739 let gas_token = native_token(&self.chain).map_err(|_| SolverBuildError::GasToken)?;
740 let computation_config = ComputationManagerConfig::new()
741 .with_gas_token(gas_token)
742 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
743 let (computation_manager, _) =
746 ComputationManager::new(computation_config, market_data.clone())
747 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
748
749 let derived_data: SharedDerivedDataRef = computation_manager.store();
750 let derived_event_tx = computation_manager.event_sender();
751
752 let computation_event_rx = tycho_feed.subscribe();
755 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
756
757 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
758 let mut worker_pools: Vec<WorkerPool> = Vec::new();
759
760 let pools = std::mem::take(&mut self.pools);
761
762 for pool_entry in pools {
763 let pool_event_rx = tycho_feed.subscribe();
764 let derived_rx = derived_event_tx.subscribe();
765
766 let pool_scope = pool_entry
767 .liquidity_scope()
768 .unwrap_or_default();
769
770 let (worker_pool, task_handle) = match pool_entry {
771 PoolEntry::BuiltIn {
772 name,
773 algorithm,
774 num_workers,
775 task_queue_capacity,
776 min_hops,
777 max_hops,
778 timeout_ms,
779 max_routes,
780 connector_tokens,
781 liquidity_scope: _,
782 } => {
783 let mut algo_cfg = AlgorithmConfig::new(
784 min_hops,
785 max_hops,
786 Duration::from_millis(timeout_ms),
787 max_routes,
788 )?;
789 if let Some(tokens) = connector_tokens {
790 algo_cfg = algo_cfg.with_connector_tokens(tokens);
791 }
792 let builder = WorkerPoolBuilder::new()
793 .name(name)
794 .algorithm(algorithm)
795 .algorithm_config(algo_cfg)
796 .num_workers(num_workers)
797 .task_queue_capacity(task_queue_capacity)
798 .liquidity_scope(pool_scope);
799 builder.build(
800 market_data.clone(),
801 Arc::clone(&derived_data),
802 pool_event_rx,
803 derived_rx,
804 )?
805 }
806 PoolEntry::Custom(custom) => {
807 let algo_cfg = AlgorithmConfig::new(
808 custom.min_hops,
809 custom.max_hops,
810 Duration::from_millis(custom.timeout_ms),
811 custom.max_routes,
812 )?;
813 let builder = WorkerPoolBuilder::new()
814 .name(custom.name)
815 .algorithm_config(algo_cfg)
816 .num_workers(custom.num_workers)
817 .task_queue_capacity(custom.task_queue_capacity)
818 .liquidity_scope(pool_scope);
819 let builder = (custom.configure)(builder);
820 builder.build(
821 market_data.clone(),
822 Arc::clone(&derived_data),
823 pool_event_rx,
824 derived_rx,
825 )?
826 }
827 };
828
829 solver_pool_handles.push(
830 SolverPoolHandle::new(worker_pool.name(), task_handle)
831 .with_liquidity_scope(pool_scope),
832 );
833 worker_pools.push(worker_pool);
834 }
835
836 let encoder = match self.encoder {
837 Some(enc) => enc,
838 None => {
839 let registry = SwapEncoderRegistry::new(self.chain)
840 .add_default_encoders(None)
841 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
842 Encoder::new(self.chain, registry)
843 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?
844 }
845 };
846 let encoder = match self.calldata_watermark {
847 Some(watermark) => encoder.with_calldata_watermark(watermark),
848 None => encoder,
849 };
850
851 let chain = self.chain;
852 let router_address = encoder.router_address().cloned();
853 let router_fees = encoder.router_fees();
854
855 let router_fee_fetcher = match &router_address {
856 Some(addr) => Some(
857 RouterFeeFetcher::new(
858 self.rpc_url.as_str(),
859 addr,
860 router_fees.clone(),
861 defaults::ROUTER_FEE_REFRESH_INTERVAL,
862 )
863 .map_err(|e| SolverBuildError::RouterFeeFetcher(e.to_string()))?,
864 ),
865 None => {
866 tracing::warn!(
867 %chain,
868 "no Tycho router for this chain; running quote-only (encoding disabled)"
869 );
870 None
871 }
872 };
873
874 let router_config = WorkerPoolRouterConfig::default()
877 .with_timeout(self.router_timeout)
878 .with_min_responses(self.router_min_responses);
879 let mut router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
880
881 if self.price_guard_enabled {
882 let mut registry = PriceProviderRegistry::new();
883 let mut worker_handles = Vec::new();
884 for mut provider in self.price_providers {
885 worker_handles.push(provider.start(market_data.clone()));
886 registry = registry.register(provider);
887 }
888 let price_guard = PriceGuard::new(registry, worker_handles);
889 router = router.with_price_guard(price_guard);
890 }
891
892 Ok(BuiltComponents {
893 tycho_feed,
894 gas_price_fetcher,
895 router_fee_fetcher,
896 computation_manager,
897 computation_event_rx,
898 computation_shutdown_tx,
899 computation_shutdown_rx,
900 router,
901 worker_pools,
902 market_data,
903 derived_data,
904 router_fees,
905 chain,
906 router_address,
907 pending_indexers: self.pending_indexers,
908 market_event_tx,
909 })
910 }
911
912 pub fn build(self) -> Result<Solver, SolverBuildError> {
918 let mut c = self.assemble_components()?;
919
920 let feed_handle = tokio::spawn(async move {
921 if let Err(e) = c.tycho_feed.run().await {
922 metrics::counter!("tycho_feed_failures_total").increment(1);
923 tracing::error!(error = %e, "tycho feed error");
924 }
925 });
926 let gas_price_handle = tokio::spawn(async move {
927 c.gas_price_fetcher.run().await;
928 });
929 let metrics_sampler =
930 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
931 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
932 let router_fee_handle = match c.router_fee_fetcher {
933 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
934 None => tokio::spawn(async {}),
935 };
936 let computation_handle = tokio::spawn(async move {
937 c.computation_manager
938 .run(c.computation_event_rx, c.computation_shutdown_rx)
939 .await;
940 });
941
942 Ok(Solver {
943 router: c.router,
944 worker_pools: c.worker_pools,
945 market_data: c.market_data,
946 derived_data: c.derived_data,
947 router_fees: c.router_fees,
948 feed_handle,
949 gas_price_handle,
950 metrics_sampler_handle,
951 router_fee_handle,
952 computation_handle,
953 computation_shutdown_tx: c.computation_shutdown_tx,
954 chain: c.chain,
955 router_address: c.router_address,
956 market_event_tx: c.market_event_tx,
957 })
958 }
959
960 pub async fn build_with_pending(
972 self,
973 ) -> Result<(Solver, PendingBlockProcessor), SolverBuildError> {
974 let mut c = self.assemble_components()?;
975
976 let (pending_tx, pending_rx) =
977 tokio::sync::oneshot::channel::<Result<PendingBlockProcessor, String>>();
978
979 let pending_indexers = c.pending_indexers;
980 let feed_handle = tokio::spawn(async move {
981 if let Err(e) = c
982 .tycho_feed
983 .run_with_pending(pending_tx, pending_indexers)
984 .await
985 {
986 metrics::counter!("tycho_feed_failures_total").increment(1);
987 tracing::error!(error = %e, "tycho feed error");
988 }
989 });
990 let gas_price_handle = tokio::spawn(async move {
991 c.gas_price_fetcher.run().await;
992 });
993 let metrics_sampler =
994 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
995 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
996 let router_fee_handle = match c.router_fee_fetcher {
997 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
998 None => tokio::spawn(async {}),
999 };
1000 let computation_handle = tokio::spawn(async move {
1001 c.computation_manager
1002 .run(c.computation_event_rx, c.computation_shutdown_rx)
1003 .await;
1004 });
1005
1006 let pending = pending_rx
1007 .await
1008 .map_err(|_| SolverBuildError::PendingChannelClosed)?
1009 .map_err(SolverBuildError::FeedSetup)?;
1010
1011 Ok((
1012 Solver {
1013 router: c.router,
1014 worker_pools: c.worker_pools,
1015 market_data: c.market_data,
1016 derived_data: c.derived_data,
1017 router_fees: c.router_fees,
1018 feed_handle,
1019 gas_price_handle,
1020 metrics_sampler_handle,
1021 router_fee_handle,
1022 computation_handle,
1023 computation_shutdown_tx: c.computation_shutdown_tx,
1024 chain: c.chain,
1025 router_address: c.router_address,
1026 market_event_tx: c.market_event_tx,
1027 },
1028 pending,
1029 ))
1030 }
1031
1032 #[cfg(feature = "experimental")]
1047 pub async fn build_with_step_controller(
1048 self,
1049 ) -> Result<(Solver, BlockStepController), SolverBuildError> {
1050 let mut c = self.assemble_components()?;
1051
1052 let (controller_tx, controller_rx) =
1053 tokio::sync::oneshot::channel::<Result<BlockStepController, String>>();
1054
1055 let feed_handle = tokio::spawn(async move {
1056 if let Err(e) = c
1057 .tycho_feed
1058 .run_with_step_controller(controller_tx)
1059 .await
1060 {
1061 tracing::error!(error = %e, "tycho feed error");
1062 }
1063 });
1064 let gas_price_handle = tokio::spawn(async move {
1065 c.gas_price_fetcher.run().await;
1066 });
1067 let metrics_sampler =
1068 MetricsSampler::new(c.market_data.clone(), defaults::METRICS_SAMPLE_INTERVAL);
1069 let metrics_sampler_handle = tokio::spawn(async move { metrics_sampler.run().await });
1070 let router_fee_handle = match c.router_fee_fetcher {
1071 Some(fetcher) => tokio::spawn(async move { fetcher.run().await }),
1072 None => tokio::spawn(async {}),
1073 };
1074 let computation_handle = tokio::spawn(async move {
1075 c.computation_manager
1076 .run(c.computation_event_rx, c.computation_shutdown_rx)
1077 .await;
1078 });
1079
1080 let controller = controller_rx
1081 .await
1082 .map_err(|_| SolverBuildError::StepControllerChannelClosed)?
1083 .map_err(SolverBuildError::FeedSetup)?;
1084
1085 Ok((
1086 Solver {
1087 router: c.router,
1088 worker_pools: c.worker_pools,
1089 market_data: c.market_data,
1090 derived_data: c.derived_data,
1091 router_fees: c.router_fees,
1092 feed_handle,
1093 gas_price_handle,
1094 metrics_sampler_handle,
1095 router_fee_handle,
1096 computation_handle,
1097 computation_shutdown_tx: c.computation_shutdown_tx,
1098 chain: c.chain,
1099 router_address: c.router_address,
1100 market_event_tx: c.market_event_tx,
1101 },
1102 controller,
1103 ))
1104 }
1105} pub struct Solver {
1109 router: WorkerPoolRouter,
1110 worker_pools: Vec<WorkerPool>,
1111 market_data: MarketData,
1112 derived_data: SharedDerivedDataRef,
1113 router_fees: SharedRouterFees,
1114 feed_handle: JoinHandle<()>,
1115 gas_price_handle: JoinHandle<()>,
1116 metrics_sampler_handle: JoinHandle<()>,
1117 router_fee_handle: JoinHandle<()>,
1118 computation_handle: JoinHandle<()>,
1119 computation_shutdown_tx: broadcast::Sender<()>,
1120 chain: Chain,
1121 router_address: Option<Bytes>,
1122 market_event_tx: broadcast::Sender<MarketEvent>,
1123}
1124
1125impl Solver {
1126 pub fn market_data(&self) -> MarketData {
1128 self.market_data.clone()
1129 }
1130
1131 pub fn router_address(&self) -> Option<&Bytes> {
1133 self.router_address.as_ref()
1134 }
1135
1136 pub fn derived_data(&self) -> SharedDerivedDataRef {
1138 Arc::clone(&self.derived_data)
1139 }
1140
1141 pub fn subscribe_market_events(&self) -> broadcast::Receiver<crate::feed::events::MarketEvent> {
1146 self.market_event_tx.subscribe()
1147 }
1148
1149 pub async fn quote(&self, request: QuoteRequest) -> Result<Quote, SolveError> {
1159 self.router
1160 .quote(request, ExclusiveAccess::Granted)
1161 .await
1162 }
1163
1164 pub async fn wait_until_ready(&self, timeout: Duration) -> Result<(), WaitReadyError> {
1181 const POLL_INTERVAL: Duration = Duration::from_millis(500);
1182
1183 let deadline = tokio::time::Instant::now() + timeout;
1184
1185 loop {
1186 let market_ready = self
1187 .market_data
1188 .read()
1189 .await
1190 .last_updated()
1191 .is_some();
1192 let derived_ready = self
1193 .derived_data
1194 .read()
1195 .await
1196 .derived_data_ready();
1197
1198 if market_ready && derived_ready {
1199 return Ok(());
1200 }
1201
1202 if tokio::time::Instant::now() >= deadline {
1203 return Err(WaitReadyError { timeout_ms: timeout.as_millis() as u64 });
1204 }
1205
1206 tokio::time::sleep(POLL_INTERVAL).await;
1207 }
1208 }
1209
1210 #[cfg(feature = "test-utils")]
1223 pub async fn from_recording(
1224 chain: Chain,
1225 updates: Vec<tycho_simulation::protocol::models::Update>,
1226 pools: std::collections::HashMap<String, PoolConfig>,
1227 gas_price_wei: Option<num_bigint::BigUint>,
1228 ) -> Result<Self, SolverBuildError> {
1229 if pools.is_empty() {
1230 return Err(SolverBuildError::NoPools);
1231 }
1232
1233 let market_data = MarketData::new_shared();
1234
1235 let feed_config =
1237 TychoFeedConfig::new("ws://replay".to_string(), chain, None, false, vec![], 0.0);
1238 let feed = TychoFeed::new(feed_config, market_data.clone());
1239 let market_event_tx = feed.event_sender();
1240 let _feed_rx = feed.subscribe();
1241
1242 for update in updates {
1243 feed.handle_tycho_message(update)
1244 .await
1245 .map_err(|e| SolverBuildError::Replay(e.to_string()))?;
1246 }
1247
1248 let gas_price = match gas_price_wei {
1250 Some(price) => price,
1251 None => {
1252 tracing::warn!("no recorded gas price, defaulting to 10 gwei");
1253 num_bigint::BigUint::from(10_000_000_000u64)
1254 }
1255 };
1256 let block_number = match market_data.read().await.last_updated() {
1257 Some(block) => block.number(),
1258 None => {
1259 tracing::warn!("no block number from replayed updates, defaulting to 0");
1260 0
1261 }
1262 };
1263 {
1264 let mut market = market_data.write().await;
1265 market.update_gas_price(BlockGasPrice {
1266 block_number,
1267 block_hash: Default::default(),
1268 block_timestamp: 0,
1269 pricing: GasPrice::Legacy { gas_price },
1270 });
1271 }
1272
1273 let gas_token = native_token(&chain).map_err(|_| SolverBuildError::GasToken)?;
1275 let computation_config = ComputationManagerConfig::new()
1276 .with_gas_token(gas_token)
1277 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
1278 let (computation_manager, _) =
1279 ComputationManager::new(computation_config, market_data.clone())
1280 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
1281
1282 let derived_data: SharedDerivedDataRef = computation_manager.store();
1283 let derived_event_tx = computation_manager.event_sender();
1284
1285 let computation_event_rx = feed.subscribe();
1286 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
1287
1288 let computation_handle = tokio::spawn(async move {
1289 computation_manager
1290 .run(computation_event_rx, computation_shutdown_rx)
1291 .await;
1292 });
1293
1294 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
1296 let mut worker_pools: Vec<WorkerPool> = Vec::new();
1297 let mut max_timeout_ms = 0u64;
1298
1299 for (name, pool_cfg) in &pools {
1300 let algo_cfg = AlgorithmConfig::new(
1301 pool_cfg.min_hops(),
1302 pool_cfg.max_hops(),
1303 Duration::from_millis(pool_cfg.timeout_ms()),
1304 pool_cfg.max_routes(),
1305 )?;
1306
1307 let pool_event_rx = feed.subscribe();
1308 let derived_rx = derived_event_tx.subscribe();
1309
1310 let (worker_pool, task_handle) = WorkerPoolBuilder::new()
1311 .name(name.clone())
1312 .algorithm(pool_cfg.algorithm().to_string())
1313 .algorithm_config(algo_cfg)
1314 .num_workers(pool_cfg.num_workers())
1315 .task_queue_capacity(pool_cfg.task_queue_capacity())
1316 .build(market_data.clone(), Arc::clone(&derived_data), pool_event_rx, derived_rx)?;
1317
1318 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
1319 max_timeout_ms = max_timeout_ms.max(pool_cfg.timeout_ms());
1320 worker_pools.push(worker_pool);
1321 }
1322
1323 let encoder = {
1325 let registry = SwapEncoderRegistry::new(chain)
1326 .add_default_encoders(None)
1327 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1328 Encoder::new(chain, registry).map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1329 };
1330
1331 let router_address = encoder.router_address().cloned();
1332 let router_fees = encoder.router_fees();
1336 router_fees.set(crate::encoding::router_fees::RouterFees::new(
1337 100_000_000,
1338 0,
1339 0,
1340 rustc_hash::FxHashMap::default(),
1341 ));
1342 let router_config = WorkerPoolRouterConfig::default()
1343 .with_timeout(Duration::from_millis(max_timeout_ms.max(5000)))
1344 .with_min_responses(defaults::ROUTER_MIN_RESPONSES);
1345 let router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1346
1347 let market_read = market_data.read().await;
1349 let added = market_read.component_topology();
1350 drop(market_read);
1351
1352 if market_event_tx
1353 .send(MarketEvent::MarketUpdated {
1354 added_components: added,
1355 removed_components: vec![],
1356 updated_components: vec![],
1357 })
1358 .is_err()
1359 {
1360 tracing::warn!("no receivers for initial MarketUpdated broadcast");
1361 }
1362
1363 let feed_handle = tokio::spawn(futures::future::pending::<()>());
1366 let gas_price_handle = tokio::spawn(async { });
1367 let metrics_sampler_handle = tokio::spawn(async { });
1368 let router_fee_handle = tokio::spawn(async { });
1369
1370 Ok(Solver {
1371 router,
1372 worker_pools,
1373 market_data,
1374 derived_data,
1375 router_fees,
1376 feed_handle,
1377 gas_price_handle,
1378 metrics_sampler_handle,
1379 router_fee_handle,
1380 computation_handle,
1381 computation_shutdown_tx,
1382 chain,
1383 router_address,
1384 market_event_tx,
1385 })
1386 }
1387
1388 pub fn shutdown(self) {
1390 let _ = self.computation_shutdown_tx.send(());
1391 for pool in self.worker_pools {
1392 pool.shutdown();
1393 }
1394 self.feed_handle.abort();
1395 self.gas_price_handle.abort();
1396 self.metrics_sampler_handle.abort();
1397 self.router_fee_handle.abort();
1398 }
1399
1400 pub fn into_parts(self) -> SolverParts {
1402 SolverParts {
1403 router: self.router,
1404 worker_pools: self.worker_pools,
1405 market_data: self.market_data,
1406 derived_data: self.derived_data,
1407 router_fees: self.router_fees,
1408 feed_handle: self.feed_handle,
1409 gas_price_handle: self.gas_price_handle,
1410 metrics_sampler_handle: self.metrics_sampler_handle,
1411 router_fee_handle: self.router_fee_handle,
1412 computation_handle: self.computation_handle,
1413 computation_shutdown_tx: self.computation_shutdown_tx,
1414 chain: self.chain,
1415 router_address: self.router_address,
1416 }
1417 }
1418}
1419
1420pub struct SolverParts {
1424 router: WorkerPoolRouter,
1426 worker_pools: Vec<WorkerPool>,
1428 market_data: MarketData,
1430 derived_data: SharedDerivedDataRef,
1432 router_fees: SharedRouterFees,
1434 feed_handle: JoinHandle<()>,
1436 gas_price_handle: JoinHandle<()>,
1438 metrics_sampler_handle: JoinHandle<()>,
1440 router_fee_handle: JoinHandle<()>,
1442 computation_handle: JoinHandle<()>,
1444 computation_shutdown_tx: broadcast::Sender<()>,
1446 chain: Chain,
1448 router_address: Option<Bytes>,
1450}
1451
1452impl SolverParts {
1453 pub fn chain(&self) -> Chain {
1455 self.chain
1456 }
1457
1458 pub fn router_address(&self) -> Option<&Bytes> {
1460 self.router_address.as_ref()
1461 }
1462
1463 pub fn worker_pools(&self) -> &[WorkerPool] {
1465 &self.worker_pools
1466 }
1467
1468 pub fn market_data(&self) -> &MarketData {
1470 &self.market_data
1471 }
1472
1473 pub fn derived_data(&self) -> &SharedDerivedDataRef {
1475 &self.derived_data
1476 }
1477
1478 pub fn router_fees(&self) -> &SharedRouterFees {
1480 &self.router_fees
1481 }
1482
1483 pub fn into_router(self) -> WorkerPoolRouter {
1485 self.router
1486 }
1487
1488 #[allow(clippy::type_complexity)]
1490 pub fn into_components(
1491 self,
1492 ) -> (
1493 WorkerPoolRouter,
1494 Vec<WorkerPool>,
1495 MarketData,
1496 SharedDerivedDataRef,
1497 JoinHandle<()>,
1498 JoinHandle<()>,
1499 JoinHandle<()>,
1500 JoinHandle<()>,
1501 JoinHandle<()>,
1502 broadcast::Sender<()>,
1503 ) {
1504 (
1505 self.router,
1506 self.worker_pools,
1507 self.market_data,
1508 self.derived_data,
1509 self.feed_handle,
1510 self.gas_price_handle,
1511 self.metrics_sampler_handle,
1512 self.router_fee_handle,
1513 self.computation_handle,
1514 self.computation_shutdown_tx,
1515 )
1516 }
1517}
1518
1519#[cfg(test)]
1520mod tests {
1521 use super::*;
1522
1523 #[test]
1526 fn test_unscoped_pool_resolves_to_public_only() {
1527 let config = PoolConfig::new("most_liquid");
1528 assert_eq!(config.liquidity_scope(), None);
1529 assert_eq!(
1530 config
1531 .liquidity_scope()
1532 .unwrap_or_default(),
1533 LiquidityScope::PublicOnly
1534 );
1535 }
1536
1537 #[test]
1540 fn test_build_all_exclusive_pools() {
1541 let config =
1542 PoolConfig::new("most_liquid").with_liquidity_scope(LiquidityScope::IncludeExclusive);
1543 let result = FyndBuilder::new(
1544 Chain::Ethereum,
1545 "wss://example.invalid",
1546 "https://example.invalid",
1547 vec!["uniswap_v2".to_string()],
1548 100.0,
1549 )
1550 .add_pool("exclusive", &config)
1551 .expect("add_pool should accept the config")
1552 .build();
1553
1554 assert!(matches!(result, Err(SolverBuildError::NoPublicPool)));
1555 }
1556}