tycho_simulation/evm/stream.rs
1//! Builder for configuring a multi-protocol stream.
2//!
3//! Provides a builder for creating a multi-protocol stream that produces
4//! protocol state update messages. It runs one synchronization worker per protocol
5//! and a supervisor that aggregates updates, ensuring gap‑free streaming
6//! and robust state tracking.
7//!
8//! ## Context
9//!
10//! This stream wraps a `TychoStream` from `tycho-client`. It decodes `FeedMessage`s
11//! into protocol state updates. Internally, each protocol runs in its own
12//! synchronization worker, and a supervisor aggregates their messages per block.
13//!
14//! ### Protocol Synchronization Worker
15//! A synchronization worker runs the snapshot + delta protocol from `tycho-indexer`.
16//! - It first downloads components and their snapshots.
17//! - It then streams deltas.
18//! - It reacts to new or paused components by pulling snapshots or removing them from the active
19//! set.
20//!
21//! Each worker emits snapshots and deltas to the supervisor.
22//!
23//! ### Stream Supervisor
24//! The supervisor aggregates worker messages by block and assigns sync status.
25//! - It ensures workers produce gap-free messages.
26//! - It flags late workers as `Delayed`, and marks them `Stale` if they exceed `max_missed_blocks`.
27//! - It marks workers with terminal errors as `Ended`.
28//!
29//! Aggregating by block adds small latency, since the supervisor waits briefly for
30//! all workers to emit. This latency only applies to workers in `Ready` or `Delayed`.
31//!
32//! The stream ends only when **all** workers are `Stale` or `Ended`.
33//!
34//! ## Configuration
35//!
36//! The builder lets you customize:
37//!
38//! ### Protocols
39//! Select which protocols to synchronize.
40//!
41//! ### Tokens & Minimum Token Quality
42//! Provide token metadata up front so the decoder can initialize protocol states from startup
43//! snapshots. `set_tokens` does not act as an ongoing filter — components arriving after startup
44//! include their own token metadata. To restrict processing to specific tokens, apply that filter
45//! in your consumer when reading `new_components`. New tokens arriving via stream deltas are added
46//! automatically when their quality exceeds `min_token_quality`.
47//!
48//! ### StreamEndPolicy
49//! Control when the stream ends based on worker states. By default, it ends when all
50//! workers are `Stale` or `Ended`.
51//!
52//! ## Stream
53//! The stream emits one protocol state update every `block_time`. Each update
54//! reports protocol synchronization states and any changes.
55//!
56//! The `new_components` field lists newly deployed components and their tokens.
57//!
58//! The stream aims to run indefinitely. Internal retry and reconnect logic handle
59//! most errors, so users should rarely need to restart it manually.
60//!
61//! ## Example
62//! ```no_run
63//! use tycho_common::models::Chain;
64//! use tycho_simulation::evm::stream::ProtocolStreamBuilder;
65//! use tycho_simulation::utils::load_all_tokens;
66//! use futures::StreamExt;
67//! use tycho_client::feed::component_tracker::ComponentFilter;
68//! use tycho_simulation::evm::protocol::uniswap_v2::state::UniswapV2State;
69//!
70//! #[tokio::main]
71//! async fn main() {
72//! let all_tokens = load_all_tokens(
73//! "tycho-beta.propellerheads.xyz",
74//! false,
75//! Some("sampletoken"),
76//! true,
77//! Chain::Ethereum,
78//! None,
79//! None,
80//! )
81//! .await
82//! .expect("Failed loading tokens");
83//!
84//! let protocol_stream =
85//! ProtocolStreamBuilder::new("tycho-beta.propellerheads.xyz", Chain::Ethereum)
86//! .auth_key(Some("sampletoken".to_string()))
87//! .skip_state_decode_failures(true)
88//! .exchange::<UniswapV2State>(
89//! "uniswap_v2", ComponentFilter::with_tvl_range(5.0, 10.0), None
90//! )
91//! .set_tokens(all_tokens)
92//! .await
93//! .build()
94//! .await
95//! .expect("Failed building protocol stream");
96//! tokio::pin!(protocol_stream);
97//!
98//! // Loop through block updates
99//! while let Some(msg) = protocol_stream.next().await {
100//! dbg!(msg).expect("failed decoding");
101//! }
102//! }
103//! ```
104use std::{
105 collections::{HashMap, HashSet},
106 sync::Arc,
107 time,
108};
109
110use futures::{future::Either, stream, Stream, StreamExt};
111use tokio_stream::wrappers::ReceiverStream;
112use tracing::{debug, error, warn};
113use tycho_client::{
114 feed::{
115 component_tracker::ComponentFilter, synchronizer::ComponentWithState, BlockHeader,
116 BlockSynchronizerError, FeedMessage, SynchronizerState,
117 },
118 stream::{RetryConfiguration, StreamError, TychoStreamBuilder},
119};
120use tycho_common::{
121 models::{token::Token, Chain},
122 simulation::protocol_sim::ProtocolSim,
123 traits::TxDeltaIndexer,
124 Bytes,
125};
126
127use crate::{
128 evm::{
129 decoder::{StreamDecodeError, TychoStreamDecoder},
130 override_stream::{self, StateOverrideProvider},
131 pending::PendingBlockProcessor,
132 protocol::{
133 native_wrapper::state::NativeWrapperState,
134 uniswap_v4::hooks::hook_handler_creator::initialize_hook_handlers,
135 },
136 },
137 protocol::{
138 errors::InvalidSnapshotError,
139 models::{DecoderContext, TryFromWithBlock, Update},
140 },
141 utils::default_blocklist,
142};
143
144const EXCHANGES_REQUIRING_FILTER: [&str; 4] = ["vm:balancer_v2", "fluid_v1", "erc4626", "ekubo_v3"];
145
146#[derive(Default, Debug, Clone, Copy)]
147pub enum StreamEndPolicy {
148 /// End stream if all states are Stale or Ended (default)
149 #[default]
150 AllEndedOrStale,
151 /// End stream if any protocol ended
152 AnyEnded,
153 /// End stream if any protocol ended or is stale
154 AnyEndedOrStale,
155 /// End stream if any protocol is stale
156 AnyStale,
157}
158
159impl StreamEndPolicy {
160 fn should_end<'a>(&self, states: impl IntoIterator<Item = &'a SynchronizerState>) -> bool {
161 let mut it = states.into_iter();
162 match self {
163 StreamEndPolicy::AllEndedOrStale => false,
164 StreamEndPolicy::AnyEnded => it.any(|s| matches!(s, SynchronizerState::Ended(_))),
165 StreamEndPolicy::AnyStale => it.any(|s| matches!(s, SynchronizerState::Stale(_))),
166 StreamEndPolicy::AnyEndedOrStale => {
167 it.any(|s| matches!(s, SynchronizerState::Stale(_) | SynchronizerState::Ended(_)))
168 }
169 }
170 }
171}
172
173/// Handle returned by [`ProtocolStreamBuilder::with_step_controller`] that gives external
174/// control over when each buffered block is released for decoding.
175///
176/// Intended for complex test scenarios where the caller needs to observe what the next
177/// block contains before allowing the decoder pipeline to process it.
178///
179/// ## Drop behaviour
180///
181/// Dropping this controller ungates the stream: the gating task detects the closed trigger
182/// channel, forwards the currently-buffered block (if any), then continues passing subsequent
183/// blocks through without waiting for triggers — exactly as if step-control had never been
184/// enabled. The stream runs to its natural end.
185pub struct BlockStepController {
186 /// Sends a trigger signal to release the next buffered block.
187 trigger_tx: tokio::sync::mpsc::UnboundedSender<()>,
188 /// Watch channel containing the next buffered raw message, or `None` if no block is pending.
189 peek_rx: tokio::sync::watch::Receiver<Option<FeedMessage<BlockHeader>>>,
190}
191
192impl BlockStepController {
193 /// Releases the next buffered block for decoding and emission.
194 ///
195 /// Returns an error if the stream has already ended and the sender is disconnected.
196 pub fn trigger_next_block(&self) -> Result<(), tokio::sync::mpsc::error::SendError<()>> {
197 // Send a unit value on the trigger channel to unblock the gating task.
198 self.trigger_tx.send(())
199 }
200
201 /// Returns the currently buffered block immediately, or `None` if no block is buffered yet.
202 pub fn try_peek_next_block(&self) -> Option<FeedMessage<BlockHeader>> {
203 self.peek_rx.borrow().clone()
204 }
205
206 /// Waits until a block is buffered and returns it without consuming it.
207 ///
208 /// Returns `None` only if the stream has ended and no further blocks will arrive.
209 /// If a block is already buffered when this is called, it returns immediately.
210 pub async fn peek_next_block(&self) -> Option<FeedMessage<BlockHeader>> {
211 // Clone so we don't hold a mutable borrow on self; wait_for checks the current
212 // value first, so this returns immediately if a block is already present.
213 let mut rx = self.peek_rx.clone();
214 let guard = rx
215 .wait_for(|v| v.is_some())
216 .await
217 .ok()?;
218 guard.clone()
219 }
220}
221
222/// Builds and configures the multi protocol stream described in the [module-level docs](self).
223///
224/// See the module documentation for details on protocols, configuration options, and
225/// stream behavior.
226pub struct ProtocolStreamBuilder {
227 decoder: TychoStreamDecoder<BlockHeader>,
228 stream_builder: TychoStreamBuilder,
229 stream_end_policy: StreamEndPolicy,
230 chain: Chain,
231 pending_indexers: HashMap<String, Box<dyn TxDeltaIndexer>>,
232 /// Watch sender used to publish the currently-buffered raw block so the controller can peek
233 /// at it before triggering. `Some` iff step-control mode is active.
234 step_peek_tx: Option<tokio::sync::watch::Sender<Option<FeedMessage<BlockHeader>>>>,
235 /// Receiver half of the trigger channel. Held here until `build()` / `build_with_pending()`
236 /// transfers ownership to the gating task. `Some` iff step-control mode is active.
237 step_trigger_rx: Option<tokio::sync::mpsc::UnboundedReceiver<()>>,
238 /// State-override providers explicitly registered by the consumer, keyed by `protocol_system`.
239 /// These take precedence over the built-in default registry and are installed onto the decoder
240 /// at build time.
241 override_providers: HashMap<String, Arc<dyn StateOverrideProvider>>,
242 /// Names of all exchanges registered on the builder, used to decide which built-in override
243 /// providers to auto-register at build time.
244 registered_exchanges: HashSet<String>,
245}
246
247impl ProtocolStreamBuilder {
248 /// Creates a new builder for a multi-protocol stream.
249 ///
250 /// The shipped pool blocklist is applied by default, excluding components known to break
251 /// simulation. Use [`blocklist_components`](Self::blocklist_components) to exclude additional
252 /// components.
253 ///
254 /// See the [module-level docs](self) for full details on stream behavior and configuration.
255 pub fn new(tycho_url: &str, chain: Chain) -> Self {
256 Self {
257 decoder: TychoStreamDecoder::new(),
258 stream_builder: TychoStreamBuilder::new(tycho_url, chain)
259 .blocklisted_ids(default_blocklist()),
260 stream_end_policy: StreamEndPolicy::default(),
261 chain,
262 pending_indexers: HashMap::new(),
263 step_peek_tx: None,
264 step_trigger_rx: None,
265 override_providers: HashMap::new(),
266 registered_exchanges: HashSet::new(),
267 }
268 }
269
270 /// Adds a specific exchange to the stream.
271 ///
272 /// This configures the builder to include a new protocol synchronizer for `name`,
273 /// filtering its components according to `filter` and optionally `filter_fn`.
274 ///
275 /// The type parameter `T` specifies the decoder type for this exchange. All
276 /// component states for this exchange will be decoded into instances of `T`.
277 ///
278 /// # Parameters
279 ///
280 /// - `name`: The protocol or exchange name (e.g., `"uniswap_v4"`, `"vm:balancer_v2"`).
281 /// - `filter`: Defines the set of components to include in the stream.
282 /// - `filter_fn`: Optional custom filter function for client-side filtering of components not
283 /// expressible in `filter`.
284 ///
285 /// # Notes
286 ///
287 /// For certain protocols (e.g., `"uniswap_v4"`, `"vm:balancer_v2"`, `"vm:curve"`), omitting
288 /// `filter_fn` may cause decoding errors or incorrect results. In these cases, a proper
289 /// filter function is required to ensure correct decoding and quoting logic.
290 pub fn exchange<T>(
291 mut self,
292 name: &str,
293 filter: ComponentFilter,
294 filter_fn: Option<fn(&ComponentWithState) -> bool>,
295 ) -> Self
296 where
297 T: ProtocolSim
298 + TryFromWithBlock<ComponentWithState, BlockHeader, Error = InvalidSnapshotError>
299 + Send
300 + 'static,
301 {
302 self.stream_builder = self
303 .stream_builder
304 .exchange(name, filter);
305 self.registered_exchanges
306 .insert(name.to_string());
307 self.decoder.register_decoder::<T>(name);
308 if let Some(predicate) = filter_fn {
309 self.decoder
310 .register_filter(name, predicate);
311 }
312
313 if EXCHANGES_REQUIRING_FILTER.contains(&name) && filter_fn.is_none() {
314 warn!(
315 "Warning: For exchange type '{}', it is necessary to set a filter function because not all pools are supported. See all filters at src/evm/protocol/filters.rs",
316 name
317 );
318 }
319
320 self
321 }
322
323 /// Adds a specific exchange to the stream with decoder context.
324 ///
325 /// This configures the builder to include a new protocol synchronizer for `name`,
326 /// filtering its components according to `filter` and optionally `filter_fn`. It also registers
327 /// the DecoderContext (this is useful to test protocols that are not live yet)
328 ///
329 /// The type parameter `T` specifies the decoder type for this exchange. All
330 /// component states for this exchange will be decoded into instances of `T`.
331 ///
332 /// # Parameters
333 ///
334 /// - `name`: The protocol or exchange name (e.g., `"uniswap_v4"`, `"vm:balancer_v2"`).
335 /// - `filter`: Defines the set of components to include in the stream.
336 /// - `filter_fn`: Optional custom filter function for client-side filtering of components not
337 /// expressible in `filter`.
338 /// - `decoder_context`: The decoder context for this exchange
339 ///
340 /// # Notes
341 ///
342 /// For certain protocols (e.g., `"uniswap_v4"`, `"vm:balancer_v2"`, `"vm:curve"`), omitting
343 /// `filter_fn` may cause decoding errors or incorrect results. In these cases, a proper
344 /// filter function is required to ensure correct decoding and quoting logic.
345 pub fn exchange_with_decoder_context<T>(
346 mut self,
347 name: &str,
348 filter: ComponentFilter,
349 filter_fn: Option<fn(&ComponentWithState) -> bool>,
350 decoder_context: DecoderContext,
351 ) -> Self
352 where
353 T: ProtocolSim
354 + TryFromWithBlock<ComponentWithState, BlockHeader, Error = InvalidSnapshotError>
355 + Send
356 + 'static,
357 {
358 self.stream_builder = self
359 .stream_builder
360 .exchange(name, filter);
361 self.registered_exchanges
362 .insert(name.to_string());
363 self.decoder
364 .register_decoder_with_context::<T>(name, decoder_context);
365 if let Some(predicate) = filter_fn {
366 self.decoder
367 .register_filter(name, predicate);
368 }
369
370 if EXCHANGES_REQUIRING_FILTER.contains(&name) && filter_fn.is_none() {
371 warn!(
372 "Warning: For exchange type '{}', it is necessary to set a filter function because not all pools are supported. See all filters at src/evm/protocol/filters.rs",
373 name
374 );
375 }
376
377 self
378 }
379
380 /// Sets the block time interval for the stream.
381 ///
382 /// This controls how often the stream produces updates.
383 pub fn block_time(mut self, block_time: u64) -> Self {
384 self.stream_builder = self
385 .stream_builder
386 .block_time(block_time);
387 self
388 }
389
390 /// Sets the network operation timeout (deprecated).
391 ///
392 /// Use [`latency_buffer()`](Self::latency_buffer) instead for controlling latency.
393 /// This method is retained for backwards compatibility.
394 #[deprecated = "Use latency_buffer instead"]
395 pub fn timeout(mut self, timeout: u64) -> Self {
396 self.stream_builder = self.stream_builder.timeout(timeout);
397 self
398 }
399
400 /// Sets the latency buffer to aggregate same-block messages.
401 ///
402 /// This allows the supervisor to wait a short interval for all synchronizers to emit
403 /// before aggregating.
404 pub fn latency_buffer(mut self, timeout: u64) -> Self {
405 self.stream_builder = self.stream_builder.timeout(timeout);
406 self
407 }
408
409 /// Sets the maximum number of blocks a synchronizer may miss before being marked as `Stale`.
410 pub fn max_missed_blocks(mut self, n: u64) -> Self {
411 self.stream_builder = self.stream_builder.max_missed_blocks(n);
412 self
413 }
414
415 /// Sets how long a synchronizer may take to process the initial message.
416 ///
417 /// Useful for data-intensive protocols where startup decoding takes longer.
418 pub fn startup_timeout(mut self, timeout: time::Duration) -> Self {
419 self.stream_builder = self
420 .stream_builder
421 .startup_timeout(timeout);
422 self
423 }
424
425 /// Configures the stream to exclude state updates.
426 ///
427 /// This reduces bandwidth and decoding workload if protocol state is not of
428 /// interest (e.g. only process new tokens).
429 pub fn no_state(mut self, no_state: bool) -> Self {
430 self.stream_builder = self.stream_builder.no_state(no_state);
431 self
432 }
433
434 /// Sets the API key for authenticating with the Tycho server.
435 pub fn auth_key(mut self, auth_key: Option<String>) -> Self {
436 self.stream_builder = self.stream_builder.auth_key(auth_key);
437 self
438 }
439
440 /// Disables TLS/ SSL for the connection, using http and ws protocols.
441 ///
442 /// This is not recommended for production use.
443 pub fn no_tls(mut self, no_tls: bool) -> Self {
444 self.stream_builder = self.stream_builder.no_tls(no_tls);
445 self
446 }
447
448 /// Disable compression for the connection.
449 pub fn disable_compression(mut self) -> Self {
450 self.stream_builder = self
451 .stream_builder
452 .disable_compression();
453 self
454 }
455
456 /// Enables partial block updates (flashblocks).
457 pub fn enable_partial_blocks(mut self) -> Self {
458 self.stream_builder = self
459 .stream_builder
460 .enable_partial_blocks();
461 self
462 }
463
464 /// Exclude additional component IDs from all registered exchanges.
465 ///
466 /// These IDs are added to the shipped blocklist that is already applied by default (see
467 /// [`new`](Self::new)).
468 pub fn blocklist_components(mut self, ids: HashSet<String>) -> Self {
469 if !ids.is_empty() {
470 tracing::info!("Blocklisting {} components", ids.len());
471 self.stream_builder = self.stream_builder.blocklisted_ids(ids);
472 }
473 self
474 }
475
476 /// Sets the stream end policy.
477 ///
478 /// Controls when the stream should stop based on synchronizer states.
479 ///
480 /// ## Note
481 /// The stream always ends latest if all protocols are stale or ended independent of
482 /// this configuration. This allows you to end the stream earlier than that.
483 ///
484 /// See [self::StreamEndPolicy] for possible configuration options.
485 pub fn stream_end_policy(mut self, stream_end_policy: StreamEndPolicy) -> Self {
486 self.stream_end_policy = stream_end_policy;
487 self
488 }
489
490 /// Provides token metadata used to decode startup snapshots and initialize protocol states.
491 ///
492 /// This is not a stream filter — components arriving after startup include their own token
493 /// metadata. To restrict to specific tokens, filter in your consumer logic. New tokens
494 /// arriving via stream deltas are added automatically if they meet the quality threshold.
495 pub async fn set_tokens(self, tokens: HashMap<Bytes, Token>) -> Self {
496 self.decoder.set_tokens(tokens).await;
497 self
498 }
499
500 /// Skips decoding errors for component state updates.
501 ///
502 /// Allows the stream to continue processing even if some states fail to decode,
503 /// logging a warning instead of panicking.
504 pub fn skip_state_decode_failures(mut self, skip: bool) -> Self {
505 self.decoder
506 .skip_state_decode_failures(skip);
507 self
508 }
509
510 /// Sets the minimum token quality for tokens added via the stream.
511 ///
512 /// Tokens arriving in stream deltas below this threshold are ignored. Defaults to 100.
513 /// Set this to the same value used in [`load_all_tokens()`](crate::utils::load_all_tokens) to
514 /// apply consistent filtering.
515 pub fn min_token_quality(mut self, quality: u32) -> Self {
516 self.decoder.min_token_quality(quality);
517 self
518 }
519
520 /// Configures the retry policy for websocket reconnects.
521 pub fn websocket_retry_config(mut self, config: &RetryConfiguration) -> Self {
522 self.stream_builder = self
523 .stream_builder
524 .websockets_retry_config(config);
525 self
526 }
527
528 /// Configures the retry policy for state synchronization.
529 pub fn state_synchronizer_retry_config(mut self, config: &RetryConfiguration) -> Self {
530 self.stream_builder = self
531 .stream_builder
532 .state_synchronizer_retry_config(config);
533 self
534 }
535
536 pub fn get_decoder(&self) -> &TychoStreamDecoder<BlockHeader> {
537 &self.decoder
538 }
539
540 /// Registers a [`TxDeltaIndexer`] for ephemeral pending-block simulation.
541 ///
542 /// The indexer is associated with `extractor` (the protocol synchronizer name, e.g.
543 /// `"uniswap_v3"`). Use [`build_with_pending`](Self::build_with_pending) to obtain both
544 /// the confirmed stream and the pending processor.
545 ///
546 /// Returns an error if `extractor` names a VM protocol (prefix `"vm:"`), which requires
547 /// `update_engine()` and cannot be simulated natively.
548 pub fn with_pending_indexer(
549 mut self,
550 extractor: &str,
551 indexer: Box<dyn TxDeltaIndexer>,
552 ) -> Result<Self, StreamError> {
553 if extractor.starts_with("vm:") {
554 return Err(StreamError::SetUpError(format!(
555 "extractor '{extractor}' is a VM protocol; TxDeltaIndexer only supports native protocols"
556 )));
557 }
558 self.pending_indexers
559 .insert(extractor.to_string(), indexer);
560 Ok(self)
561 }
562
563 /// Enables controlled-step mode for testing.
564 ///
565 /// Returns a [`BlockStepController`] that lets the caller decide when each buffered block
566 /// is released for decoding. Call this before [`build`](Self::build) or
567 /// [`build_with_pending`](Self::build_with_pending) — both detect and wire up the gating
568 /// automatically.
569 ///
570 /// In production code, do not call this method; the stream runs at full speed.
571 pub fn with_step_controller(mut self) -> (Self, BlockStepController) {
572 let (trigger_tx, trigger_rx) = tokio::sync::mpsc::unbounded_channel::<()>();
573 let (peek_tx, peek_rx) =
574 tokio::sync::watch::channel::<Option<FeedMessage<BlockHeader>>>(None);
575
576 self.step_peek_tx = Some(peek_tx);
577 self.step_trigger_rx = Some(trigger_rx);
578
579 let controller = BlockStepController { trigger_tx, peek_rx };
580 (self, controller)
581 }
582
583 /// Spawns a background task that gates `FeedMessage` delivery.
584 ///
585 /// The task buffers each incoming message, publishes it to `peek_tx` so the
586 /// [`BlockStepController`] can inspect it, waits for a trigger, then forwards the message to
587 /// `output_tx` for the decode pipeline. If `advance_tx` is `Some`, a clone of the message is
588 /// also forwarded there (used by the pending-processor path) before the decode step.
589 /// When the input channel closes or a terminal error is received according to
590 /// `stream_end_policy`, the task exits and all output channels are dropped.
591 fn run_gating_task(
592 raw_rx: tokio::sync::mpsc::Receiver<
593 Result<FeedMessage<BlockHeader>, BlockSynchronizerError>,
594 >,
595 mut trigger_rx: tokio::sync::mpsc::UnboundedReceiver<()>,
596 peek_tx: tokio::sync::watch::Sender<Option<FeedMessage<BlockHeader>>>,
597 output_tx: tokio::sync::mpsc::Sender<FeedMessage<BlockHeader>>,
598 stream_end_policy: StreamEndPolicy,
599 ) {
600 tokio::spawn(async move {
601 let mut raw_stream = ReceiverStream::new(raw_rx);
602 loop {
603 let msg = match raw_stream.next().await {
604 Some(Ok(msg)) => msg,
605 Some(Err(e)) => {
606 error!("Block stream ended with terminal error: {e}");
607 break;
608 }
609 None => break,
610 };
611
612 if stream_end_policy.should_end(msg.sync_states.values()) {
613 error!(
614 "Block stream ended due to {:?}: {:?}",
615 stream_end_policy, msg.sync_states
616 );
617 break;
618 }
619
620 // Publish the buffered message so the caller can peek before triggering.
621 let _ = peek_tx.send(Some(msg.clone()));
622
623 // Block until the controller fires trigger_next_block(), or until it is dropped.
624 if trigger_rx.recv().await.is_none() {
625 // Controller dropped — forward the buffered message and drain the rest
626 // without gating, so the stream continues to its natural end.
627 let _ = peek_tx.send(None);
628 if output_tx.send(msg).await.is_err() {
629 break;
630 }
631 while let Some(item) = raw_stream.next().await {
632 let Ok(msg) = item else { break };
633 if stream_end_policy.should_end(msg.sync_states.values()) {
634 break;
635 }
636 if output_tx.send(msg).await.is_err() {
637 break;
638 }
639 }
640 break;
641 }
642
643 // Clear the peek slot before decoding so callers see None between blocks.
644 let _ = peek_tx.send(None);
645
646 if output_tx.send(msg).await.is_err() {
647 break;
648 }
649 }
650 });
651 }
652
653 /// Registers `provider` as the live override source for `protocol_system`.
654 ///
655 /// Explicit registrations take precedence over the built-in default registry, so this is how
656 /// you swap a venue (e.g. `vm:bopamm`) onto a different provider. Registering the same provider
657 /// for several protocols is cheap — it is shared via `Arc`, not duplicated.
658 pub fn with_override_provider(
659 mut self,
660 protocol_system: impl Into<String>,
661 provider: Arc<dyn StateOverrideProvider>,
662 ) -> Self {
663 self.override_providers
664 .insert(protocol_system.into(), provider);
665 self
666 }
667
668 /// Installs override providers onto the decoder before the stream is built.
669 ///
670 /// Explicit consumer registrations win; the built-in default registry (see
671 /// [`default_override_providers`](crate::evm::override_stream::default_override_providers))
672 /// then fills every remaining protocol it can serve.
673 fn install_override_providers(&mut self) {
674 let explicit = std::mem::take(&mut self.override_providers);
675 // Protocols eligible for a built-in default provider: registered exchanges not explicitly
676 // overridden by the consumer.
677 let uncovered = self
678 .registered_exchanges
679 .clone()
680 .into_iter()
681 .filter(|exchange| !explicit.contains_key(exchange));
682 let defaults = override_stream::default_override_providers(uncovered);
683 for (protocol_system, provider) in defaults.into_iter().chain(explicit) {
684 self.decoder
685 .set_override_provider(protocol_system, provider);
686 }
687 }
688
689 /// Builds the confirmed protocol stream and a [`PendingBlockProcessor`] that stays
690 /// in sync with it automatically.
691 ///
692 /// The stream pipeline forwards every confirmed [`FeedMessage`] to the processor via an
693 /// internal unbounded channel — it never blocks waiting for the consumer. The consumer
694 /// owns the returned `PendingBlockProcessor` exclusively and may wrap it in whatever
695 /// synchronisation primitive suits their use case (e.g. `Mutex` for shared access,
696 /// nothing for single-threaded use).
697 ///
698 /// Call [`generate_pending_update`](PendingBlockProcessor::generate_pending_update) to
699 /// simulate a candidate bundle; it drains the channel automatically before computing.
700 pub async fn build_with_pending(
701 mut self,
702 ) -> Result<
703 (impl Stream<Item = Result<Update, StreamDecodeError>>, PendingBlockProcessor),
704 StreamError,
705 > {
706 initialize_hook_handlers().map_err(|e| {
707 StreamError::SetUpError(format!("Error initializing hook handlers: {e:?}"))
708 })?;
709 self.install_override_providers();
710 let (_, rx) = self.stream_builder.build().await?;
711 let decoder = Arc::new(self.decoder);
712
713 let (advance_tx, advance_rx) =
714 tokio::sync::mpsc::unbounded_channel::<FeedMessage<BlockHeader>>();
715 let pending = PendingBlockProcessor::new(
716 self.pending_indexers,
717 decoder.clone(),
718 self.chain,
719 advance_rx,
720 );
721
722 let chain = self.chain;
723 let stream_end_policy = self.stream_end_policy;
724
725 let decode_stream: Box<dyn Stream<Item = FeedMessage<BlockHeader>> + Send + Unpin> =
726 if let (Some(peek_tx), Some(trigger_rx)) = (self.step_peek_tx, self.step_trigger_rx) {
727 let (gated_tx, gated_rx) =
728 tokio::sync::mpsc::channel::<FeedMessage<BlockHeader>>(1);
729 Self::run_gating_task(rx, trigger_rx, peek_tx, gated_tx, stream_end_policy);
730 Box::new(ReceiverStream::new(gated_rx))
731 } else {
732 let normal = ReceiverStream::new(rx)
733 .take_while(move |msg| match msg {
734 Ok(msg) => {
735 let states = msg.sync_states.values();
736 if stream_end_policy.should_end(states) {
737 error!(
738 "Block stream ended due to {:?}: {:?}",
739 stream_end_policy, msg.sync_states
740 );
741 futures::future::ready(false)
742 } else {
743 futures::future::ready(true)
744 }
745 }
746 Err(e) => {
747 error!("Block stream ended with terminal error: {e}");
748 futures::future::ready(false)
749 }
750 })
751 .map(|msg| msg.expect("Safe since stream ends if we receive an error"));
752 Box::new(Box::pin(normal))
753 };
754
755 let stream = Box::pin(decode_stream.then({
756 let decoder = decoder.clone();
757 move |msg| {
758 let decoder = decoder.clone();
759 let advance_tx = advance_tx.clone();
760 async move {
761 let _ = advance_tx.send(msg.clone());
762 decoder.decode(&msg).await.map_err(|e| {
763 debug!(msg=?msg, "Decode error: {}", e);
764 e
765 })
766 }
767 }
768 }));
769 let stream = inject_native_wrapper(stream, chain);
770 Ok((stream, pending))
771 }
772
773 /// Builds and returns the configured protocol stream.
774 ///
775 /// See the module-level docs for details on stream behavior and emitted messages.
776 /// This method applies all builder settings and starts the stream.
777 pub async fn build(
778 mut self,
779 ) -> Result<impl Stream<Item = Result<Update, StreamDecodeError>>, StreamError> {
780 initialize_hook_handlers().map_err(|e| {
781 StreamError::SetUpError(format!("Error initializing hook handlers: {e:?}"))
782 })?;
783 self.install_override_providers();
784 let (_, rx) = self.stream_builder.build().await?;
785 let decoder = Arc::new(self.decoder);
786 let chain = self.chain;
787 let stream_end_policy = self.stream_end_policy;
788
789 let decode_stream: Box<dyn Stream<Item = FeedMessage<BlockHeader>> + Send + Unpin> =
790 if let (Some(peek_tx), Some(trigger_rx)) = (self.step_peek_tx, self.step_trigger_rx) {
791 let (gated_tx, gated_rx) =
792 tokio::sync::mpsc::channel::<FeedMessage<BlockHeader>>(1);
793 Self::run_gating_task(rx, trigger_rx, peek_tx, gated_tx, stream_end_policy);
794 Box::new(ReceiverStream::new(gated_rx))
795 } else {
796 let normal = ReceiverStream::new(rx)
797 .take_while(move |msg| match msg {
798 Ok(msg) => {
799 let states = msg.sync_states.values();
800 if stream_end_policy.should_end(states) {
801 error!(
802 "Block stream ended due to {:?}: {:?}",
803 stream_end_policy, msg.sync_states
804 );
805 futures::future::ready(false)
806 } else {
807 futures::future::ready(true)
808 }
809 }
810 Err(e) => {
811 error!("Block stream ended with terminal error: {e}");
812 futures::future::ready(false)
813 }
814 })
815 .map(|msg| msg.expect("Safe since stream ends if we receive an error"));
816 Box::new(Box::pin(normal))
817 };
818
819 let stream = Box::pin(decode_stream.then({
820 let decoder = decoder.clone();
821 move |msg| {
822 let decoder = decoder.clone();
823 async move {
824 decoder.decode(&msg).await.map_err(|e| {
825 debug!(msg=?msg, "Decode error: {}", e);
826 e
827 })
828 }
829 }
830 }));
831 let stream = inject_native_wrapper(stream, chain);
832 Ok(stream)
833 }
834}
835
836/// Wraps a decoded protocol stream to inject a `NativeWrapperState` component
837/// on the first successful update.
838///
839/// Skips injection for chains where the native and wrapped-native tokens share
840/// the same address (e.g. Starknet).
841fn inject_native_wrapper(
842 inner: impl Stream<Item = Result<Update, StreamDecodeError>> + Unpin + Send + 'static,
843 chain: Chain,
844) -> impl Stream<Item = Result<Update, StreamDecodeError>> + Send {
845 let has_distinct_wrapper = chain.native_token().address != chain.wrapped_native_token().address;
846 if !has_distinct_wrapper {
847 return Either::Left(inner);
848 }
849
850 Either::Right(
851 stream::once(async move {
852 let mut inner = inner;
853 let first = inner.next().await;
854 let modified = first.into_iter().map(move |result| {
855 result.map(|mut update| {
856 let component = NativeWrapperState::component(chain);
857 let id = component.id.to_string();
858 update
859 .new_pairs
860 .insert(id.clone(), component);
861 update
862 .states
863 .insert(id, Box::new(NativeWrapperState::new(chain)));
864 debug!("Injected native_wrapper component for {chain}");
865 update
866 })
867 });
868 stream::iter(modified).chain(inner)
869 })
870 .flatten(),
871 )
872}
873
874#[cfg(test)]
875mod tests {
876 use std::collections::HashMap;
877
878 use futures::{stream, StreamExt};
879 use tycho_common::models::Chain;
880
881 use super::*;
882 use crate::protocol::models::Update;
883
884 fn empty_update(block: u64) -> Update {
885 Update::new(block, HashMap::new(), HashMap::new())
886 }
887
888 #[tokio::test]
889 async fn test_inject_native_wrapper_first_message_only() {
890 let updates = vec![Ok(empty_update(1)), Ok(empty_update(2)), Ok(empty_update(3))];
891 let input = stream::iter(updates);
892
893 let results: Vec<_> = inject_native_wrapper(input, Chain::Ethereum)
894 .collect()
895 .await;
896
897 assert_eq!(results.len(), 3);
898
899 let expected_id = NativeWrapperState::component(Chain::Ethereum)
900 .id
901 .to_string();
902
903 let first = results[0]
904 .as_ref()
905 .expect("first update ok");
906 assert!(
907 first
908 .new_pairs
909 .contains_key(&expected_id),
910 "first message should have native_wrapper component"
911 );
912 assert!(
913 first.states.contains_key(&expected_id),
914 "first message should have native_wrapper state"
915 );
916
917 let second = results[1]
918 .as_ref()
919 .expect("second update ok");
920 assert!(
921 !second
922 .new_pairs
923 .contains_key(&expected_id),
924 "second message should NOT have native_wrapper component"
925 );
926 assert!(
927 !second.states.contains_key(&expected_id),
928 "second message should NOT have native_wrapper state"
929 );
930 }
931
932 /// Verifies that `with_step_controller` returns both a modified builder and a controller.
933 ///
934 /// This test only checks that the builder method is callable and that the returned controller
935 /// compiles — it does not start any network connection.
936 #[tokio::test]
937 async fn test_with_step_controller_returns_controller() {
938 let builder = ProtocolStreamBuilder::new("tycho-beta.propellerheads.xyz", Chain::Ethereum);
939 let (_builder, controller) = builder.with_step_controller();
940 // The controller was successfully returned — verifying the public API is callable.
941 drop(controller);
942 }
943
944 /// Connects to a live Tycho instance, verifies that the stream blocks until
945 /// `trigger_next_block` is called, and that `peek_next_block` exposes the buffered message.
946 #[ignore = "requires live Tycho connection (TYCHO_AUTH_TOKEN env var)"]
947 #[tokio::test]
948 async fn test_step_controller_trigger_releases_block() {
949 use std::{env, time::Duration};
950
951 use crate::evm::protocol::uniswap_v2::state::UniswapV2State;
952
953 let auth = env::var("TYCHO_AUTH_TOKEN").expect("TYCHO_AUTH_TOKEN must be set");
954
955 // Track a single well-known pool to minimise startup latency.
956 let usdc_weth_v2 = "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc".to_string();
957 let (builder, controller) =
958 ProtocolStreamBuilder::new("tycho-beta.propellerheads.xyz", Chain::Ethereum)
959 .auth_key(Some(auth))
960 .exchange::<UniswapV2State>(
961 "uniswap_v2",
962 ComponentFilter::Ids(vec![usdc_weth_v2]),
963 None,
964 )
965 .with_step_controller();
966
967 let (stream, _pending) = builder
968 .build_with_pending()
969 .await
970 .expect("build_with_pending failed");
971 tokio::pin!(stream);
972
973 // Wait up to 60 s for the first block to arrive in the gating buffer.
974 let peeked = tokio::time::timeout(Duration::from_secs(60), controller.peek_next_block())
975 .await
976 .expect("timed out waiting for first block to buffer")
977 .expect("stream ended before a block arrived");
978
979 assert!(!peeked.sync_states.is_empty(), "peeked block should carry sync states");
980
981 // Stream must be empty before we trigger — the gating task should be holding the block.
982 let pre_trigger = tokio::time::timeout(Duration::from_millis(200), stream.next()).await;
983 assert!(
984 pre_trigger.is_err(),
985 "stream should be blocked before trigger_next_block, got an item"
986 );
987
988 // Release the block.
989 controller
990 .trigger_next_block()
991 .expect("trigger_next_block failed");
992
993 // Stream should now yield the decoded update within one block time.
994 let update = tokio::time::timeout(Duration::from_secs(30), stream.next())
995 .await
996 .expect("timed out waiting for update after trigger")
997 .expect("stream ended unexpectedly");
998
999 assert!(update.is_ok(), "decoded update should be Ok, got: {:?}", update);
1000 }
1001}