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