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