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