Skip to main content

tycho_simulation/rfq/protocols/bebop/
decoder.rs

1use std::collections::HashMap;
2
3use tycho_client::feed::synchronizer::ComponentWithState;
4use tycho_common::{models::token::Token, Bytes};
5
6use super::{models::BebopPriceData, state::BebopState};
7use crate::{
8    protocol::{
9        errors::InvalidSnapshotError,
10        models::{DecoderContext, TryFromWithBlock},
11    },
12    rfq::{
13        constants::{get_bebop_auth, get_bebop_origins},
14        models::TimestampHeader,
15        protocols::bebop::client_builder::BebopClientBuilder,
16    },
17};
18
19impl TryFromWithBlock<ComponentWithState, TimestampHeader> for BebopState {
20    type Error = InvalidSnapshotError;
21
22    async fn try_from_with_header(
23        snapshot: ComponentWithState,
24        timestamp_header: TimestampHeader,
25        _account_balances: &HashMap<Bytes, HashMap<Bytes, Bytes>>,
26        all_tokens: &HashMap<Bytes, Token>,
27        _decoder_context: &DecoderContext,
28    ) -> Result<Self, Self::Error> {
29        let state_attrs = snapshot.state.attributes;
30
31        if snapshot.component.tokens.len() != 2 {
32            return Err(InvalidSnapshotError::ValueError(
33                "Component must have 2 tokens (base and quote)".to_string(),
34            ));
35        }
36
37        let base_token_address = &snapshot.component.tokens[0];
38        let quote_token_address = &snapshot.component.tokens[1];
39
40        let base_token = all_tokens
41            .get(base_token_address)
42            .ok_or_else(|| {
43                InvalidSnapshotError::ValueError(format!(
44                    "Base token not found: {base_token_address}"
45                ))
46            })?
47            .clone();
48
49        let quote_token = all_tokens
50            .get(quote_token_address)
51            .ok_or_else(|| {
52                InvalidSnapshotError::ValueError(format!(
53                    "Quote token not found: {quote_token_address}"
54                ))
55            })?
56            .clone();
57
58        let empty_array_bytes: Bytes = "[]".as_bytes().to_vec().into();
59        let bids_json = state_attrs
60            .get("bids")
61            .unwrap_or(&empty_array_bytes);
62        let asks_json = state_attrs
63            .get("asks")
64            .unwrap_or(&empty_array_bytes);
65
66        // Parse bids and asks from JSON
67        let bids: Vec<(f32, f32)> = serde_json::from_slice(bids_json)
68            .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid bids JSON: {e}")))?;
69        let asks: Vec<(f32, f32)> = serde_json::from_slice(asks_json)
70            .map_err(|e| InvalidSnapshotError::ValueError(format!("Invalid asks JSON: {e}")))?;
71
72        let price_data = BebopPriceData {
73            base: base_token.address.to_vec(),
74            quote: quote_token.address.to_vec(),
75            last_update_ts: timestamp_header.timestamp,
76            bids: bids
77                .iter()
78                .flat_map(|(price, size)| [*price, *size])
79                .collect(),
80            asks: asks
81                .iter()
82                .flat_map(|(price, size)| [*price, *size])
83                .collect(),
84        };
85
86        let auth = get_bebop_auth().map_err(|e| {
87            InvalidSnapshotError::ValueError(format!("Failed to get Bebop authentication: {e}"))
88        })?;
89        let origins = get_bebop_origins().map_err(|e| {
90            InvalidSnapshotError::ValueError(format!("Failed to get Bebop origins: {e}"))
91        })?;
92
93        let mut client_builder = BebopClientBuilder::new(snapshot.component.chain, auth.key);
94        if let Some(origin_address) = origins.address {
95            client_builder = client_builder.origin_address(origin_address);
96        }
97        if let Some(origin_target) = origins.target {
98            client_builder = client_builder.origin_target(origin_target);
99        }
100        if let Some(origin_source) = origins.source {
101            client_builder = client_builder.origin_source(origin_source);
102        }
103        let client = client_builder.build().map_err(|e| {
104            InvalidSnapshotError::MissingAttribute(format!("Couldn't create BebopClient: {e}"))
105        })?;
106
107        Ok(BebopState { base_token, quote_token, price_data, client })
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use std::env;
114
115    use tycho_common::models::{
116        protocol::{ProtocolComponent, ProtocolComponentState},
117        Chain, ChangeType,
118    };
119
120    use super::*;
121
122    fn wbtc() -> Token {
123        Token::new(
124            &hex::decode("2260fac5e5542a773aa44fbcfedf7c193bc2c599")
125                .unwrap()
126                .into(),
127            "WBTC",
128            8,
129            0,
130            &[Some(10_000)],
131            Chain::Ethereum,
132            100,
133        )
134    }
135
136    fn usdc() -> Token {
137        Token::new(
138            &hex::decode("a0b86991c6218a76c1d19d4a2e9eb0ce3606eb48")
139                .unwrap()
140                .into(),
141            "USDC",
142            6,
143            0,
144            &[Some(10_000)],
145            Chain::Ethereum,
146            100,
147        )
148    }
149
150    fn create_test_snapshot() -> (ComponentWithState, HashMap<Bytes, Token>) {
151        let wbtc_token = wbtc();
152        let usdc_token = usdc();
153
154        let mut tokens = HashMap::new();
155        tokens.insert(wbtc_token.address.clone(), wbtc_token.clone());
156        tokens.insert(usdc_token.address.clone(), usdc_token.clone());
157
158        let mut state_attributes = HashMap::new();
159        state_attributes.insert(
160            "bids".to_string(),
161            "[[65000.0, 1.5], [64950.0, 2.0], [64900.0, 0.5]]"
162                .as_bytes()
163                .to_vec()
164                .into(),
165        );
166        state_attributes.insert(
167            "asks".to_string(),
168            "[[65100.0, 1.0], [65150.0, 2.5], [65200.0, 1.5]]"
169                .as_bytes()
170                .to_vec()
171                .into(),
172        );
173
174        let snapshot = ComponentWithState {
175            state: ProtocolComponentState {
176                attributes: state_attributes,
177                component_id: "bebop_wbtc_usdc".to_string(),
178                balances: HashMap::new(),
179            },
180            component: ProtocolComponent {
181                id: "bebop_wbtc_usdc".to_string(),
182                protocol_system: "bebop".to_string(),
183                protocol_type_name: "bebop".to_string(),
184                chain: Chain::Ethereum,
185                tokens: vec![wbtc_token.address.clone(), usdc_token.address.clone()],
186                contract_addresses: Vec::new(),
187                static_attributes: HashMap::new(),
188                change: ChangeType::Creation,
189                creation_tx: Bytes::default(),
190                created_at: chrono::NaiveDateTime::default(),
191            },
192            component_tvl: None,
193            entrypoints: Vec::new(),
194        };
195
196        (snapshot, tokens)
197    }
198
199    #[tokio::test]
200    async fn test_try_from_with_header() {
201        env::set_var("BEBOP_KEY", "test_key");
202
203        let (snapshot, tokens) = create_test_snapshot();
204
205        let result = BebopState::try_from_with_header(
206            snapshot,
207            TimestampHeader { timestamp: 1703097600u64 },
208            &HashMap::new(),
209            &tokens,
210            &DecoderContext::new(),
211        )
212        .await
213        .expect("create state from snapshot");
214
215        assert_eq!(result.base_token.symbol, "WBTC");
216        assert_eq!(result.quote_token.symbol, "USDC");
217        assert_eq!(result.price_data.last_update_ts, 1703097600);
218        assert_eq!(result.price_data.get_bids().len(), 3);
219        assert_eq!(result.price_data.get_asks().len(), 3);
220        assert_eq!(result.price_data.get_bids()[0], (65000.0, 1.5));
221        assert_eq!(result.price_data.get_asks()[0], (65100.0, 1.0));
222    }
223
224    #[tokio::test]
225    async fn test_try_from_missing_token() {
226        env::set_var("BEBOP_KEY", "test_key");
227
228        // Test missing second token (only one token in array)
229        let (mut snapshot, tokens) = create_test_snapshot();
230        snapshot.component.tokens.pop(); // Remove the second token
231        let result = BebopState::try_from_with_header(
232            snapshot,
233            TimestampHeader::default(),
234            &HashMap::new(),
235            &tokens,
236            &DecoderContext::new(),
237        )
238        .await;
239        assert!(result.is_err());
240    }
241
242    #[tokio::test]
243    async fn test_try_from_missing_bids() {
244        env::set_var("BEBOP_KEY", "test_key");
245
246        // Should decode an empty array of bids
247        let (mut snapshot, tokens) = create_test_snapshot();
248        snapshot.state.attributes.remove("bids");
249        let result = BebopState::try_from_with_header(
250            snapshot,
251            TimestampHeader::default(),
252            &HashMap::new(),
253            &tokens,
254            &DecoderContext::new(),
255        )
256        .await
257        .expect("create state from snapshot");
258        assert_eq!(result.price_data.bids.len(), 0);
259    }
260
261    #[tokio::test]
262    async fn test_try_from_invalid_json() {
263        env::set_var("BEBOP_KEY", "test_key");
264
265        let (mut snapshot, tokens) = create_test_snapshot();
266
267        // Test invalid bids JSON
268        snapshot.state.attributes.insert(
269            "bids".to_string(),
270            "invalid json"
271                .as_bytes()
272                .to_vec()
273                .into(),
274        );
275        let result = BebopState::try_from_with_header(
276            snapshot,
277            TimestampHeader::default(),
278            &HashMap::new(),
279            &tokens,
280            &DecoderContext::new(),
281        )
282        .await;
283        assert!(result.is_err());
284    }
285}