Skip to main content

tycho_simulation/rfq/
stream.rs

1use std::collections::HashMap;
2
3use futures::{stream::select_all, StreamExt};
4use tycho_client::feed::{synchronizer::ComponentWithState, FeedMessage};
5use tycho_common::{
6    models::{token::Token, Chain},
7    simulation::{errors::SimulationError, protocol_sim::ProtocolSim},
8    Bytes,
9};
10
11use crate::{
12    evm::decoder::TychoStreamDecoder,
13    protocol::{
14        errors::InvalidSnapshotError,
15        models::{TryFromWithBlock, Update},
16    },
17    rfq::{client::RFQClient, models::TimestampHeader},
18};
19
20/// `RFQStreamBuilder` is a utility for constructing and managing a merged stream of RFQ (Request
21/// For Quote) providers in Tycho.
22///
23/// It allows you to:
24/// - Register multiple `RFQClient` implementations, each providing its own stream of RFQ price
25///   updates.
26/// - Dynamically decode incoming updates into `Update` objects using `TychoStreamDecoder`.
27///
28/// The `build` method consumes the builder and runs the event loop, sending decoded `Update`s
29/// through the provided `mpsc::Sender`. It returns an error if decoding an update or forwarding
30/// it to the channel fails.
31///
32/// ### Error Handling:
33/// - Each `RFQClient`'s stream is expected to yield `Result<(String, StateSyncMessage), RFQError>`.
34/// - If a client's stream returns an `Err` (e.g., `RFQError::FatalError`), the client is
35///   **removed** from the merged stream, and the system continues running without it.
36pub struct RFQStreamBuilder {
37    clients: Vec<Box<dyn RFQClient>>,
38    decoder: TychoStreamDecoder<TimestampHeader>,
39}
40
41impl Default for RFQStreamBuilder {
42    fn default() -> Self {
43        Self::new()
44    }
45}
46
47impl RFQStreamBuilder {
48    pub fn new() -> Self {
49        Self {
50            clients: Vec::new(),
51            // RFQ states carry a timestamp, never a block header, so the decoder's block-time
52            // projection is unreachable by type (`TimestampHeader::block()` is `None`) and the
53            // chain here is never read.
54            decoder: TychoStreamDecoder::new(Chain::Ethereum),
55        }
56    }
57
58    pub fn add_client<T>(mut self, name: &str, provider: Box<dyn RFQClient>) -> Self
59    where
60        T: ProtocolSim
61            + TryFromWithBlock<ComponentWithState, TimestampHeader, Error = InvalidSnapshotError>
62            + Send
63            + 'static,
64    {
65        self.clients.push(provider);
66        self.decoder.register_decoder::<T>(name);
67        self
68    }
69
70    pub async fn build(self, tx: tokio::sync::mpsc::Sender<Update>) -> Result<(), SimulationError> {
71        let streams: Vec<_> = self
72            .clients
73            .into_iter()
74            .map(|provider| provider.stream())
75            .collect();
76
77        let mut merged = select_all(streams);
78
79        while let Some(next) = merged.next().await {
80            match next {
81                Ok((provider, msg)) => {
82                    let update = self
83                        .decoder
84                        .decode(&FeedMessage {
85                            state_msgs: HashMap::from([(provider.clone(), msg)]),
86                            sync_states: HashMap::new(),
87                        })
88                        .await
89                        .map_err(|e| {
90                            SimulationError::RecoverableError(format!("Decoding error: {e}"))
91                        })?;
92                    tx.send(update).await.map_err(|e| {
93                        SimulationError::RecoverableError(format!(
94                            "Failed to send update through channel: {e}"
95                        ))
96                    })?;
97                }
98                Err(e) => {
99                    tracing::error!(
100                        "RFQ stream fatal error: {e}. Assuming this stream will not emit more messages."
101                    );
102                }
103            }
104        }
105
106        Ok(())
107    }
108
109    /// Provides token metadata used to decode startup snapshots and initialize protocol states.
110    ///
111    /// This is not an ongoing stream filter. Components arriving after startup include their
112    /// own token metadata for decoding.
113    pub async fn set_tokens(self, tokens: HashMap<Bytes, Token>) -> Self {
114        self.decoder.set_tokens(tokens).await;
115        self
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use std::{any::Any, time::Duration};
122
123    use async_trait::async_trait;
124    use futures::stream::BoxStream;
125    use num_bigint::BigUint;
126    use serde::{Deserialize, Serialize};
127    use tokio::sync::mpsc;
128    use tokio_stream::wrappers::IntervalStream;
129    use tycho_client::feed::synchronizer::{Snapshot, StateSyncMessage};
130    use tycho_common::{
131        dto::ProtocolStateDelta,
132        models::{
133            protocol::{GetAmountOutParams, ProtocolComponent, ProtocolComponentState},
134            token::Token,
135        },
136        simulation::{
137            errors::{SimulationError, TransitionError},
138            indicatively_priced::SignedQuote,
139            protocol_sim::{Balances, GetAmountOutResult},
140        },
141        Bytes,
142    };
143
144    use super::*;
145    use crate::{protocol::models::DecoderContext, rfq::errors::RFQError};
146
147    #[derive(Clone, Debug, Serialize, Deserialize)]
148    pub struct DummyProtocol;
149
150    #[typetag::serde]
151    impl ProtocolSim for DummyProtocol {
152        fn fee(&self) -> f64 {
153            unimplemented!("Not needed for this test")
154        }
155
156        fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
157            unimplemented!("Not needed for this test")
158        }
159
160        fn get_amount_out(
161            &self,
162            _amount_in: BigUint,
163            _token_in: &Token,
164            _token_out: &Token,
165        ) -> Result<GetAmountOutResult, SimulationError> {
166            unimplemented!("Not needed for this test")
167        }
168
169        fn get_limits(
170            &self,
171            _sell_token: Bytes,
172            _buy_token: Bytes,
173        ) -> Result<(BigUint, BigUint), SimulationError> {
174            unimplemented!("Not needed for this test")
175        }
176
177        fn delta_transition(
178            &mut self,
179            _delta: ProtocolStateDelta,
180            _tokens: &HashMap<Bytes, Token>,
181            _balances: &Balances,
182        ) -> Result<(), TransitionError> {
183            unimplemented!("Not needed for this test")
184        }
185
186        fn clone_box(&self) -> Box<dyn ProtocolSim> {
187            Box::new(self.clone())
188        }
189
190        fn as_any(&self) -> &dyn Any {
191            self
192        }
193
194        fn as_any_mut(&mut self) -> &mut dyn Any {
195            self
196        }
197        fn eq(&self, _other: &dyn ProtocolSim) -> bool {
198            unimplemented!("Not needed for this test")
199        }
200    }
201
202    impl TryFromWithBlock<ComponentWithState, TimestampHeader> for DummyProtocol {
203        type Error = InvalidSnapshotError;
204        async fn try_from_with_header(
205            _value: ComponentWithState,
206            _header: TimestampHeader,
207            _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
208            _all_tokens: &HashMap<Bytes, Token>,
209            _decoder_context: &DecoderContext,
210        ) -> Result<Self, Self::Error> {
211            Ok(DummyProtocol)
212        }
213    }
214
215    pub struct MockRFQClient {
216        name: String,
217        interval: Duration,
218        error_at_time: Option<u128>,
219    }
220
221    impl MockRFQClient {
222        pub fn new(name: &str, interval: Duration, error_at_time: Option<u128>) -> Self {
223            Self { name: name.to_string(), interval, error_at_time }
224        }
225    }
226
227    #[async_trait]
228    impl RFQClient for MockRFQClient {
229        fn stream(
230            &self,
231        ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>>
232        {
233            let name = self.name.clone();
234            let error_at_time = self.error_at_time;
235            let mut current_time: u128 = 0;
236            let interval = self.interval;
237            let interval =
238                IntervalStream::new(tokio::time::interval(self.interval)).map(move |_| {
239                    if let Some(error_at_time) = error_at_time {
240                        if error_at_time == current_time {
241                            return Err(RFQError::FatalError(format!(
242                                "{name} stream is dying and can't go on"
243                            )));
244                        };
245                    };
246                    let protocol_component =
247                        ProtocolComponent { protocol_system: name.clone(), ..Default::default() };
248
249                    let snapshot = Snapshot {
250                        states: HashMap::from([(
251                            name.clone(),
252                            ComponentWithState {
253                                state: ProtocolComponentState {
254                                    component_id: name.clone(),
255                                    attributes: HashMap::new(),
256                                    balances: HashMap::new(),
257                                },
258                                component: protocol_component,
259                                component_tvl: None,
260                                entrypoints: vec![],
261                            },
262                        )]),
263                        vm_storage: HashMap::new(),
264                    };
265
266                    let msg = StateSyncMessage {
267                        header: TimestampHeader { timestamp: current_time as u64 },
268                        snapshots: snapshot,
269                        ..Default::default()
270                    };
271
272                    current_time += interval.as_millis();
273                    Ok((name.clone(), msg))
274                });
275            Box::pin(interval)
276        }
277
278        async fn request_binding_quote(
279            &self,
280            _params: &GetAmountOutParams,
281        ) -> Result<SignedQuote, RFQError> {
282            unimplemented!("Not needed for this test")
283        }
284    }
285
286    #[tokio::test]
287    async fn test_rfq_stream_builder() {
288        // This test has two mocked RFQ clients
289        // 1. Bebop client that emits a message every 100ms
290        // 2. Hashflow client that emits a message every 200m
291        let (tx, mut rx) = mpsc::channel::<Update>(10);
292
293        let builder = RFQStreamBuilder::new()
294            .add_client::<DummyProtocol>(
295                "bebop",
296                Box::new(MockRFQClient::new("bebop", Duration::from_millis(100), Some(300))),
297            )
298            .add_client::<DummyProtocol>(
299                "hashflow",
300                Box::new(MockRFQClient::new("hashflow", Duration::from_millis(200), None)),
301            );
302
303        tokio::spawn(builder.build(tx));
304
305        // Collect only the first 10 messages
306        let mut updates = Vec::new();
307        for _ in 0..6 {
308            let update = rx.recv().await.unwrap();
309            updates.push(update);
310        }
311
312        // Collect all timestamps per provider
313        let bebop_updates: Vec<_> = updates
314            .iter()
315            .filter(|u| u.new_pairs.contains_key("bebop"))
316            .collect();
317        let hashflow_updates: Vec<_> = updates
318            .iter()
319            .filter(|u| u.new_pairs.contains_key("hashflow"))
320            .collect();
321
322        assert_eq!(bebop_updates[0].block_number_or_timestamp, 0,);
323        assert_eq!(hashflow_updates[0].block_number_or_timestamp, 0,);
324        assert_eq!(bebop_updates[1].block_number_or_timestamp, 100);
325        assert_eq!(bebop_updates[2].block_number_or_timestamp, 200);
326        assert_eq!(hashflow_updates[1].block_number_or_timestamp, 200);
327        // At this point the bebop stream dies, and we shouldn't have any more bebop updates, only
328        // hashflow
329        assert_eq!(bebop_updates.len(), 3);
330        assert_eq!(hashflow_updates[2].block_number_or_timestamp, 400);
331    }
332}