1use std::{any::Any, collections::HashMap, fmt, sync::Arc};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use serde::{Deserialize, Serialize};
6use tracing::debug;
7use tycho_common::{
8 dto::ProtocolStateDelta,
9 models::{protocol::GetAmountOutParams, token::Token},
10 simulation::{
11 errors::{SimulationError, TransitionError},
12 indicatively_priced::{IndicativelyPriced, SignedQuote},
13 protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
14 },
15 Bytes,
16};
17
18use crate::rfq::protocols::bebop::{
19 client::BebopClient, models::BebopPriceData, state::BebopState,
20};
21
22#[derive(Clone, Serialize, Deserialize)]
27pub struct BebopAllPairsState {
28 pairs: Arc<Vec<BebopState>>,
30 used: bool,
32}
33
34impl fmt::Debug for BebopAllPairsState {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.debug_struct("BebopAllPairsState")
37 .field("pairs", &self.pairs.len())
38 .field("used", &self.used)
39 .finish()
40 }
41}
42
43impl BebopAllPairsState {
44 pub fn new(
46 books: Vec<BebopPriceData>,
47 tokens: HashMap<Bytes, Token>,
48 client: BebopClient,
49 ) -> Self {
50 let mut pairs = Vec::with_capacity(books.len());
51 for book in books {
52 let (Some(base_token), Some(quote_token)) =
53 (book_token(&tokens, &book.base), book_token(&tokens, &book.quote))
54 else {
55 debug!(
56 "Skipping Bebop book 0x{}/0x{}: a token is missing from the state.",
57 hex::encode(&book.base),
58 hex::encode(&book.quote)
59 );
60 continue;
61 };
62 pairs.push(BebopState::new(base_token, quote_token, book, client.clone()));
63 }
64 pairs.sort_by(|a, b| {
65 (&a.base_token.address, &a.quote_token.address)
66 .cmp(&(&b.base_token.address, &b.quote_token.address))
67 });
68 Self { pairs: Arc::new(pairs), used: false }
69 }
70
71 fn find(&self, base: &Bytes, quote: &Bytes) -> Option<&BebopState> {
73 let index = self.pairs.partition_point(|pair| {
74 (&pair.base_token.address, &pair.quote_token.address) < (base, quote)
75 });
76 self.pairs
77 .get(index)
78 .filter(|pair| &pair.base_token.address == base && &pair.quote_token.address == quote)
79 }
80
81 fn pair_state(
84 &self,
85 token_in: &Bytes,
86 token_out: &Bytes,
87 ) -> Result<&BebopState, SimulationError> {
88 self.find(token_in, token_out)
89 .or_else(|| self.find(token_out, token_in))
90 .ok_or_else(|| {
91 SimulationError::RecoverableError(format!(
92 "Invalid token addresses: {token_in}, {token_out}"
93 ))
94 })
95 }
96
97 fn check_unused(&self) -> Result<(), SimulationError> {
98 if self.used {
99 return Err(SimulationError::RecoverableError(
100 "Bebop already quoted in this route".into(),
101 ));
102 }
103 Ok(())
104 }
105
106 fn used_state(&self) -> Self {
107 Self { pairs: self.pairs.clone(), used: true }
108 }
109}
110
111fn book_token(tokens: &HashMap<Bytes, Token>, address: &[u8]) -> Option<Token> {
113 tokens
114 .get(&Bytes::from(address.to_vec()))
115 .cloned()
116}
117
118#[typetag::serde]
119impl ProtocolSim for BebopAllPairsState {
120 fn fee(&self) -> f64 {
121 0.0
122 }
123
124 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
125 let pair_state = self.pair_state(&base.address, "e.address)?;
126 self.check_unused()?;
127 pair_state.spot_price(base, quote)
128 }
129
130 fn get_amount_out(
131 &self,
132 amount_in: BigUint,
133 token_in: &Token,
134 token_out: &Token,
135 ) -> Result<GetAmountOutResult, SimulationError> {
136 let pair_state = self.pair_state(&token_in.address, &token_out.address)?;
137 self.check_unused()?;
138 let (token_in, token_out) = if pair_state.base_token.address == token_in.address {
140 (&pair_state.base_token, &pair_state.quote_token)
141 } else {
142 (&pair_state.quote_token, &pair_state.base_token)
143 };
144 match pair_state.get_amount_out(amount_in, token_in, token_out) {
145 Ok(mut res) => {
146 res.new_state = Box::new(self.used_state());
147 Ok(res)
148 }
149 Err(SimulationError::InvalidInput(msg, Some(mut res))) => {
150 res.new_state = Box::new(self.used_state());
151 Err(SimulationError::InvalidInput(msg, Some(res)))
152 }
153 Err(e) => Err(e),
154 }
155 }
156
157 fn get_limits(
158 &self,
159 sell_token: Bytes,
160 buy_token: Bytes,
161 ) -> Result<(BigUint, BigUint), SimulationError> {
162 let pair_state = self.pair_state(&sell_token, &buy_token)?;
163 self.check_unused()?;
164 pair_state.get_limits(sell_token, buy_token)
165 }
166
167 fn delta_transition(
168 &mut self,
169 _delta: ProtocolStateDelta,
170 _tokens: &HashMap<Bytes, Token>,
171 _balances: &Balances,
172 ) -> Result<(), TransitionError> {
173 Err(TransitionError::DecodeError("Not implemented".into()))
174 }
175
176 fn clone_box(&self) -> Box<dyn ProtocolSim> {
177 Box::new(self.clone())
178 }
179
180 fn as_any(&self) -> &dyn Any {
181 self
182 }
183
184 fn as_any_mut(&mut self) -> &mut dyn Any {
185 self
186 }
187
188 fn eq(&self, other: &dyn ProtocolSim) -> bool {
189 let Some(other) = other
190 .as_any()
191 .downcast_ref::<BebopAllPairsState>()
192 else {
193 return false;
194 };
195 self.used == other.used &&
196 self.pairs.len() == other.pairs.len() &&
197 self.pairs
198 .iter()
199 .zip(other.pairs.iter())
200 .all(|(a, b)| a.price_data == b.price_data)
201 }
202
203 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
204 Ok(self)
205 }
206}
207
208#[async_trait]
209impl IndicativelyPriced for BebopAllPairsState {
210 async fn request_signed_quote(
211 &self,
212 params: GetAmountOutParams,
213 ) -> Result<SignedQuote, SimulationError> {
214 self.pair_state(¶ms.token_in, ¶ms.token_out)?
215 .request_signed_quote(params)
216 .await
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use std::{collections::HashSet, str::FromStr};
223
224 use tokio::time::Duration;
225 use tycho_common::models::Chain;
226
227 use super::*;
228 use crate::rfq::protocols::test_utils::{usdc, wbtc, weth};
229
230 fn empty_bebop_client() -> BebopClient {
231 BebopClient::new(
232 Chain::Ethereum,
233 HashSet::new(),
234 0.0,
235 "".to_string(),
236 HashSet::new(),
237 Duration::from_secs(30),
238 None,
239 None,
240 None,
241 )
242 .unwrap()
243 }
244
245 fn book(base: &Token, quote: &Token, bids: &[f32], asks: &[f32]) -> BebopPriceData {
246 BebopPriceData {
247 base: base.address.to_vec(),
248 quote: quote.address.to_vec(),
249 last_update_ts: 1703097600,
250 bids: bids.to_vec(),
251 asks: asks.to_vec(),
252 }
253 }
254
255 fn state(books: Vec<BebopPriceData>) -> BebopAllPairsState {
256 BebopAllPairsState::new(
257 books,
258 HashMap::from([
259 (wbtc().address, wbtc()),
260 (usdc().address, usdc()),
261 (weth().address, weth()),
262 ]),
263 empty_bebop_client(),
264 )
265 }
266
267 fn create_test_bebop_state() -> BebopAllPairsState {
269 state(vec![
270 book(
271 &wbtc(),
272 &usdc(),
273 &[65000.0, 1.5, 64950.0, 2.0, 64900.0, 0.5],
274 &[65100.0, 1.0, 65150.0, 2.5, 65200.0, 1.5],
275 ),
276 book(&weth(), &usdc(), &[3000.0, 2.0, 2900.0, 2.5], &[3100.0, 1.5, 3000.0, 3.0]),
277 ])
278 }
279
280 #[test]
281 fn test_book_quoting_the_direction_beats_the_inverted_one() {
282 let state = state(vec![
285 book(&weth(), &usdc(), &[3000.0, 2.0], &[3100.0, 1.5]),
286 book(&usdc(), &weth(), &[0.00048828125, 2000.0], &[]),
287 ]);
288 let result = state
289 .get_amount_out(BigUint::from(2_000_000_000u64), &usdc(), &weth())
290 .unwrap();
291 assert_eq!(result.amount, BigUint::from_str("0_976562500000000000").unwrap());
292 }
293
294 #[test]
295 fn test_get_limits_used_venue() {
296 let state = create_test_bebop_state().used_state();
297 let result = state.get_limits(wbtc().address, usdc().address);
298 assert!(
299 matches!(result, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
300 );
301 }
302
303 #[test]
304 fn test_get_limits_invalid_token_pair() {
305 let state = create_test_bebop_state();
306 let result = state.get_limits(wbtc().address, weth().address);
307 assert!(
308 matches!(result, Err(SimulationError::RecoverableError(msg)) if msg.contains("Invalid token addresses"))
309 );
310 }
311
312 #[test]
313 fn test_get_amount_out() {
314 let state = create_test_bebop_state();
315
316 let amount_out_result = state
318 .get_amount_out(BigUint::from_str("3_000000000000000000").unwrap(), &weth(), &usdc())
319 .unwrap();
320 assert_eq!(amount_out_result.amount, BigUint::from_str("8900_000_000").unwrap());
321
322 let amount_out_result = state
324 .get_amount_out(BigUint::from_str("7000_000_000").unwrap(), &usdc(), &weth())
325 .unwrap();
326 assert_eq!(amount_out_result.amount, BigUint::from_str("2_283333333333333248").unwrap());
327 }
328
329 #[test]
330 fn test_get_amount_out_once_per_venue() {
331 let state = create_test_bebop_state();
332 let first = state
333 .get_amount_out(BigUint::from_str("1_000000000000000000").unwrap(), &weth(), &usdc())
334 .unwrap();
335 let after_first = first
336 .new_state
337 .as_any()
338 .downcast_ref::<BebopAllPairsState>()
339 .unwrap();
340 assert!(after_first.used);
341
342 let second = after_first.get_amount_out(BigUint::from(100_000_000u64), &wbtc(), &usdc());
343 assert!(
344 matches!(second, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
345 );
346 let spot_price = after_first.spot_price(&wbtc(), &usdc());
347 assert!(
348 matches!(spot_price, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
349 );
350 }
351}