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.into(), auth.user, 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::{
104 dto::{Chain, ChangeType, ProtocolComponent, ResponseProtocolState},
105 models::Chain as ModelChain,
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 ModelChain::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 ModelChain::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: ResponseProtocolState {
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_ids: 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_USER", "test_user");
190 env::set_var("BEBOP_KEY", "test_key");
191
192 let (snapshot, tokens) = create_test_snapshot();
193
194 let result = BebopState::try_from_with_header(
195 snapshot,
196 TimestampHeader { timestamp: 1703097600u64 },
197 &HashMap::new(),
198 &tokens,
199 &DecoderContext::new(),
200 )
201 .await
202 .expect("create state from snapshot");
203
204 assert_eq!(result.base_token.symbol, "WBTC");
205 assert_eq!(result.quote_token.symbol, "USDC");
206 assert_eq!(result.price_data.last_update_ts, 1703097600);
207 assert_eq!(result.price_data.get_bids().len(), 3);
208 assert_eq!(result.price_data.get_asks().len(), 3);
209 assert_eq!(result.price_data.get_bids()[0], (65000.0, 1.5));
210 assert_eq!(result.price_data.get_asks()[0], (65100.0, 1.0));
211 }
212
213 #[tokio::test]
214 async fn test_try_from_missing_token() {
215 env::set_var("BEBOP_USER", "test_user");
216 env::set_var("BEBOP_KEY", "test_key");
217
218 let (mut snapshot, tokens) = create_test_snapshot();
220 snapshot.component.tokens.pop(); let result = BebopState::try_from_with_header(
222 snapshot,
223 TimestampHeader::default(),
224 &HashMap::new(),
225 &tokens,
226 &DecoderContext::new(),
227 )
228 .await;
229 assert!(result.is_err());
230 }
231
232 #[tokio::test]
233 async fn test_try_from_missing_bids() {
234 env::set_var("BEBOP_USER", "test_user");
235 env::set_var("BEBOP_KEY", "test_key");
236
237 let (mut snapshot, tokens) = create_test_snapshot();
239 snapshot.state.attributes.remove("bids");
240 let result = BebopState::try_from_with_header(
241 snapshot,
242 TimestampHeader::default(),
243 &HashMap::new(),
244 &tokens,
245 &DecoderContext::new(),
246 )
247 .await
248 .expect("create state from snapshot");
249 assert_eq!(result.price_data.bids.len(), 0);
250 }
251
252 #[tokio::test]
253 async fn test_try_from_invalid_json() {
254 env::set_var("BEBOP_USER", "test_user");
255 env::set_var("BEBOP_KEY", "test_key");
256
257 let (mut snapshot, tokens) = create_test_snapshot();
258
259 snapshot.state.attributes.insert(
261 "bids".to_string(),
262 "invalid json"
263 .as_bytes()
264 .to_vec()
265 .into(),
266 );
267 let result = BebopState::try_from_with_header(
268 snapshot,
269 TimestampHeader::default(),
270 &HashMap::new(),
271 &tokens,
272 &DecoderContext::new(),
273 )
274 .await;
275 assert!(result.is_err());
276 }
277}