1use std::{collections::HashSet, str::FromStr, sync::Arc, time::Duration};
14
15use num_cpus;
16use serde::{Deserialize, Serialize};
17use tokio::{sync::broadcast, task::JoinHandle};
18use tycho_execution::encoding::evm::swap_encoder::swap_encoder_registry::SwapEncoderRegistry;
19#[cfg(feature = "experimental")]
20use tycho_simulation::evm::stream::BlockStepController;
21#[cfg(feature = "test-utils")]
22use tycho_simulation::tycho_ethereum::gas::{BlockGasPrice, GasPrice};
23use tycho_simulation::{
24 evm::pending::PendingBlockProcessor,
25 tycho_common::{models::Chain, traits::TxDeltaIndexer, Bytes},
26 tycho_core::models::Address,
27 tycho_ethereum::rpc::EthereumRpcClient,
28};
29
30use crate::{
31 algorithm::{AlgorithmConfig, AlgorithmError},
32 derived::{ComputationManager, ComputationManagerConfig, SharedDerivedDataRef},
33 encoding::{encoder::Encoder, fee_fetcher::RouterFeeFetcher, router_fees::SharedRouterFees},
34 feed::{
35 events::{MarketEvent, MarketEventHandler},
36 gas::GasPriceFetcher,
37 market_data::MarketData,
38 tycho_feed::TychoFeed,
39 TychoFeedConfig,
40 },
41 graph::EdgeWeightUpdaterWithDerived,
42 price_guard::{
43 guard::PriceGuard, provider::PriceProvider, provider_registry::PriceProviderRegistry,
44 },
45 types::constants::native_token,
46 worker_pool::{
47 pool::{WorkerPool, WorkerPoolBuilder},
48 registry::UnknownAlgorithmError,
49 },
50 worker_pool_router::{config::WorkerPoolRouterConfig, SolverPoolHandle, WorkerPoolRouter},
51 Algorithm, Quote, QuoteRequest, SolveError,
52};
53
54pub mod defaults {
60 use std::time::Duration;
61
62 pub const MIN_TOKEN_QUALITY: i32 = 100;
64 pub const TRADED_N_DAYS_AGO: u64 = 3;
66 pub const TVL_BUFFER_RATIO: f64 = 1.1;
68 pub const GAS_REFRESH_INTERVAL: Duration = Duration::from_secs(30);
70 pub const ROUTER_FEE_REFRESH_INTERVAL: Duration = Duration::from_secs(300);
72 pub const RECONNECT_DELAY: Duration = Duration::from_secs(5);
74 pub const ROUTER_MIN_RESPONSES: usize = 0;
77 pub const POOL_TASK_QUEUE_CAPACITY: usize = 1000;
79 pub const POOL_MIN_HOPS: usize = 1;
81 pub const POOL_MAX_HOPS: usize = 3;
83 pub const POOL_TIMEOUT_MS: u64 = 100;
85}
86
87const DEFAULT_TYCHO_USE_TLS: bool = true;
89const DEFAULT_DEPTH_SLIPPAGE_THRESHOLD: f64 = 0.01;
90const DEFAULT_ROUTER_TIMEOUT: Duration = Duration::from_secs(10);
93
94fn default_task_queue_capacity() -> usize {
97 defaults::POOL_TASK_QUEUE_CAPACITY
98}
99
100fn default_min_hops() -> usize {
101 defaults::POOL_MIN_HOPS
102}
103
104fn default_max_hops() -> usize {
105 defaults::POOL_MAX_HOPS
106}
107
108fn default_algo_timeout_ms() -> u64 {
109 defaults::POOL_TIMEOUT_MS
110}
111
112fn parse_connector_tokens(
113 raw: Option<&[String]>,
114) -> Result<Option<HashSet<Address>>, SolverBuildError> {
115 let Some(strings) = raw else {
116 return Ok(None);
117 };
118 let mut set = HashSet::with_capacity(strings.len());
119 for s in strings {
120 let addr = Address::from_str(s).map_err(|e| AlgorithmError::InvalidConfiguration {
121 reason: format!("connector_tokens: invalid address {s:?}: {e}"),
122 })?;
123 set.insert(addr);
124 }
125 Ok(Some(set))
126}
127
128#[must_use]
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct PoolConfig {
132 algorithm: String,
134 #[serde(default = "num_cpus::get")]
136 num_workers: usize,
137 #[serde(default = "default_task_queue_capacity")]
139 task_queue_capacity: usize,
140 #[serde(default = "default_min_hops")]
142 min_hops: usize,
143 #[serde(default = "default_max_hops")]
145 max_hops: usize,
146 #[serde(default = "default_algo_timeout_ms")]
148 timeout_ms: u64,
149 #[serde(default)]
151 max_routes: Option<usize>,
152 #[serde(default)]
155 connector_tokens: Option<Vec<String>>,
156}
157
158impl PoolConfig {
159 pub fn new(algorithm: impl Into<String>) -> Self {
161 Self {
162 algorithm: algorithm.into(),
163 num_workers: num_cpus::get(),
164 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
165 min_hops: defaults::POOL_MIN_HOPS,
166 max_hops: defaults::POOL_MAX_HOPS,
167 timeout_ms: defaults::POOL_TIMEOUT_MS,
168 max_routes: None,
169 connector_tokens: None,
170 }
171 }
172
173 pub fn algorithm(&self) -> &str {
175 &self.algorithm
176 }
177
178 pub fn num_workers(&self) -> usize {
180 self.num_workers
181 }
182
183 pub fn with_num_workers(mut self, num_workers: usize) -> Self {
185 self.num_workers = num_workers;
186 self
187 }
188
189 pub fn with_task_queue_capacity(mut self, task_queue_capacity: usize) -> Self {
191 self.task_queue_capacity = task_queue_capacity;
192 self
193 }
194
195 pub fn with_min_hops(mut self, min_hops: usize) -> Self {
197 self.min_hops = min_hops;
198 self
199 }
200
201 pub fn with_max_hops(mut self, max_hops: usize) -> Self {
203 self.max_hops = max_hops;
204 self
205 }
206
207 pub fn with_timeout_ms(mut self, timeout_ms: u64) -> Self {
209 self.timeout_ms = timeout_ms;
210 self
211 }
212
213 pub fn with_max_routes(mut self, max_routes: Option<usize>) -> Self {
215 self.max_routes = max_routes;
216 self
217 }
218
219 pub fn task_queue_capacity(&self) -> usize {
221 self.task_queue_capacity
222 }
223
224 pub fn min_hops(&self) -> usize {
226 self.min_hops
227 }
228
229 pub fn max_hops(&self) -> usize {
231 self.max_hops
232 }
233
234 pub fn timeout_ms(&self) -> u64 {
236 self.timeout_ms
237 }
238
239 pub fn max_routes(&self) -> Option<usize> {
241 self.max_routes
242 }
243
244 pub fn with_connector_tokens(mut self, tokens: Vec<String>) -> Self {
247 self.connector_tokens = Some(tokens);
248 self
249 }
250
251 pub fn connector_tokens(&self) -> Option<&[String]> {
253 self.connector_tokens.as_deref()
254 }
255}
256
257#[derive(Debug, thiserror::Error)]
259#[error("timed out after {timeout_ms}ms waiting for market data and derived computations")]
260pub struct WaitReadyError {
261 timeout_ms: u64,
262}
263
264#[non_exhaustive]
266#[derive(Debug, thiserror::Error)]
267pub enum SolverBuildError {
268 #[error("failed to create ethereum RPC client: {0}")]
270 RpcClient(String),
271 #[error(transparent)]
273 AlgorithmConfig(#[from] AlgorithmError),
274 #[error("failed to create computation manager: {0}")]
276 ComputationManager(String),
277 #[error("failed to create encoder: {0}")]
279 Encoder(String),
280 #[error("failed to create router fee fetcher: {0}")]
282 RouterFeeFetcher(String),
283 #[error(transparent)]
285 UnknownAlgorithm(#[from] UnknownAlgorithmError),
286 #[error("gas token not configured for chain")]
288 GasToken,
289 #[error("no worker pools configured")]
291 NoPools,
292 #[cfg(feature = "test-utils")]
294 #[error("replay failed: {0}")]
295 Replay(String),
296 #[error("feed setup failed before delivering pending processor: {0}")]
301 FeedSetup(String),
302 #[error("pending processor channel closed before processor was delivered")]
305 PendingChannelClosed,
306 #[cfg(feature = "experimental")]
309 #[error("step controller channel closed before controller was delivered")]
310 StepControllerChannelClosed,
311}
312
313enum PoolEntry {
315 BuiltIn {
316 name: String,
317 algorithm: String,
318 num_workers: usize,
319 task_queue_capacity: usize,
320 min_hops: usize,
321 max_hops: usize,
322 timeout_ms: u64,
323 max_routes: Option<usize>,
324 connector_tokens: Option<HashSet<Address>>,
325 },
326 Custom(CustomPoolEntry),
327}
328
329struct CustomPoolEntry {
331 name: String,
332 num_workers: usize,
333 task_queue_capacity: usize,
334 min_hops: usize,
335 max_hops: usize,
336 timeout_ms: u64,
337 max_routes: Option<usize>,
338 configure: Box<dyn FnOnce(WorkerPoolBuilder) -> WorkerPoolBuilder + Send>,
340}
341
342struct BuiltComponents {
345 tycho_feed: TychoFeed,
346 gas_price_fetcher: GasPriceFetcher<EthereumRpcClient>,
347 router_fee_fetcher: RouterFeeFetcher,
348 computation_manager: ComputationManager,
349 computation_event_rx: broadcast::Receiver<MarketEvent>,
350 computation_shutdown_tx: broadcast::Sender<()>,
351 computation_shutdown_rx: broadcast::Receiver<()>,
352 router: WorkerPoolRouter,
353 worker_pools: Vec<WorkerPool>,
354 market_data: MarketData,
355 derived_data: SharedDerivedDataRef,
356 router_fees: SharedRouterFees,
357 chain: Chain,
358 router_address: Bytes,
359 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
360 market_event_tx: broadcast::Sender<MarketEvent>,
361}
362
363#[must_use = "a builder does nothing until .build() is called"]
368pub struct FyndBuilder {
369 chain: Chain,
370 tycho_url: String,
371 rpc_url: String,
372 protocols: Vec<String>,
373 min_tvl: f64,
374 tycho_api_key: Option<String>,
375 tycho_use_tls: bool,
376 min_token_quality: i32,
377 traded_n_days_ago: u64,
378 tvl_buffer_ratio: f64,
379 gas_refresh_interval: Duration,
380 reconnect_delay: Duration,
381 blocklisted_components: HashSet<String>,
382 partial_blocks: bool,
383 router_timeout: Duration,
384 router_min_responses: usize,
385 encoder: Option<Encoder>,
386 pools: Vec<PoolEntry>,
387 price_guard_enabled: bool,
388 price_providers: Vec<Box<dyn PriceProvider>>,
389 pending_indexers: Vec<(String, Box<dyn TxDeltaIndexer>)>,
390}
391
392impl FyndBuilder {
393 pub fn new(
395 chain: Chain,
396 tycho_url: impl Into<String>,
397 rpc_url: impl Into<String>,
398 protocols: Vec<String>,
399 min_tvl: f64,
400 ) -> Self {
401 Self {
402 chain,
403 tycho_url: tycho_url.into(),
404 rpc_url: rpc_url.into(),
405 protocols,
406 min_tvl,
407 tycho_api_key: None,
408 tycho_use_tls: DEFAULT_TYCHO_USE_TLS,
409 min_token_quality: defaults::MIN_TOKEN_QUALITY,
410 traded_n_days_ago: defaults::TRADED_N_DAYS_AGO,
411 tvl_buffer_ratio: defaults::TVL_BUFFER_RATIO,
412 gas_refresh_interval: defaults::GAS_REFRESH_INTERVAL,
413 reconnect_delay: defaults::RECONNECT_DELAY,
414 blocklisted_components: HashSet::new(),
415 partial_blocks: false,
416 router_timeout: DEFAULT_ROUTER_TIMEOUT,
417 router_min_responses: defaults::ROUTER_MIN_RESPONSES,
418 encoder: None,
419 pools: Vec::new(),
420 price_guard_enabled: false,
421 price_providers: Vec::new(),
422 pending_indexers: Vec::new(),
423 }
424 }
425
426 pub fn chain(&self) -> Chain {
428 self.chain
429 }
430
431 pub fn tycho_api_key(mut self, key: impl Into<String>) -> Self {
433 self.tycho_api_key = Some(key.into());
434 self
435 }
436
437 pub fn min_tvl(mut self, min_tvl: f64) -> Self {
439 self.min_tvl = min_tvl;
440 self
441 }
442
443 pub fn tycho_use_tls(mut self, use_tls: bool) -> Self {
445 self.tycho_use_tls = use_tls;
446 self
447 }
448
449 pub fn min_token_quality(mut self, quality: i32) -> Self {
452 self.min_token_quality = quality;
453 self
454 }
455
456 pub fn traded_n_days_ago(mut self, days: u64) -> Self {
458 self.traded_n_days_ago = days;
459 self
460 }
461
462 pub fn tvl_buffer_ratio(mut self, ratio: f64) -> Self {
464 self.tvl_buffer_ratio = ratio;
465 self
466 }
467
468 pub fn gas_refresh_interval(mut self, interval: Duration) -> Self {
470 self.gas_refresh_interval = interval;
471 self
472 }
473
474 pub fn reconnect_delay(mut self, delay: Duration) -> Self {
476 self.reconnect_delay = delay;
477 self
478 }
479
480 pub fn blocklisted_components(mut self, components: HashSet<String>) -> Self {
482 self.blocklisted_components = components;
483 self
484 }
485
486 pub fn partial_blocks(mut self, enabled: bool) -> Self {
492 self.partial_blocks = enabled;
493 self
494 }
495
496 pub fn worker_router_timeout(mut self, timeout: Duration) -> Self {
498 self.router_timeout = timeout;
499 self
500 }
501
502 pub fn worker_router_min_responses(mut self, min: usize) -> Self {
504 self.router_min_responses = min;
505 self
506 }
507
508 pub fn encoder(mut self, encoder: Encoder) -> Self {
510 self.encoder = Some(encoder);
511 self
512 }
513
514 pub fn algorithm(mut self, algorithm: impl Into<String>) -> Self {
516 self.pools.push(PoolEntry::BuiltIn {
517 name: "default".to_string(),
518 algorithm: algorithm.into(),
519 num_workers: num_cpus::get(),
520 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
521 min_hops: defaults::POOL_MIN_HOPS,
522 max_hops: defaults::POOL_MAX_HOPS,
523 timeout_ms: defaults::POOL_TIMEOUT_MS,
524 max_routes: None,
525 connector_tokens: None,
526 });
527 self
528 }
529
530 pub fn with_algorithm<A, F>(mut self, name: impl Into<String>, factory: F) -> Self
534 where
535 A: Algorithm + 'static,
536 A::GraphManager: MarketEventHandler + EdgeWeightUpdaterWithDerived + 'static,
537 F: Fn(AlgorithmConfig) -> A + Clone + Send + Sync + 'static,
538 {
539 let name = name.into();
540 let algo_name = name.clone();
541 let configure =
542 Box::new(move |builder: WorkerPoolBuilder| builder.with_algorithm(algo_name, factory));
543 self.pools
544 .push(PoolEntry::Custom(CustomPoolEntry {
545 name,
546 num_workers: num_cpus::get(),
547 task_queue_capacity: defaults::POOL_TASK_QUEUE_CAPACITY,
548 min_hops: defaults::POOL_MIN_HOPS,
549 max_hops: defaults::POOL_MAX_HOPS,
550 timeout_ms: defaults::POOL_TIMEOUT_MS,
551 max_routes: None,
552 configure,
553 }));
554 self
555 }
556
557 pub fn add_default_price_providers(self) -> Self {
564 self.register_price_provider(Box::new(
565 crate::price_guard::hyperliquid::HyperliquidProvider::default(),
566 ))
567 .register_price_provider(Box::new(
568 crate::price_guard::binance_ws::BinanceWsProvider::default(),
569 ))
570 }
571
572 pub fn register_price_provider(mut self, provider: Box<dyn PriceProvider>) -> Self {
577 self.price_providers.push(provider);
578 self
579 }
580
581 pub fn with_pending_indexer(
587 mut self,
588 extractor: impl Into<String>,
589 indexer: Box<dyn TxDeltaIndexer>,
590 ) -> Self {
591 self.pending_indexers
592 .push((extractor.into(), indexer));
593 self
594 }
595
596 pub fn price_guard_enabled(mut self, enabled: bool) -> Self {
603 self.price_guard_enabled = enabled;
604 self
605 }
606
607 pub fn add_pool(
614 mut self,
615 name: impl Into<String>,
616 config: &PoolConfig,
617 ) -> Result<Self, SolverBuildError> {
618 let connector_tokens = parse_connector_tokens(config.connector_tokens())?;
619 self.pools.push(PoolEntry::BuiltIn {
620 name: name.into(),
621 algorithm: config.algorithm().to_string(),
622 num_workers: config.num_workers(),
623 task_queue_capacity: config.task_queue_capacity(),
624 min_hops: config.min_hops(),
625 max_hops: config.max_hops(),
626 timeout_ms: config.timeout_ms(),
627 max_routes: config.max_routes(),
628 connector_tokens,
629 });
630 Ok(self)
631 }
632
633 fn assemble_components(mut self) -> Result<BuiltComponents, SolverBuildError> {
636 if self.pools.is_empty() {
637 return Err(SolverBuildError::NoPools);
638 }
639
640 if self.price_providers.is_empty() {
642 self = self.add_default_price_providers();
643 }
644
645 let market_data = MarketData::new_shared();
646
647 let tycho_feed_config = TychoFeedConfig::new(
648 self.tycho_url,
649 self.chain,
650 self.tycho_api_key,
651 self.tycho_use_tls,
652 self.protocols,
653 self.min_tvl,
654 )
655 .tvl_buffer_ratio(self.tvl_buffer_ratio)
656 .reconnect_delay(self.reconnect_delay)
657 .min_token_quality(self.min_token_quality)
658 .traded_n_days_ago(self.traded_n_days_ago)
659 .blocklisted_components(self.blocklisted_components)
660 .partial_blocks(self.partial_blocks);
661
662 let ethereum_client = EthereumRpcClient::new(self.rpc_url.as_str())
663 .map_err(|e| SolverBuildError::RpcClient(e.to_string()))?;
664
665 let gas_price_fetcher =
666 GasPriceFetcher::new(ethereum_client, market_data.clone(), self.gas_refresh_interval);
667
668 let tycho_feed = TychoFeed::new(tycho_feed_config, market_data.clone());
669 let market_event_tx = tycho_feed.event_sender();
670
671 let gas_token = native_token(&self.chain).map_err(|_| SolverBuildError::GasToken)?;
672 let computation_config = ComputationManagerConfig::new()
673 .with_gas_token(gas_token)
674 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
675 let (computation_manager, _) =
678 ComputationManager::new(computation_config, market_data.clone())
679 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
680
681 let derived_data: SharedDerivedDataRef = computation_manager.store();
682 let derived_event_tx = computation_manager.event_sender();
683
684 let computation_event_rx = tycho_feed.subscribe();
686 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
687
688 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
689 let mut worker_pools: Vec<WorkerPool> = Vec::new();
690
691 for pool_entry in self.pools {
692 let pool_event_rx = tycho_feed.subscribe();
693 let derived_rx = derived_event_tx.subscribe();
694
695 let (worker_pool, task_handle) = match pool_entry {
696 PoolEntry::BuiltIn {
697 name,
698 algorithm,
699 num_workers,
700 task_queue_capacity,
701 min_hops,
702 max_hops,
703 timeout_ms,
704 max_routes,
705 connector_tokens,
706 } => {
707 let mut algo_cfg = AlgorithmConfig::new(
708 min_hops,
709 max_hops,
710 Duration::from_millis(timeout_ms),
711 max_routes,
712 )?;
713 if let Some(tokens) = connector_tokens {
714 algo_cfg = algo_cfg.with_connector_tokens(tokens);
715 }
716 WorkerPoolBuilder::new()
717 .name(name)
718 .algorithm(algorithm)
719 .algorithm_config(algo_cfg)
720 .num_workers(num_workers)
721 .task_queue_capacity(task_queue_capacity)
722 .build(
723 market_data.clone(),
724 Arc::clone(&derived_data),
725 pool_event_rx,
726 derived_rx,
727 )?
728 }
729 PoolEntry::Custom(custom) => {
730 let algo_cfg = AlgorithmConfig::new(
731 custom.min_hops,
732 custom.max_hops,
733 Duration::from_millis(custom.timeout_ms),
734 custom.max_routes,
735 )?;
736 let builder = WorkerPoolBuilder::new()
737 .name(custom.name)
738 .algorithm_config(algo_cfg)
739 .num_workers(custom.num_workers)
740 .task_queue_capacity(custom.task_queue_capacity);
741 let builder = (custom.configure)(builder);
742 builder.build(
743 market_data.clone(),
744 Arc::clone(&derived_data),
745 pool_event_rx,
746 derived_rx,
747 )?
748 }
749 };
750
751 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
752 worker_pools.push(worker_pool);
753 }
754
755 let encoder = match self.encoder {
756 Some(enc) => enc,
757 None => {
758 let registry = SwapEncoderRegistry::new(self.chain)
759 .add_default_encoders(None)
760 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
761 Encoder::new(self.chain, registry)
762 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?
763 }
764 };
765
766 let chain = self.chain;
767 let router_address = encoder.router_address().clone();
768 let router_fees = encoder.router_fees();
769
770 let router_fee_fetcher = RouterFeeFetcher::new(
771 self.rpc_url.as_str(),
772 &router_address,
773 router_fees.clone(),
774 defaults::ROUTER_FEE_REFRESH_INTERVAL,
775 )
776 .map_err(|e| SolverBuildError::RouterFeeFetcher(e.to_string()))?;
777
778 let router_config = WorkerPoolRouterConfig::default()
781 .with_timeout(self.router_timeout)
782 .with_min_responses(self.router_min_responses);
783 let mut router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
784
785 if self.price_guard_enabled {
786 let mut registry = PriceProviderRegistry::new();
787 let mut worker_handles = Vec::new();
788 for mut provider in self.price_providers {
789 worker_handles.push(provider.start(market_data.clone()));
790 registry = registry.register(provider);
791 }
792 let price_guard = PriceGuard::new(registry, worker_handles);
793 router = router.with_price_guard(price_guard);
794 }
795
796 Ok(BuiltComponents {
797 tycho_feed,
798 gas_price_fetcher,
799 router_fee_fetcher,
800 computation_manager,
801 computation_event_rx,
802 computation_shutdown_tx,
803 computation_shutdown_rx,
804 router,
805 worker_pools,
806 market_data,
807 derived_data,
808 router_fees,
809 chain,
810 router_address,
811 pending_indexers: self.pending_indexers,
812 market_event_tx,
813 })
814 }
815
816 pub fn build(self) -> Result<Solver, SolverBuildError> {
822 let mut c = self.assemble_components()?;
823
824 let feed_handle = tokio::spawn(async move {
825 if let Err(e) = c.tycho_feed.run().await {
826 tracing::error!(error = %e, "tycho feed error");
827 }
828 });
829 let gas_price_handle = tokio::spawn(async move {
830 c.gas_price_fetcher.run().await;
831 });
832 let router_fee_handle = tokio::spawn(async move {
833 c.router_fee_fetcher.run().await;
834 });
835 let computation_handle = tokio::spawn(async move {
836 c.computation_manager
837 .run(c.computation_event_rx, c.computation_shutdown_rx)
838 .await;
839 });
840
841 Ok(Solver {
842 router: c.router,
843 worker_pools: c.worker_pools,
844 market_data: c.market_data,
845 derived_data: c.derived_data,
846 router_fees: c.router_fees,
847 feed_handle,
848 gas_price_handle,
849 router_fee_handle,
850 computation_handle,
851 computation_shutdown_tx: c.computation_shutdown_tx,
852 chain: c.chain,
853 router_address: c.router_address,
854 market_event_tx: c.market_event_tx,
855 })
856 }
857
858 pub async fn build_with_pending(
870 self,
871 ) -> Result<(Solver, PendingBlockProcessor), SolverBuildError> {
872 let mut c = self.assemble_components()?;
873
874 let (pending_tx, pending_rx) =
875 tokio::sync::oneshot::channel::<Result<PendingBlockProcessor, String>>();
876
877 let pending_indexers = c.pending_indexers;
878 let feed_handle = tokio::spawn(async move {
879 if let Err(e) = c
880 .tycho_feed
881 .run_with_pending(pending_tx, pending_indexers)
882 .await
883 {
884 tracing::error!(error = %e, "tycho feed error");
885 }
886 });
887 let gas_price_handle = tokio::spawn(async move {
888 c.gas_price_fetcher.run().await;
889 });
890 let router_fee_handle = tokio::spawn(async move {
891 c.router_fee_fetcher.run().await;
892 });
893 let computation_handle = tokio::spawn(async move {
894 c.computation_manager
895 .run(c.computation_event_rx, c.computation_shutdown_rx)
896 .await;
897 });
898
899 let pending = pending_rx
900 .await
901 .map_err(|_| SolverBuildError::PendingChannelClosed)?
902 .map_err(SolverBuildError::FeedSetup)?;
903
904 Ok((
905 Solver {
906 router: c.router,
907 worker_pools: c.worker_pools,
908 market_data: c.market_data,
909 derived_data: c.derived_data,
910 router_fees: c.router_fees,
911 feed_handle,
912 gas_price_handle,
913 router_fee_handle,
914 computation_handle,
915 computation_shutdown_tx: c.computation_shutdown_tx,
916 chain: c.chain,
917 router_address: c.router_address,
918 market_event_tx: c.market_event_tx,
919 },
920 pending,
921 ))
922 }
923
924 #[cfg(feature = "experimental")]
939 pub async fn build_with_step_controller(
940 self,
941 ) -> Result<(Solver, BlockStepController), SolverBuildError> {
942 let mut c = self.assemble_components()?;
943
944 let (controller_tx, controller_rx) =
945 tokio::sync::oneshot::channel::<Result<BlockStepController, String>>();
946
947 let feed_handle = tokio::spawn(async move {
948 if let Err(e) = c
949 .tycho_feed
950 .run_with_step_controller(controller_tx)
951 .await
952 {
953 tracing::error!(error = %e, "tycho feed error");
954 }
955 });
956 let gas_price_handle = tokio::spawn(async move {
957 c.gas_price_fetcher.run().await;
958 });
959 let router_fee_handle = tokio::spawn(async move {
960 c.router_fee_fetcher.run().await;
961 });
962 let computation_handle = tokio::spawn(async move {
963 c.computation_manager
964 .run(c.computation_event_rx, c.computation_shutdown_rx)
965 .await;
966 });
967
968 let controller = controller_rx
969 .await
970 .map_err(|_| SolverBuildError::StepControllerChannelClosed)?
971 .map_err(SolverBuildError::FeedSetup)?;
972
973 Ok((
974 Solver {
975 router: c.router,
976 worker_pools: c.worker_pools,
977 market_data: c.market_data,
978 derived_data: c.derived_data,
979 router_fees: c.router_fees,
980 feed_handle,
981 gas_price_handle,
982 router_fee_handle,
983 computation_handle,
984 computation_shutdown_tx: c.computation_shutdown_tx,
985 chain: c.chain,
986 router_address: c.router_address,
987 market_event_tx: c.market_event_tx,
988 },
989 controller,
990 ))
991 }
992} pub struct Solver {
996 router: WorkerPoolRouter,
997 worker_pools: Vec<WorkerPool>,
998 market_data: MarketData,
999 derived_data: SharedDerivedDataRef,
1000 router_fees: SharedRouterFees,
1001 feed_handle: JoinHandle<()>,
1002 gas_price_handle: JoinHandle<()>,
1003 router_fee_handle: JoinHandle<()>,
1004 computation_handle: JoinHandle<()>,
1005 computation_shutdown_tx: broadcast::Sender<()>,
1006 chain: Chain,
1007 router_address: Bytes,
1008 market_event_tx: broadcast::Sender<MarketEvent>,
1009}
1010
1011impl Solver {
1012 pub fn market_data(&self) -> MarketData {
1014 self.market_data.clone()
1015 }
1016
1017 pub fn derived_data(&self) -> SharedDerivedDataRef {
1019 Arc::clone(&self.derived_data)
1020 }
1021
1022 pub fn subscribe_market_events(&self) -> broadcast::Receiver<crate::feed::events::MarketEvent> {
1027 self.market_event_tx.subscribe()
1028 }
1029
1030 pub async fn quote(&self, request: QuoteRequest) -> Result<Quote, SolveError> {
1036 self.router.quote(request).await
1037 }
1038
1039 pub async fn wait_until_ready(&self, timeout: Duration) -> Result<(), WaitReadyError> {
1056 const POLL_INTERVAL: Duration = Duration::from_millis(500);
1057
1058 let deadline = tokio::time::Instant::now() + timeout;
1059
1060 loop {
1061 let market_ready = self
1062 .market_data
1063 .read()
1064 .await
1065 .last_updated()
1066 .is_some();
1067 let derived_ready = self
1068 .derived_data
1069 .read()
1070 .await
1071 .derived_data_ready();
1072
1073 if market_ready && derived_ready {
1074 return Ok(());
1075 }
1076
1077 if tokio::time::Instant::now() >= deadline {
1078 return Err(WaitReadyError { timeout_ms: timeout.as_millis() as u64 });
1079 }
1080
1081 tokio::time::sleep(POLL_INTERVAL).await;
1082 }
1083 }
1084
1085 #[cfg(feature = "test-utils")]
1098 pub async fn from_recording(
1099 chain: Chain,
1100 updates: Vec<tycho_simulation::protocol::models::Update>,
1101 pools: std::collections::HashMap<String, PoolConfig>,
1102 gas_price_wei: Option<num_bigint::BigUint>,
1103 ) -> Result<Self, SolverBuildError> {
1104 if pools.is_empty() {
1105 return Err(SolverBuildError::NoPools);
1106 }
1107
1108 let market_data = MarketData::new_shared();
1109
1110 let feed_config =
1112 TychoFeedConfig::new("ws://replay".to_string(), chain, None, false, vec![], 0.0);
1113 let feed = TychoFeed::new(feed_config, market_data.clone());
1114 let market_event_tx = feed.event_sender();
1115 let _feed_rx = feed.subscribe();
1116
1117 for update in updates {
1118 feed.handle_tycho_message(update)
1119 .await
1120 .map_err(|e| SolverBuildError::Replay(e.to_string()))?;
1121 }
1122
1123 let gas_price = match gas_price_wei {
1125 Some(price) => price,
1126 None => {
1127 tracing::warn!("no recorded gas price, defaulting to 10 gwei");
1128 num_bigint::BigUint::from(10_000_000_000u64)
1129 }
1130 };
1131 let block_number = match market_data.read().await.last_updated() {
1132 Some(block) => block.number(),
1133 None => {
1134 tracing::warn!("no block number from replayed updates, defaulting to 0");
1135 0
1136 }
1137 };
1138 {
1139 let mut market = market_data.write().await;
1140 market.update_gas_price(BlockGasPrice {
1141 block_number,
1142 block_hash: Default::default(),
1143 block_timestamp: 0,
1144 pricing: GasPrice::Legacy { gas_price },
1145 });
1146 }
1147
1148 let gas_token = native_token(&chain).map_err(|_| SolverBuildError::GasToken)?;
1150 let computation_config = ComputationManagerConfig::new()
1151 .with_gas_token(gas_token)
1152 .with_depth_slippage_threshold(DEFAULT_DEPTH_SLIPPAGE_THRESHOLD);
1153 let (computation_manager, _) =
1154 ComputationManager::new(computation_config, market_data.clone())
1155 .map_err(|e| SolverBuildError::ComputationManager(e.to_string()))?;
1156
1157 let derived_data: SharedDerivedDataRef = computation_manager.store();
1158 let derived_event_tx = computation_manager.event_sender();
1159
1160 let computation_event_rx = feed.subscribe();
1161 let (computation_shutdown_tx, computation_shutdown_rx) = broadcast::channel(1);
1162
1163 let computation_handle = tokio::spawn(async move {
1164 computation_manager
1165 .run(computation_event_rx, computation_shutdown_rx)
1166 .await;
1167 });
1168
1169 let mut solver_pool_handles: Vec<SolverPoolHandle> = Vec::new();
1171 let mut worker_pools: Vec<WorkerPool> = Vec::new();
1172 let mut max_timeout_ms = 0u64;
1173
1174 for (name, pool_cfg) in &pools {
1175 let algo_cfg = AlgorithmConfig::new(
1176 pool_cfg.min_hops(),
1177 pool_cfg.max_hops(),
1178 Duration::from_millis(pool_cfg.timeout_ms()),
1179 pool_cfg.max_routes(),
1180 )?;
1181
1182 let pool_event_rx = feed.subscribe();
1183 let derived_rx = derived_event_tx.subscribe();
1184
1185 let (worker_pool, task_handle) = WorkerPoolBuilder::new()
1186 .name(name.clone())
1187 .algorithm(pool_cfg.algorithm().to_string())
1188 .algorithm_config(algo_cfg)
1189 .num_workers(pool_cfg.num_workers())
1190 .task_queue_capacity(pool_cfg.task_queue_capacity())
1191 .build(market_data.clone(), Arc::clone(&derived_data), pool_event_rx, derived_rx)?;
1192
1193 solver_pool_handles.push(SolverPoolHandle::new(worker_pool.name(), task_handle));
1194 max_timeout_ms = max_timeout_ms.max(pool_cfg.timeout_ms());
1195 worker_pools.push(worker_pool);
1196 }
1197
1198 let encoder = {
1200 let registry = SwapEncoderRegistry::new(chain)
1201 .add_default_encoders(None)
1202 .map_err(|e| SolverBuildError::Encoder(e.to_string()))?;
1203 Encoder::new(chain, registry).map_err(|e| SolverBuildError::Encoder(e.to_string()))?
1204 };
1205
1206 let router_address = encoder.router_address().clone();
1207 let router_fees = encoder.router_fees();
1211 router_fees.set(crate::encoding::router_fees::RouterFees::new(
1212 100_000_000,
1213 0,
1214 0,
1215 std::collections::HashMap::new(),
1216 ));
1217 let router_config = WorkerPoolRouterConfig::default()
1218 .with_timeout(Duration::from_millis(max_timeout_ms.max(5000)))
1219 .with_min_responses(defaults::ROUTER_MIN_RESPONSES);
1220 let router = WorkerPoolRouter::new(solver_pool_handles, router_config, encoder);
1221
1222 let market_read = market_data.read().await;
1224 let added = market_read.component_topology();
1225 drop(market_read);
1226
1227 if market_event_tx
1228 .send(MarketEvent::MarketUpdated {
1229 added_components: added,
1230 removed_components: vec![],
1231 updated_components: vec![],
1232 })
1233 .is_err()
1234 {
1235 tracing::warn!("no receivers for initial MarketUpdated broadcast");
1236 }
1237
1238 let feed_handle = tokio::spawn(futures::future::pending::<()>());
1241 let gas_price_handle = tokio::spawn(async { });
1242 let router_fee_handle = tokio::spawn(async { });
1243
1244 Ok(Solver {
1245 router,
1246 worker_pools,
1247 market_data,
1248 derived_data,
1249 router_fees,
1250 feed_handle,
1251 gas_price_handle,
1252 router_fee_handle,
1253 computation_handle,
1254 computation_shutdown_tx,
1255 chain,
1256 router_address,
1257 market_event_tx,
1258 })
1259 }
1260
1261 pub fn shutdown(self) {
1263 let _ = self.computation_shutdown_tx.send(());
1264 for pool in self.worker_pools {
1265 pool.shutdown();
1266 }
1267 self.feed_handle.abort();
1268 self.gas_price_handle.abort();
1269 self.router_fee_handle.abort();
1270 }
1271
1272 pub fn into_parts(self) -> SolverParts {
1274 SolverParts {
1275 router: self.router,
1276 worker_pools: self.worker_pools,
1277 market_data: self.market_data,
1278 derived_data: self.derived_data,
1279 router_fees: self.router_fees,
1280 feed_handle: self.feed_handle,
1281 gas_price_handle: self.gas_price_handle,
1282 router_fee_handle: self.router_fee_handle,
1283 computation_handle: self.computation_handle,
1284 computation_shutdown_tx: self.computation_shutdown_tx,
1285 chain: self.chain,
1286 router_address: self.router_address,
1287 }
1288 }
1289}
1290
1291pub struct SolverParts {
1295 router: WorkerPoolRouter,
1297 worker_pools: Vec<WorkerPool>,
1299 market_data: MarketData,
1301 derived_data: SharedDerivedDataRef,
1303 router_fees: SharedRouterFees,
1305 feed_handle: JoinHandle<()>,
1307 gas_price_handle: JoinHandle<()>,
1309 router_fee_handle: JoinHandle<()>,
1311 computation_handle: JoinHandle<()>,
1313 computation_shutdown_tx: broadcast::Sender<()>,
1315 chain: Chain,
1317 router_address: Bytes,
1319}
1320
1321impl SolverParts {
1322 pub fn chain(&self) -> Chain {
1324 self.chain
1325 }
1326
1327 pub fn router_address(&self) -> &Bytes {
1329 &self.router_address
1330 }
1331
1332 pub fn worker_pools(&self) -> &[WorkerPool] {
1334 &self.worker_pools
1335 }
1336
1337 pub fn market_data(&self) -> &MarketData {
1339 &self.market_data
1340 }
1341
1342 pub fn derived_data(&self) -> &SharedDerivedDataRef {
1344 &self.derived_data
1345 }
1346
1347 pub fn router_fees(&self) -> &SharedRouterFees {
1349 &self.router_fees
1350 }
1351
1352 pub fn into_router(self) -> WorkerPoolRouter {
1354 self.router
1355 }
1356
1357 #[allow(clippy::type_complexity)]
1359 pub fn into_components(
1360 self,
1361 ) -> (
1362 WorkerPoolRouter,
1363 Vec<WorkerPool>,
1364 MarketData,
1365 SharedDerivedDataRef,
1366 JoinHandle<()>,
1367 JoinHandle<()>,
1368 JoinHandle<()>,
1369 JoinHandle<()>,
1370 broadcast::Sender<()>,
1371 ) {
1372 (
1373 self.router,
1374 self.worker_pools,
1375 self.market_data,
1376 self.derived_data,
1377 self.feed_handle,
1378 self.gas_price_handle,
1379 self.router_fee_handle,
1380 self.computation_handle,
1381 self.computation_shutdown_tx,
1382 )
1383 }
1384}