use std::{any::Any, collections::HashMap, fmt, sync::Arc};
use async_trait::async_trait;
use num_bigint::BigUint;
use serde::{Deserialize, Serialize};
use tycho_common::{
dto::ProtocolStateDelta,
models::{protocol::GetAmountOutParams, token::Token},
simulation::{
errors::{SimulationError, TransitionError},
indicatively_priced::{IndicativelyPriced, SignedQuote},
protocol_sim::{Balances, GetAmountOutResult, ProtocolSim},
},
Bytes,
};
use crate::rfq::protocols::bebop::{
client::BebopClient, models::BebopPriceData, state::BebopState,
};
#[derive(Clone, Serialize, Deserialize)]
pub struct BebopAllPairsState {
pairs: Arc<Vec<BebopState>>,
used: bool,
}
impl fmt::Debug for BebopAllPairsState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BebopAllPairsState")
.field("pairs", &self.pairs.len())
.field("used", &self.used)
.finish()
}
}
impl BebopAllPairsState {
pub fn new(
books: Vec<BebopPriceData>,
tokens: HashMap<Bytes, Token>,
client: BebopClient,
) -> Result<Self, SimulationError> {
let mut pairs = Vec::with_capacity(books.len());
for book in books {
let base_token = book_token(&tokens, &book.base)?;
let quote_token = book_token(&tokens, &book.quote)?;
pairs.push(BebopState::new(base_token, quote_token, book, client.clone()));
}
pairs.sort_by(|a, b| {
(&a.base_token.address, &a.quote_token.address)
.cmp(&(&b.base_token.address, &b.quote_token.address))
});
Ok(Self { pairs: Arc::new(pairs), used: false })
}
fn find(&self, base: &Bytes, quote: &Bytes) -> Option<&BebopState> {
let index = self.pairs.partition_point(|pair| {
(&pair.base_token.address, &pair.quote_token.address) < (base, quote)
});
self.pairs
.get(index)
.filter(|pair| &pair.base_token.address == base && &pair.quote_token.address == quote)
}
fn pair_state(
&self,
token_in: &Bytes,
token_out: &Bytes,
) -> Result<&BebopState, SimulationError> {
self.find(token_in, token_out)
.or_else(|| self.find(token_out, token_in))
.ok_or_else(|| {
SimulationError::RecoverableError(format!(
"Invalid token addresses: {token_in}, {token_out}"
))
})
}
fn check_unused(&self) -> Result<(), SimulationError> {
if self.used {
return Err(SimulationError::RecoverableError(
"Bebop already quoted in this route".into(),
));
}
Ok(())
}
fn used_state(&self) -> Self {
Self { pairs: self.pairs.clone(), used: true }
}
}
fn book_token(tokens: &HashMap<Bytes, Token>, address: &[u8]) -> Result<Token, SimulationError> {
tokens
.get(&Bytes::from(address.to_vec()))
.cloned()
.ok_or_else(|| {
SimulationError::FatalError(format!(
"Bebop book names token 0x{}, which the state does not carry",
hex::encode(address)
))
})
}
#[typetag::serde]
impl ProtocolSim for BebopAllPairsState {
fn fee(&self) -> f64 {
0.0
}
fn spot_price(&self, base: &Token, quote: &Token) -> Result<f64, SimulationError> {
let pair_state = self.pair_state(&base.address, "e.address)?;
self.check_unused()?;
pair_state.spot_price(base, quote)
}
fn get_amount_out(
&self,
amount_in: BigUint,
token_in: &Token,
token_out: &Token,
) -> Result<GetAmountOutResult, SimulationError> {
let pair_state = self.pair_state(&token_in.address, &token_out.address)?;
self.check_unused()?;
let (token_in, token_out) = if pair_state.base_token.address == token_in.address {
(&pair_state.base_token, &pair_state.quote_token)
} else {
(&pair_state.quote_token, &pair_state.base_token)
};
match pair_state.get_amount_out(amount_in, token_in, token_out) {
Ok(mut res) => {
res.new_state = Box::new(self.used_state());
Ok(res)
}
Err(SimulationError::InvalidInput(msg, Some(mut res))) => {
res.new_state = Box::new(self.used_state());
Err(SimulationError::InvalidInput(msg, Some(res)))
}
Err(e) => Err(e),
}
}
fn get_limits(
&self,
sell_token: Bytes,
buy_token: Bytes,
) -> Result<(BigUint, BigUint), SimulationError> {
let pair_state = self.pair_state(&sell_token, &buy_token)?;
self.check_unused()?;
pair_state.get_limits(sell_token, buy_token)
}
fn delta_transition(
&mut self,
_delta: ProtocolStateDelta,
_tokens: &HashMap<Bytes, Token>,
_balances: &Balances,
) -> Result<(), TransitionError> {
Err(TransitionError::DecodeError("Not implemented".into()))
}
fn clone_box(&self) -> Box<dyn ProtocolSim> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
fn eq(&self, other: &dyn ProtocolSim) -> bool {
let Some(other) = other
.as_any()
.downcast_ref::<BebopAllPairsState>()
else {
return false;
};
self.used == other.used &&
self.pairs.len() == other.pairs.len() &&
self.pairs
.iter()
.zip(other.pairs.iter())
.all(|(a, b)| a.price_data == b.price_data)
}
fn as_indicatively_priced(&self) -> Result<&dyn IndicativelyPriced, SimulationError> {
Ok(self)
}
}
#[async_trait]
impl IndicativelyPriced for BebopAllPairsState {
async fn request_signed_quote(
&self,
params: GetAmountOutParams,
) -> Result<SignedQuote, SimulationError> {
self.pair_state(¶ms.token_in, ¶ms.token_out)?
.request_signed_quote(params)
.await
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashSet, str::FromStr};
use tokio::time::Duration;
use tycho_common::models::Chain;
use super::*;
use crate::rfq::protocols::test_utils::{usdc, wbtc, weth};
fn empty_bebop_client() -> BebopClient {
BebopClient::new(
Chain::Ethereum,
HashSet::new(),
0.0,
"".to_string(),
HashSet::new(),
Duration::from_secs(30),
None,
None,
None,
)
.unwrap()
}
fn book(base: &Token, quote: &Token, bids: &[f32], asks: &[f32]) -> BebopPriceData {
BebopPriceData {
base: base.address.to_vec(),
quote: quote.address.to_vec(),
last_update_ts: 1703097600,
bids: bids.to_vec(),
asks: asks.to_vec(),
}
}
fn state(books: Vec<BebopPriceData>) -> BebopAllPairsState {
BebopAllPairsState::new(
books,
HashMap::from([
(wbtc().address, wbtc()),
(usdc().address, usdc()),
(weth().address, weth()),
]),
empty_bebop_client(),
)
.unwrap()
}
fn create_test_bebop_state() -> BebopAllPairsState {
state(vec![
book(
&wbtc(),
&usdc(),
&[65000.0, 1.5, 64950.0, 2.0, 64900.0, 0.5],
&[65100.0, 1.0, 65150.0, 2.5, 65200.0, 1.5],
),
book(&weth(), &usdc(), &[3000.0, 2.0, 2900.0, 2.5], &[3100.0, 1.5, 3000.0, 3.0]),
])
}
#[test]
fn test_book_quoting_the_direction_beats_the_inverted_one() {
let state = state(vec![
book(&weth(), &usdc(), &[3000.0, 2.0], &[3100.0, 1.5]),
book(&usdc(), &weth(), &[0.00048828125, 2000.0], &[]),
]);
let result = state
.get_amount_out(BigUint::from(2_000_000_000u64), &usdc(), &weth())
.unwrap();
assert_eq!(result.amount, BigUint::from_str("0_976562500000000000").unwrap());
}
#[test]
fn test_get_limits_used_venue() {
let state = create_test_bebop_state().used_state();
let result = state.get_limits(wbtc().address, usdc().address);
assert!(
matches!(result, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
);
}
#[test]
fn test_get_limits_invalid_token_pair() {
let state = create_test_bebop_state();
let result = state.get_limits(wbtc().address, weth().address);
assert!(
matches!(result, Err(SimulationError::RecoverableError(msg)) if msg.contains("Invalid token addresses"))
);
}
#[test]
fn test_get_amount_out() {
let state = create_test_bebop_state();
let amount_out_result = state
.get_amount_out(BigUint::from_str("3_000000000000000000").unwrap(), &weth(), &usdc())
.unwrap();
assert_eq!(amount_out_result.amount, BigUint::from_str("8900_000_000").unwrap());
let amount_out_result = state
.get_amount_out(BigUint::from_str("7000_000_000").unwrap(), &usdc(), &weth())
.unwrap();
assert_eq!(amount_out_result.amount, BigUint::from_str("2_283333333333333248").unwrap());
}
#[test]
fn test_get_amount_out_once_per_venue() {
let state = create_test_bebop_state();
let first = state
.get_amount_out(BigUint::from_str("1_000000000000000000").unwrap(), &weth(), &usdc())
.unwrap();
let after_first = first
.new_state
.as_any()
.downcast_ref::<BebopAllPairsState>()
.unwrap();
assert!(after_first.used);
let second = after_first.get_amount_out(BigUint::from(100_000_000u64), &wbtc(), &usdc());
assert!(
matches!(second, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
);
let spot_price = after_first.spot_price(&wbtc(), &usdc());
assert!(
matches!(spot_price, Err(SimulationError::RecoverableError(msg)) if msg == "Bebop already quoted in this route")
);
}
}