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, 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 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 let (mut snapshot, tokens) = create_test_snapshot();
230 snapshot.component.tokens.pop(); 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 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 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}