Skip to main content

tycho_simulation/rfq/protocols/bebop/
all_pairs_decoder.rs

1use std::collections::HashMap;
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{all_pairs_state::BebopAllPairsState, models::BebopPriceData};
7use crate::{
8    protocol::{
9        errors::InvalidSnapshotError,
10        models::{DecoderContext, TryFromWithBlock},
11    },
12    rfq::{
13        constants::{get_bebop_auth, get_bebop_origins},
14        models::{ComponentLayout, QuoteRule, TimestampHeader},
15        protocols::{
16            bebop::client_builder::BebopClientBuilder,
17            component::{decode_all_pairs_component, DecodedAllPairs},
18        },
19    },
20};
21
22impl TryFromWithBlock<ComponentWithState, TimestampHeader> for BebopAllPairsState {
23    type Error = InvalidSnapshotError;
24
25    async fn try_from_with_header(
26        snapshot: ComponentWithState,
27        _timestamp_header: TimestampHeader,
28        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
29        all_tokens: &HashMap<Bytes, Token>,
30        _decoder_context: &DecoderContext,
31    ) -> Result<Self, Self::Error> {
32        let DecodedAllPairs { books, tokens, quote_rule } =
33            decode_all_pairs_component::<BebopPriceData>(&snapshot, all_tokens)?;
34        if quote_rule.is_some_and(|rule| rule != QuoteRule::OncePerVenue) {
35            return Err(InvalidSnapshotError::ValueError(
36                "Bebop names no market maker; its quote rule is once_per_venue".into(),
37            ));
38        }
39
40        let auth = get_bebop_auth().map_err(|e| {
41            InvalidSnapshotError::ValueError(format!("Failed to get Bebop authentication: {e}"))
42        })?;
43        let origins = get_bebop_origins().map_err(|e| {
44            InvalidSnapshotError::ValueError(format!("Failed to get Bebop origins: {e}"))
45        })?;
46        let mut builder = BebopClientBuilder::new(snapshot.component.chain, auth.key)
47            .tokens(tokens.keys().cloned().collect())
48            .component_layout(ComponentLayout::AllPairs);
49        if let Some(origin_address) = origins.address {
50            builder = builder.origin_address(origin_address);
51        }
52        if let Some(origin_target) = origins.target {
53            builder = builder.origin_target(origin_target);
54        }
55        if let Some(origin_source) = origins.source {
56            builder = builder.origin_source(origin_source);
57        }
58        let client = builder.build().map_err(|e| {
59            InvalidSnapshotError::ValueError(format!("Couldn't create BebopClient: {e}"))
60        })?;
61
62        BebopAllPairsState::new(books, tokens, client)
63            .map_err(|e| InvalidSnapshotError::ValueError(e.to_string()))
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use std::env;
70
71    use tycho_common::simulation::protocol_sim::ProtocolSim;
72
73    use super::*;
74    use crate::rfq::protocols::test_utils::{all_pairs_snapshot, decode, usdc, wbtc, weth};
75
76    fn snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
77        env::set_var("BEBOP_KEY", "test_key");
78        let books = vec![
79            BebopPriceData {
80                base: wbtc().address.to_vec(),
81                quote: usdc().address.to_vec(),
82                last_update_ts: 1703097600,
83                bids: vec![65000.0, 1.5, 64950.0, 2.0, 64900.0, 0.5],
84                asks: vec![65100.0, 1.0, 65150.0, 2.5, 65200.0, 1.5],
85            },
86            BebopPriceData {
87                base: weth().address.to_vec(),
88                quote: usdc().address.to_vec(),
89                last_update_ts: 1703097600,
90                bids: vec![3000.0, 2.0],
91                asks: vec![3100.0, 1.5],
92            },
93        ];
94        all_pairs_snapshot("rfq:bebop", &[wbtc(), usdc(), weth()], &books)
95    }
96
97    #[tokio::test]
98    async fn test_decodes_books() {
99        let (snapshot, tokens) = snapshot();
100        let state = decode::<BebopAllPairsState>(snapshot, &tokens)
101            .await
102            .unwrap();
103
104        let wbtc_price = state
105            .spot_price(&wbtc(), &usdc())
106            .unwrap();
107        assert!((65000.0..=65100.0).contains(&wbtc_price), "{wbtc_price}");
108        let weth_price = state
109            .spot_price(&weth(), &usdc())
110            .unwrap();
111        assert!((3000.0..=3100.0).contains(&weth_price), "{weth_price}");
112    }
113
114    #[tokio::test]
115    async fn test_once_per_maker_attribute() {
116        let (mut snapshot, tokens) = snapshot();
117        snapshot
118            .component
119            .static_attributes
120            .insert(QuoteRule::ATTRIBUTE.to_string(), b"once_per_maker".into());
121        let result = decode::<BebopAllPairsState>(snapshot, &tokens).await;
122        assert!(
123            matches!(result.unwrap_err(), InvalidSnapshotError::ValueError(msg) if msg.contains("names no market maker"))
124        );
125    }
126}