Skip to main content

tycho_simulation/rfq/
stream.rs

1use std::collections::{BTreeSet, HashMap, HashSet};
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::{ProtocolComponent, TryFromWithBlock, Update},
16    },
17    rfq::{
18        client::RFQClient,
19        models::{ComponentLayout, TimestampHeader},
20        protocols::component::{
21            decode_swap_directions, encode_swap_directions, SWAP_DIRECTIONS_ATTRIBUTE,
22        },
23    },
24};
25
26/// `RFQStreamBuilder` is a utility for constructing and managing a merged stream of RFQ (Request
27/// For Quote) providers in Tycho.
28///
29/// It allows you to:
30/// - Register multiple `RFQClient` implementations, each providing its own stream of RFQ price
31///   updates.
32/// - Dynamically decode incoming updates into `Update` objects using `TychoStreamDecoder`.
33///
34/// The `build` method consumes the builder and runs the event loop, sending decoded `Update`s
35/// through the provided `mpsc::Sender`. It returns an error if decoding an update or forwarding
36/// it to the channel fails.
37///
38/// ### Error Handling:
39/// - Each `RFQClient`'s stream is expected to yield `Result<(String, StateSyncMessage), RFQError>`.
40/// - If a client's stream returns an `Err` (e.g., `RFQError::FatalError`), the client is
41///   **removed** from the merged stream, and the system continues running without it.
42pub struct RFQStreamBuilder {
43    clients: Vec<Box<dyn RFQClient>>,
44    decoder: TychoStreamDecoder<TimestampHeader>,
45}
46
47impl Default for RFQStreamBuilder {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53impl RFQStreamBuilder {
54    pub fn new() -> Self {
55        Self {
56            clients: Vec::new(),
57            // RFQ states carry a timestamp, never a block header, so the decoder's block-time
58            // projection is unreachable by type (`TimestampHeader::block()` is `None`) and the
59            // chain here is never read.
60            decoder: TychoStreamDecoder::new(Chain::Ethereum),
61        }
62    }
63
64    /// Adds `provider`, whose components decode to `T`. A provider that streams the all-pairs
65    /// layout keeps its component when a token is unknown: the decoder drops the token.
66    pub fn add_client<T>(mut self, name: &str, provider: Box<dyn RFQClient>) -> Self
67    where
68        T: ProtocolSim
69            + TryFromWithBlock<ComponentWithState, TimestampHeader, Error = InvalidSnapshotError>
70            + Send
71            + 'static,
72    {
73        match provider.component_layout() {
74            ComponentLayout::PerPair => self.decoder.register_decoder::<T>(name),
75            ComponentLayout::AllPairs => self
76                .decoder
77                .register_decoder_dropping_unknown_tokens::<T>(name),
78        }
79        self.clients.push(provider);
80        self
81    }
82
83    pub async fn build(self, tx: tokio::sync::mpsc::Sender<Update>) -> Result<(), SimulationError> {
84        let streams: Vec<_> = self
85            .clients
86            .into_iter()
87            .map(|provider| provider.stream())
88            .collect();
89
90        let mut merged = select_all(streams);
91
92        while let Some(next) = merged.next().await {
93            match next {
94                Ok((provider, msg)) => {
95                    let mut update = self
96                        .decoder
97                        .decode(&FeedMessage {
98                            state_msgs: HashMap::from([(provider.clone(), msg)]),
99                            sync_states: HashMap::new(),
100                        })
101                        .await
102                        .map_err(|e| {
103                            SimulationError::RecoverableError(format!("Decoding error: {e}"))
104                        })?;
105                    for component in update.new_pairs.values_mut() {
106                        retain_carried_directions(component).map_err(|e| {
107                            SimulationError::RecoverableError(format!("Decoding error: {e}"))
108                        })?;
109                    }
110                    tx.send(update).await.map_err(|e| {
111                        SimulationError::RecoverableError(format!(
112                            "Failed to send update through channel: {e}"
113                        ))
114                    })?;
115                }
116                Err(e) => {
117                    tracing::error!(
118                        "RFQ stream fatal error: {e}. Assuming this stream will not emit more messages."
119                    );
120                }
121            }
122        }
123
124        Ok(())
125    }
126
127    /// Provides token metadata used to decode startup snapshots and initialize protocol states.
128    ///
129    /// This is not an ongoing stream filter. Components arriving after startup include their
130    /// own token metadata for decoding.
131    pub async fn set_tokens(self, tokens: HashMap<Bytes, Token>) -> Self {
132        self.decoder.set_tokens(tokens).await;
133        self
134    }
135}
136
137/// Keeps the `swap_directions` of a component to the directions whose two tokens it carries, as
138/// the decoder drops the tokens it does not know.
139fn retain_carried_directions(component: &mut ProtocolComponent) -> Result<(), String> {
140    let Some(attribute) = component
141        .static_attributes
142        .get(SWAP_DIRECTIONS_ATTRIBUTE)
143    else {
144        return Ok(());
145    };
146    let carried: HashSet<&Bytes> = component
147        .tokens
148        .iter()
149        .map(|token| &token.address)
150        .collect();
151    let mut directions = BTreeSet::new();
152    for (token_in, token_out) in decode_swap_directions(attribute)? {
153        if carried.contains(&token_in) && carried.contains(&token_out) {
154            directions.insert((token_in, token_out));
155        }
156    }
157    let attribute = encode_swap_directions(&directions);
158    component
159        .static_attributes
160        .insert(SWAP_DIRECTIONS_ATTRIBUTE.to_string(), attribute);
161    Ok(())
162}
163
164#[cfg(test)]
165mod tests {
166    use std::{any::Any, time::Duration};
167
168    use async_trait::async_trait;
169    use futures::stream::BoxStream;
170    use num_bigint::BigUint;
171    use serde::{Deserialize, Serialize};
172    use tokio::sync::mpsc;
173    use tokio_stream::wrappers::IntervalStream;
174    use tycho_client::feed::synchronizer::{Snapshot, StateSyncMessage};
175    use tycho_common::{
176        dto::ProtocolStateDelta,
177        models::{
178            protocol::{GetAmountOutParams, ProtocolComponent, ProtocolComponentState},
179            token::Token,
180        },
181        simulation::{
182            errors::{SimulationError, TransitionError},
183            indicatively_priced::SignedQuote,
184            protocol_sim::{Balances, GetAmountOutResult},
185        },
186        Bytes,
187    };
188
189    use super::*;
190    use crate::{
191        protocol::models::DecoderContext,
192        rfq::{
193            errors::RFQError,
194            models::QuoteRule,
195            protocols::{
196                component::poll_message,
197                hashflow::all_pairs_state::HashflowAllPairsState,
198                maker_price_levels,
199                test_utils::{maker_price_levels as levels, usdc, wbtc, weth},
200            },
201        },
202    };
203
204    #[derive(Clone, Debug, Serialize, Deserialize)]
205    pub struct DummyProtocol;
206
207    #[typetag::serde]
208    impl ProtocolSim for DummyProtocol {
209        fn fee(&self) -> f64 {
210            unimplemented!("Not needed for this test")
211        }
212
213        fn spot_price(&self, _base: &Token, _quote: &Token) -> Result<f64, SimulationError> {
214            unimplemented!("Not needed for this test")
215        }
216
217        fn get_amount_out(
218            &self,
219            _amount_in: BigUint,
220            _token_in: &Token,
221            _token_out: &Token,
222        ) -> Result<GetAmountOutResult, SimulationError> {
223            unimplemented!("Not needed for this test")
224        }
225
226        fn get_limits(
227            &self,
228            _sell_token: Bytes,
229            _buy_token: Bytes,
230        ) -> Result<(BigUint, BigUint), SimulationError> {
231            unimplemented!("Not needed for this test")
232        }
233
234        fn delta_transition(
235            &mut self,
236            _delta: ProtocolStateDelta,
237            _tokens: &HashMap<Bytes, Token>,
238            _balances: &Balances,
239        ) -> Result<(), TransitionError> {
240            unimplemented!("Not needed for this test")
241        }
242
243        fn clone_box(&self) -> Box<dyn ProtocolSim> {
244            Box::new(self.clone())
245        }
246
247        fn as_any(&self) -> &dyn Any {
248            self
249        }
250
251        fn as_any_mut(&mut self) -> &mut dyn Any {
252            self
253        }
254        fn eq(&self, _other: &dyn ProtocolSim) -> bool {
255            unimplemented!("Not needed for this test")
256        }
257    }
258
259    impl TryFromWithBlock<ComponentWithState, TimestampHeader> for DummyProtocol {
260        type Error = InvalidSnapshotError;
261        async fn try_from_with_header(
262            _value: ComponentWithState,
263            _header: TimestampHeader,
264            _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
265            _all_tokens: &HashMap<Bytes, Token>,
266            _decoder_context: &DecoderContext,
267        ) -> Result<Self, Self::Error> {
268            Ok(DummyProtocol)
269        }
270    }
271
272    pub struct MockRFQClient {
273        name: String,
274        interval: Duration,
275        error_at_time: Option<u128>,
276    }
277
278    impl MockRFQClient {
279        pub fn new(name: &str, interval: Duration, error_at_time: Option<u128>) -> Self {
280            Self { name: name.to_string(), interval, error_at_time }
281        }
282    }
283
284    #[async_trait]
285    impl RFQClient for MockRFQClient {
286        fn stream(
287            &self,
288        ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>>
289        {
290            let name = self.name.clone();
291            let error_at_time = self.error_at_time;
292            let mut current_time: u128 = 0;
293            let interval = self.interval;
294            let interval =
295                IntervalStream::new(tokio::time::interval(self.interval)).map(move |_| {
296                    if let Some(error_at_time) = error_at_time {
297                        if error_at_time == current_time {
298                            return Err(RFQError::FatalError(format!(
299                                "{name} stream is dying and can't go on"
300                            )));
301                        };
302                    };
303                    let protocol_component =
304                        ProtocolComponent { protocol_system: name.clone(), ..Default::default() };
305
306                    let snapshot = Snapshot {
307                        states: HashMap::from([(
308                            name.clone(),
309                            ComponentWithState {
310                                state: ProtocolComponentState {
311                                    component_id: name.clone(),
312                                    attributes: HashMap::new(),
313                                    balances: HashMap::new(),
314                                },
315                                component: protocol_component,
316                                component_tvl: None,
317                                entrypoints: vec![],
318                            },
319                        )]),
320                        vm_storage: HashMap::new(),
321                    };
322
323                    let msg = StateSyncMessage {
324                        header: TimestampHeader { timestamp: current_time as u64 },
325                        snapshots: snapshot,
326                        ..Default::default()
327                    };
328
329                    current_time += interval.as_millis();
330                    Ok((name.clone(), msg))
331                });
332            Box::pin(interval)
333        }
334
335        async fn request_binding_quote(
336            &self,
337            _params: &GetAmountOutParams,
338        ) -> Result<SignedQuote, RFQError> {
339            unimplemented!("Not needed for this test")
340        }
341    }
342
343    #[tokio::test]
344    async fn test_rfq_stream_builder() {
345        // This test has two mocked RFQ clients
346        // 1. Bebop client that emits a message every 100ms
347        // 2. Hashflow client that emits a message every 200m
348        let (tx, mut rx) = mpsc::channel::<Update>(10);
349
350        let builder = RFQStreamBuilder::new()
351            .add_client::<DummyProtocol>(
352                "bebop",
353                Box::new(MockRFQClient::new("bebop", Duration::from_millis(100), Some(300))),
354            )
355            .add_client::<DummyProtocol>(
356                "hashflow",
357                Box::new(MockRFQClient::new("hashflow", Duration::from_millis(200), None)),
358            );
359
360        tokio::spawn(builder.build(tx));
361
362        // Collect only the first 10 messages
363        let mut updates = Vec::new();
364        for _ in 0..6 {
365            let update = rx.recv().await.unwrap();
366            updates.push(update);
367        }
368
369        // Collect all timestamps per provider
370        let bebop_updates: Vec<_> = updates
371            .iter()
372            .filter(|u| u.new_pairs.contains_key("bebop"))
373            .collect();
374        let hashflow_updates: Vec<_> = updates
375            .iter()
376            .filter(|u| u.new_pairs.contains_key("hashflow"))
377            .collect();
378
379        assert_eq!(bebop_updates[0].block_number_or_timestamp, 0,);
380        assert_eq!(hashflow_updates[0].block_number_or_timestamp, 0,);
381        assert_eq!(bebop_updates[1].block_number_or_timestamp, 100);
382        assert_eq!(bebop_updates[2].block_number_or_timestamp, 200);
383        assert_eq!(hashflow_updates[1].block_number_or_timestamp, 200);
384        // At this point the bebop stream dies, and we shouldn't have any more bebop updates, only
385        // hashflow
386        assert_eq!(bebop_updates.len(), 3);
387        assert_eq!(hashflow_updates[2].block_number_or_timestamp, 400);
388    }
389
390    /// An all-pairs client that streams one message and stops.
391    struct AllPairsRFQClient(StateSyncMessage<TimestampHeader>);
392
393    #[async_trait]
394    impl RFQClient for AllPairsRFQClient {
395        fn component_layout(&self) -> ComponentLayout {
396            ComponentLayout::AllPairs
397        }
398
399        fn stream(
400            &self,
401        ) -> BoxStream<'static, Result<(String, StateSyncMessage<TimestampHeader>), RFQError>>
402        {
403            Box::pin(futures::stream::iter([Ok(("hashflow".to_string(), self.0.clone()))]))
404        }
405
406        async fn request_binding_quote(
407            &self,
408            _params: &GetAmountOutParams,
409        ) -> Result<SignedQuote, RFQError> {
410            unimplemented!("Not needed for this test")
411        }
412    }
413
414    #[tokio::test]
415    async fn test_all_pairs_component_drops_an_unknown_token_and_its_directions() {
416        std::env::set_var("HASHFLOW_USER", "test_user");
417        std::env::set_var("HASHFLOW_KEY", "test_key");
418        let component = maker_price_levels::all_pairs_component(
419            "rfq:hashflow",
420            "hashflow_pool",
421            Chain::Ethereum,
422            vec![
423                (levels("mm", &weth(), &usdc(), &[(1.0, 3000.0)]), 100.0),
424                (levels("mm", &wbtc(), &usdc(), &[(1.0, 65000.0)]), 100.0),
425            ],
426            QuoteRule::OncePerMaker,
427        )
428        .unwrap()
429        .unwrap();
430        let id = component.component.id.clone();
431        let message =
432            poll_message(&mut HashMap::new(), HashMap::from([(id.clone(), component)]), 1);
433        let (tx, mut rx) = mpsc::channel::<Update>(10);
434        let builder = RFQStreamBuilder::new()
435            .add_client::<HashflowAllPairsState>("hashflow", Box::new(AllPairsRFQClient(message)))
436            .set_tokens(HashMap::from([(weth().address, weth()), (usdc().address, usdc())]))
437            .await;
438
439        builder.build(tx).await.unwrap();
440
441        let update = rx.recv().await.unwrap();
442        let emitted = &update.new_pairs[&id];
443        assert_eq!(emitted.tokens.len(), 2);
444        assert_eq!(
445            emitted.static_attributes[SWAP_DIRECTIONS_ATTRIBUTE],
446            encode_swap_directions(&BTreeSet::from([(weth().address, usdc().address)]))
447        );
448        assert!(update.states.contains_key(&id));
449    }
450}