Skip to main content

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