1use std::{any::Any, collections::HashMap, fmt};
2
3use async_trait::async_trait;
4use num_bigint::BigUint;
5use num_traits::{FromPrimitive, Pow, ToPrimitive};
6use serde::{Deserialize, Serialize};
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::{
19 client::RFQClient,
20 protocols::bebop::{client::BebopClient, models::BebopPriceData},
21};
22
23#[derive(Clone, Serialize, Deserialize)]
24pub struct BebopState {
25 pub base_token: Token,
26 pub quote_token: Token,
27 pub price_data: BebopPriceData,
28 pub client: BebopClient,
29}
30
31impl fmt::Debug for BebopState {
32 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33 f.debug_struct("BebopState")
34 .field("base_token", &self.base_token)
35 .field("quote_token", &self.quote_token)
36 .finish_non_exhaustive()
37 }
38}
39
40impl BebopState {
41 pub fn new(
42 base_token: Token,
43 quote_token: Token,
44 price_data: BebopPriceData,
45 client: BebopClient,
46 ) -> Self {
47 BebopState { base_token, quote_token, price_data, client }
48 }
49}
50
51#[typetag::serde]
52impl ProtocolSim for BebopState {
53 fn fee(&self) -> f64 {
54 0.0
55 }
56
57 fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
58 let best_bid = self
61 .price_data
62 .get_bids()
63 .first()
64 .map(|(price, _)| *price);
65 let best_ask = self
66 .price_data
67 .get_asks()
68 .first()
69 .map(|(price, _)| *price);
70
71 let average_price = match (best_bid, best_ask) {
73 (Some(best_bid), Some(best_ask)) => (best_bid + best_ask) / 2.0,
74 (Some(best_bid), None) => best_bid,
75 (None, Some(best_ask)) => best_ask,
76 (None, None) => {
77 return Err(SimulationError::RecoverableError("No liquidity available".to_string()))
78 }
79 };
80
81 if base.address == self.quote_token.address && quote.address == self.base_token.address {
84 Ok(1.0 / average_price)
85 } else if quote.address == self.quote_token.address &&
86 base.address == self.base_token.address
87 {
88 Ok(average_price)
89 } else {
90 Err(SimulationError::RecoverableError(format!(
91 "Invalid token addresses: {}, {}",
92 base.address, quote.address
93 )))
94 }
95 }
96
97 fn get_amount_out(
98 &self,
99 amount_in: BigUint,
100 token_in: &Token,
101 token_out: &Token,
102 ) -> Result<GetAmountOutResult, SimulationError> {
103 let sell_base = if token_in == &self.base_token && token_out == &self.quote_token {
104 true
105 } else if token_in == &self.quote_token && token_out == &self.base_token {
106 false
107 } else {
108 return Err(SimulationError::RecoverableError(format!(
109 "Invalid token addresses: {}, {}",
110 token_in.address, token_out.address
111 )));
112 };
113 let price_levels = if sell_base {
117 self.price_data.get_bids()
118 } else {
119 self.price_data
120 .get_asks()
121 .iter()
122 .map(|(price, size)| (1.0 / price, price * size))
123 .collect()
124 };
125
126 if price_levels.is_empty() {
127 return Err(SimulationError::RecoverableError("No liquidity".into()));
128 }
129
130 let amount_in = amount_in.to_f64().ok_or_else(|| {
131 SimulationError::RecoverableError("Can't convert amount in to f64".into())
132 })? / 10f64.powi(token_in.decimals as i32);
133 let (amount_out, remaining_amount_in) = self
134 .price_data
135 .get_amount_out_from_levels(amount_in, price_levels);
136 let res = GetAmountOutResult {
137 amount: BigUint::from_f64(amount_out * 10f64.powi(token_out.decimals as i32))
138 .ok_or_else(|| {
139 SimulationError::RecoverableError("Can't convert amount out to BigUInt".into())
140 })?,
141 gas: BigUint::from(70_000u64), new_state: self.clone_box(), };
144
145 if remaining_amount_in > 0.0 {
146 return Err(SimulationError::InvalidInput(
147 format!("Pool has not enough liquidity to support complete swap. input amount: {amount_in}, consumed amount: {}", amount_in-remaining_amount_in),
148 Some(res)));
149 }
150
151 Ok(res)
152 }
153
154 fn get_limits(
155 &self,
156 sell_token: Bytes,
157 buy_token: Bytes,
158 ) -> Result<(BigUint, BigUint), SimulationError> {
159 let (sell_decimals, buy_decimals, price_levels) = if sell_token == self.base_token.address &&
162 buy_token == self.quote_token.address
163 {
164 (self.base_token.decimals, self.quote_token.decimals, self.price_data.get_bids())
165 } else if buy_token == self.base_token.address && sell_token == self.quote_token.address {
166 (self.quote_token.decimals, self.base_token.decimals, self.price_data.get_asks())
167 } else {
168 return Err(SimulationError::RecoverableError(format!(
169 "Invalid token addresses: {sell_token}, {buy_token}"
170 )));
171 };
172
173 if price_levels.is_empty() {
175 return Ok((BigUint::from(0u64), BigUint::from(0u64)));
176 }
177
178 let total_base_amount: f64 = price_levels
179 .iter()
180 .map(|(_, amount)| amount)
181 .sum();
182 let total_quote_amount: f64 = price_levels
183 .iter()
184 .map(|(price, amount)| price * amount)
185 .sum();
186
187 let (total_sell_amount, total_buy_amount) =
188 if sell_token == self.base_token.address && buy_token == self.quote_token.address {
189 (total_base_amount, total_quote_amount)
190 } else {
191 (total_quote_amount, total_base_amount)
192 };
193
194 let sell_limit =
195 BigUint::from((total_sell_amount * 10_f64.pow(sell_decimals as f64)) as u128);
196 let buy_limit = BigUint::from((total_buy_amount * 10_f64.pow(buy_decimals as f64)) as u128);
197
198 Ok((sell_limit, buy_limit))
199 }
200
201 fn delta_transition(
202 &mut self,
203 _delta: ProtocolStateDelta,
204 _tokens: &HashMap<Bytes, Token>,
205 _balances: &Balances,
206 ) -> Result<(), TransitionError> {
207 Err(TransitionError::DecodeError("Not implemented".into()))
208 }
209
210 fn clone_box(&self) -> Box<dyn ProtocolSim> {
211 Box::new(self.clone())
212 }
213
214 fn as_any(&self) -> &dyn Any {
215 self
216 }
217
218 fn as_any_mut(&mut self) -> &mut dyn Any {
219 self
220 }
221
222 fn eq(&self, other: &dyn ProtocolSim) -> bool {
223 if let Some(other_state) = other
224 .as_any()
225 .downcast_ref::<BebopState>()
226 {
227 self.base_token == other_state.base_token &&
228 self.quote_token == other_state.quote_token &&
229 self.price_data == other_state.price_data
230 } else {
231 false
232 }
233 }
234
235 fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
236 Ok(self)
237 }
238}
239
240#[async_trait]
241impl IndicativelyPriced for BebopState {
242 async fn request_signed_quote(
243 &self,
244 params: GetAmountOutParams,
245 ) -> Result<SignedQuote, SimulationError> {
246 Ok(self
247 .client
248 .request_binding_quote(¶ms)
249 .await?)
250 }
251}
252
253#[cfg(test)]
254mod tests {
255 use std::{collections::HashSet, str::FromStr};
256
257 use tokio::time::Duration;
258 use tycho_common::models::Chain;
259
260 use super::*;
261
262 fn wbtc() -> Token {
263 Token::new(
264 &hex::decode("2260fac5e5542a773aa44fbcfedf7c193bc2c599")
265 .unwrap()
266 .into(),
267 "WBTC",
268 8,
269 0,
270 &[Some(10_000)],
271 Chain::Ethereum,
272 100,
273 )
274 }
275
276 fn usdc() -> Token {
277 Token::new(
278 &hex::decode("a0b86991c6218a76c1d19d4a2e9eb0ce3606eb48")
279 .unwrap()
280 .into(),
281 "USDC",
282 6,
283 0,
284 &[Some(10_000)],
285 Chain::Ethereum,
286 100,
287 )
288 }
289
290 fn weth() -> Token {
291 Token::new(
292 &Bytes::from_str("0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2").unwrap(),
293 "WETH",
294 18,
295 0,
296 &[],
297 Default::default(),
298 100,
299 )
300 }
301
302 fn empty_bebop_client() -> BebopClient {
303 BebopClient::new(
304 Chain::Ethereum,
305 HashSet::new(),
306 0.0,
307 "".to_string(),
308 "".to_string(),
309 HashSet::new(),
310 Duration::from_secs(30),
311 )
312 .unwrap()
313 }
314
315 fn create_test_bebop_state() -> BebopState {
316 BebopState {
317 base_token: wbtc(),
318 quote_token: usdc(),
319 price_data: BebopPriceData {
320 base: hex::decode("2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599").unwrap(), quote: hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), last_update_ts: 1703097600,
323 bids: vec![65000.0f32, 1.5f32, 64950.0f32, 2.0f32, 64900.0f32, 0.5f32],
324 asks: vec![65100.0f32, 1.0f32, 65150.0f32, 2.5f32, 65200.0f32, 1.5f32],
325 },
326 client: empty_bebop_client(),
327 }
328 }
329
330 #[test]
331 fn test_spot_price_matching_base_and_quote() {
332 let state = create_test_bebop_state();
333
334 let price = state
336 .spot_price(&wbtc(), &usdc())
337 .unwrap();
338 assert_eq!(price, 65050.0);
339 }
340
341 #[test]
342 fn test_spot_price_inverted_base_and_quote() {
343 let state = create_test_bebop_state();
344
345 let price = state
347 .spot_price(&usdc(), &wbtc())
348 .unwrap();
349 let expected = 0.00001537279;
350 assert!((price - expected).abs() < 1e-10);
351 }
352
353 #[test]
354 fn test_spot_price_empty_asks() {
355 let mut state = create_test_bebop_state();
356 state.price_data.asks = vec![]; let price = state
360 .spot_price(&wbtc(), &usdc())
361 .unwrap();
362 assert_eq!(price, 65000.0);
363 }
364
365 #[test]
366 fn test_spot_price_empty_bids() {
367 let mut state = create_test_bebop_state();
368 state.price_data.bids = vec![]; let price = state
371 .spot_price(&wbtc(), &usdc())
372 .unwrap();
373 assert_eq!(price, 65100.0);
374 }
375
376 #[test]
377 fn test_spot_price_no_liquidity() {
378 let mut state = create_test_bebop_state();
379 state.price_data.bids = vec![]; state.price_data.asks = vec![]; let result = state.spot_price(&wbtc(), &usdc());
383 assert!(result.is_err());
384 }
385
386 #[test]
387 fn test_get_limits_sell_base_for_quote() {
388 let state = create_test_bebop_state();
389
390 let (wbtc_limit, usdc_limit) = state
392 .get_limits(wbtc().address.clone(), usdc().address.clone())
393 .unwrap();
394
395 let expected_wbtc_limit = BigUint::from(4u64) * BigUint::from(10u64).pow(8u32);
399
400 let expected_usdc_limit = BigUint::from(259850u64) * BigUint::from(10u64).pow(6u32);
403
404 assert_eq!(wbtc_limit, expected_wbtc_limit);
405 assert_eq!(usdc_limit, expected_usdc_limit);
406 }
407
408 #[test]
409 fn test_get_limits_buy_base_with_quote() {
410 let state = create_test_bebop_state();
411
412 let (usdc_limit, wbtc_limit) = state
414 .get_limits(usdc().address.clone(), wbtc().address.clone())
415 .unwrap();
416
417 let expected_usdc_limit = BigUint::from(325775u64) * BigUint::from(10u64).pow(6u32);
422
423 let expected_wbtc_limit = BigUint::from(5u64) * BigUint::from(10u64).pow(8u32);
425
426 assert_eq!(usdc_limit, expected_usdc_limit);
427 assert_eq!(wbtc_limit, expected_wbtc_limit);
428 }
429
430 #[test]
431 fn test_get_limits_no_bids() {
432 let mut state = create_test_bebop_state();
433 state.price_data.bids = vec![]; let (token_limit, quote_limit) = state
437 .get_limits(wbtc().address.clone(), usdc().address.clone())
438 .unwrap();
439
440 assert_eq!(token_limit, BigUint::from(0u64));
441 assert_eq!(quote_limit, BigUint::from(0u64));
442 }
443
444 #[test]
445 fn test_get_limits_no_asks() {
446 let mut state = create_test_bebop_state();
447 state.price_data.asks = vec![]; let (token_limit, quote_limit) = state
451 .get_limits(usdc().address.clone(), wbtc().address.clone())
452 .unwrap();
453
454 assert_eq!(token_limit, BigUint::from(0u64));
455 assert_eq!(quote_limit, BigUint::from(0u64));
456 }
457
458 #[test]
459 fn test_get_limits_invalid_token_pair() {
460 let state = create_test_bebop_state();
461
462 let eth = Token::new(
464 &hex::decode("c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2")
465 .unwrap()
466 .into(),
467 "ETH",
468 18,
469 0,
470 &[Some(10_000)],
471 Chain::Ethereum,
472 100,
473 );
474
475 let result = state.get_limits(eth.address.clone(), usdc().address.clone());
477 assert!(result.is_err());
478
479 if let Err(SimulationError::RecoverableError(msg)) = result {
480 assert!(msg.contains("Invalid token addresses"));
481 } else {
482 panic!("Expected RecoverableError with invalid token addresses message");
483 }
484 }
485
486 #[test]
487 fn test_get_amount_out() {
488 let price_data = BebopPriceData {
490 base: hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(), quote: hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), last_update_ts: 1234567890,
493 bids: vec![3000.0f32, 2.0f32, 2900.0f32, 2.5f32],
494 asks: vec![3100.0f32, 1.5f32, 3000.0f32, 3.0f32],
495 };
496
497 let weth = weth();
498 let usdc = usdc();
499 let state = BebopState::new(weth.clone(), usdc.clone(), price_data, empty_bebop_client());
500
501 let amount_out_result = state
503 .get_amount_out(BigUint::from_str("3_000000000000000000").unwrap(), &weth, &usdc)
504 .unwrap();
505
506 assert_eq!(amount_out_result.amount, BigUint::from_str("8900_000_000").unwrap());
508
509 let amount_out_result = state
511 .get_amount_out(BigUint::from_str("7000_000_000").unwrap(), &usdc, &weth)
512 .unwrap();
513
514 assert_eq!(amount_out_result.amount, BigUint::from_str("2_283333333333333248").unwrap());
516 }
517}