use alloy::primitives::Address;
use prost::Message;
use serde::{Deserialize, Serialize};
use tycho_common::{models::protocol::GetAmountOutParams, Bytes};
use crate::rfq::errors::RFQError;
#[derive(Clone, PartialEq, Message)]
pub struct BebopPricingUpdate {
#[prost(message, repeated, tag = "1")]
pub pairs: Vec<BebopPriceData>,
}
#[derive(Clone, Serialize, Deserialize, PartialEq, Message)]
pub struct BebopPriceData {
#[prost(bytes, tag = "1")]
pub base: Vec<u8>,
#[prost(bytes, tag = "2")]
pub quote: Vec<u8>,
#[prost(uint64, tag = "3")]
pub last_update_ts: u64,
#[prost(float, repeated, packed = "true", tag = "4")]
pub bids: Vec<f32>,
#[prost(float, repeated, packed = "true", tag = "5")]
pub asks: Vec<f32>,
}
impl BebopPriceData {
pub fn to_price_size_pairs(array: &[f32]) -> Vec<(f64, f64)> {
array
.chunks_exact(2)
.map(|chunk| (chunk[0] as f64, chunk[1] as f64))
.collect()
}
pub fn get_bids(&self) -> Vec<(f64, f64)> {
Self::to_price_size_pairs(&self.bids)
}
pub fn get_asks(&self) -> Vec<(f64, f64)> {
Self::to_price_size_pairs(&self.asks)
}
pub fn get_pair_key(&self) -> String {
let base_addr = Address::from_slice(&self.base);
let quote_addr = Address::from_slice(&self.quote);
format!("{base_addr}/{quote_addr}")
}
pub fn calculate_tvl(&self, quote_price_data: Option<&BebopPriceData>) -> f64 {
let bid_tvl: f64 = self
.get_bids()
.iter()
.map(|(price, size)| price * size)
.sum();
let ask_tvl: f64 = self
.get_asks()
.iter()
.map(|(price, size)| price * size)
.sum();
let mut total_tvl = (bid_tvl + ask_tvl) / 2.0;
if let Some(quote_data) = quote_price_data {
if let Some(price_of_quote_token) = quote_data.get_mid_price(total_tvl, &self.quote) {
total_tvl *= price_of_quote_token;
} else {
return 0.0;
}
}
total_tvl
}
pub fn get_mid_price(&self, amount: f64, sell_token: &[u8]) -> Option<f64> {
if sell_token != self.base.as_slice() && sell_token != self.quote.as_slice() {
return None;
}
let inverse = sell_token == self.quote.as_slice();
let asks_price = self.get_price_for_levels(amount, self.get_asks(), inverse)?;
let bids_price = self.get_price_for_levels(amount, self.get_bids(), inverse)?;
Some((asks_price + bids_price) / 2.0)
}
fn get_price_for_levels(
&self,
amount_in: f64,
price_levels: Vec<(f64, f64)>,
invert: bool,
) -> Option<f64> {
if price_levels.is_empty() {
return None;
}
let levels = if invert { Self::invert_price_levels(price_levels) } else { price_levels };
let (amount_out, remaining_in) = self.get_amount_out_from_levels(amount_in, levels);
Some(amount_out / (amount_in - remaining_in))
}
pub fn get_amount_out_from_levels(
&self,
amount_in: f64,
price_levels: Vec<(f64, f64)>,
) -> (f64, f64) {
let mut remaining_amount_in = amount_in;
let mut amount_out = 0.0;
for (price, tokens_available) in price_levels.iter() {
if remaining_amount_in <= 0.0 {
break;
}
let amount_in_available_to_trade = remaining_amount_in.min(*tokens_available);
amount_out += amount_in_available_to_trade * price;
remaining_amount_in -= amount_in_available_to_trade;
}
(amount_out, remaining_amount_in)
}
fn invert_price_levels(price_levels: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
price_levels
.iter()
.filter(|(price, _)| *price > 0.0)
.map(|(price_quote_per_base, base_available)| {
let price_base_per_quote = 1.0 / price_quote_per_base;
let quote_size = base_available * price_quote_per_base;
(price_base_per_quote, quote_size)
})
.collect()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BebopQuoteResponse {
Success(Box<BebopQuotePartial>),
Error(BebopApiError),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BebopApiError {
pub error: BebopErrorDetail,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BebopErrorDetail {
#[serde(rename = "errorCode")]
pub error_code: u32,
pub message: String,
#[serde(rename = "requestId")]
pub request_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BebopQuotePartial {
pub status: String,
#[serde(rename = "settlementAddress")]
pub settlement_address: Bytes,
pub tx: TxData,
#[serde(rename = "toSign")]
pub to_sign: BebopOrderToSign,
#[serde(rename = "partialFillOffset")]
pub partial_fill_offset: u64,
}
impl BebopQuotePartial {
pub fn validate(&self, params: &GetAmountOutParams) -> Result<(), RFQError> {
match &self.to_sign {
BebopOrderToSign::Single(single) => {
if single.taker_token != params.token_in {
return Err(RFQError::FatalError(format!(
"Base token mismatch: expected {}, got {}",
params.token_in, single.taker_token
)));
}
if single.maker_token != params.token_out {
return Err(RFQError::FatalError(format!(
"Quote token mismatch: expected {}, got {}",
params.token_out, single.maker_token
)));
}
if single.taker_address != params.sender {
return Err(RFQError::FatalError(format!(
"Taker address mismatch: expected {}, got {}",
params.sender, single.taker_address
)));
}
if single.receiver != params.receiver {
return Err(RFQError::FatalError(format!(
"Receiver address mismatch: expected {}, got {}",
params.receiver, single.receiver
)));
}
let amount_in = params.amount_in.to_string();
if single.taker_amount != amount_in {
return Err(RFQError::FatalError(format!(
"Base token amount mismatch: expected {}, got {}",
amount_in, single.taker_amount
)));
}
}
BebopOrderToSign::Aggregate(aggregate) => {
if aggregate.taker_address != params.sender {
return Err(RFQError::FatalError(format!(
"Taker address mismatch: expected {}, got {}",
params.sender, aggregate.taker_address
)));
}
if aggregate.receiver != params.receiver {
return Err(RFQError::FatalError(format!(
"Receiver address mismatch: expected {}, got {}",
params.receiver, aggregate.receiver
)));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum BebopOrderToSign {
Single(Box<SingleOrderToSign>),
Aggregate(Box<AggregateOrderToSign>),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TxData {
pub to: Bytes,
pub data: Bytes,
pub value: String,
pub from: Bytes,
pub gas: u64,
#[serde(rename = "gasPrice")]
pub gas_price: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SingleOrderToSign {
pub maker_address: Bytes,
pub taker_address: Bytes,
pub maker_token: Bytes,
pub taker_token: Bytes,
pub maker_amount: String,
pub taker_amount: String,
pub maker_nonce: String,
pub expiry: u64,
pub receiver: Bytes,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AggregateOrderToSign {
pub taker_address: Bytes,
pub maker_tokens: Vec<Vec<Bytes>>,
pub taker_tokens: Vec<Vec<Bytes>>,
pub maker_amounts: Vec<Vec<String>>,
pub taker_amounts: Vec<Vec<String>>,
pub expiry: u64,
pub receiver: Bytes,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_tvl_no_normalization() {
let price_data = BebopPriceData {
base: hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(), quote: hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), last_update_ts: 1234567890,
bids: vec![2000.0f32, 1.0f32, 1999.0f32, 2.0f32],
asks: vec![2001.0f32, 1.5f32, 2002.0f32, 1.0f32],
};
let tvl = price_data.calculate_tvl(None);
assert!((tvl - 5500.75).abs() < 0.01);
}
#[test]
fn test_calculate_tvl_with_normalization() {
let price_data_eth_tamara = BebopPriceData {
base: hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(), quote: hex::decode("1234567890123456789012345678901234567890").unwrap(), last_update_ts: 1234567890,
bids: vec![99.0f32, 1.0f32, 98.0f32, 2.0f32],
asks: vec![101.0f32, 1.0f32, 102.0f32, 2.0f32],
};
let price_data_tamara_usdc = BebopPriceData {
base: hex::decode("1234567890123456789012345678901234567890").unwrap(), quote: hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), last_update_ts: 1234567890,
bids: vec![9.0f32, 300.0f32, 8.0f32, 300.0f32],
asks: vec![11.0f32, 300.0f32, 12.0f32, 300.0f32],
};
let tvl = price_data_eth_tamara.calculate_tvl(Some(&price_data_tamara_usdc));
assert_eq!(tvl, 3000.0);
}
#[test]
fn test_calculate_tvl_with_inverted_normalization() {
let price_data_eth_tamara = BebopPriceData {
base: hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(), quote: hex::decode("1234567890123456789012345678901234567890").unwrap(), last_update_ts: 1234567890,
bids: vec![99.0f32, 1.0f32, 98.0f32, 2.0f32],
asks: vec![101.0f32, 1.0f32, 102.0f32, 2.0f32],
};
let price_data_usdc_tamara = BebopPriceData {
base: hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap(), quote: hex::decode("1234567890123456789012345678901234567890").unwrap(), last_update_ts: 1234567890,
bids: vec![0.09f32, 3000.0f32, 0.08f32, 3000.0f32],
asks: vec![0.11f32, 3000.0f32, 0.12f32, 3000.0f32],
};
let tvl = price_data_eth_tamara.calculate_tvl(Some(&price_data_usdc_tamara));
assert!((tvl - 3051.14).abs() < 1.0);
}
#[test]
fn test_get_mid_price_bidirectional() {
let weth_addr = hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap();
let usdc_addr = hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
let price_data = BebopPriceData {
base: weth_addr.clone(),
quote: usdc_addr.clone(),
last_update_ts: 1234567890,
bids: vec![2000.0f32, 2.0f32, 1999.0f32, 3.0f32],
asks: vec![2001.0f32, 3.0f32, 2002.0f32, 1.0f32],
};
let usdc_price = price_data.get_mid_price(3.0, &weth_addr);
assert!((usdc_price.unwrap() - 2000.3333333333335).abs() < 0.01);
let weth_price = price_data.get_mid_price(6000.0, &usdc_addr);
assert!(weth_price.is_some());
let price = weth_price.unwrap();
assert!((price - 0.0005).abs() < 0.0001);
let dai_addr = hex::decode("6B175474E89094C44Da98b954EedeAC495271d0F").unwrap();
let result = price_data.get_mid_price(100.0, &dai_addr);
assert_eq!(result, None);
}
#[test]
fn test_get_mid_price() {
let weth_addr = hex::decode("C02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2").unwrap(); let usdc_addr = hex::decode("A0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48").unwrap();
let price_data = BebopPriceData {
base: weth_addr.clone(),
quote: usdc_addr.clone(),
last_update_ts: 1234567890,
bids: vec![2000.0f32, 2.0f32, 1999.0f32, 3.0f32],
asks: vec![2001.0f32, 3.0f32, 2002.0f32, 1.0f32],
};
let mid_price_large = price_data.get_mid_price(3.0, &weth_addr);
assert!((mid_price_large.unwrap() - 2000.3333333333335).abs() < 0.01);
let price_data = BebopPriceData {
base: weth_addr.clone(),
quote: usdc_addr.clone(),
last_update_ts: 1234567890,
bids: vec![],
asks: vec![2001.0f32, 3.0f32, 2002.0f32, 1.0f32],
};
assert_eq!(price_data.get_mid_price(3.0, &weth_addr), None);
let price_data = BebopPriceData {
base: weth_addr.clone(),
quote: usdc_addr.clone(),
last_update_ts: 1234567890,
bids: vec![2000.0f32, 2.0f32, 1999.0f32, 3.0f32],
asks: vec![],
};
assert_eq!(price_data.get_mid_price(3.0, &weth_addr), None);
let price_data = BebopPriceData {
base: weth_addr.clone(),
quote: usdc_addr.clone(),
last_update_ts: 1234567890,
bids: vec![2000.0f32, 2.0f32, 1999.0f32, 3.0f32],
asks: vec![2001.0f32, 3.0f32, 2002.0f32, 1.0f32],
};
let insufficient_mid = price_data.get_mid_price(10.0, &weth_addr);
assert_eq!(insufficient_mid, Some(2000.325));
}
#[test]
fn test_invert_price_levels() {
let price_levels = vec![(0.11, 3000.0), (0.12, 3000.0)];
let inverted = BebopPriceData::invert_price_levels(price_levels);
assert_eq!(inverted.len(), 2);
assert!((inverted[0].0 - 9.090909090909092).abs() < 0.0001);
assert!((inverted[0].1 - 330.0).abs() < 0.0001);
assert!((inverted[1].0 - 8.333333333333334).abs() < 0.0001);
assert!((inverted[1].1 - 360.0).abs() < 0.0001);
}
#[cfg(test)]
mod bebop_quote_partial_validate_tests {
use std::str::FromStr;
use num_bigint::BigUint;
use tycho_common::models::protocol::GetAmountOutParams;
use super::*;
fn hex_to_bytes(hex: &str) -> Bytes {
Bytes::from_str(hex).unwrap()
}
fn single_order() -> SingleOrderToSign {
SingleOrderToSign {
maker_address: hex_to_bytes("0x1111111111111111111111111111111111111111"),
taker_address: hex_to_bytes("0x2222222222222222222222222222222222222222"),
maker_token: hex_to_bytes("0x3333333333333333333333333333333333333333"),
taker_token: hex_to_bytes("0x4444444444444444444444444444444444444444"),
maker_amount: "2000".to_string(),
taker_amount: "1000".to_string(),
maker_nonce: "1".to_string(),
expiry: 123456,
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
}
}
fn aggregate_order() -> AggregateOrderToSign {
AggregateOrderToSign {
taker_address: hex_to_bytes("0x2222222222222222222222222222222222222222"),
maker_tokens: vec![vec![hex_to_bytes(
"0x3333333333333333333333333333333333333333",
)]],
taker_tokens: vec![vec![hex_to_bytes(
"0x4444444444444444444444444444444444444444",
)]],
maker_amounts: vec![vec!["2000".to_string()]],
taker_amounts: vec![vec!["1000".to_string()]],
expiry: 123456,
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
}
}
fn params() -> GetAmountOutParams {
GetAmountOutParams {
amount_in: BigUint::from(1000u32),
token_in: hex_to_bytes("0x4444444444444444444444444444444444444444"),
token_out: hex_to_bytes("0x3333333333333333333333333333333333333333"),
sender: hex_to_bytes("0x2222222222222222222222222222222222222222"),
receiver: hex_to_bytes("0x5555555555555555555555555555555555555555"),
}
}
fn quote_partial_single() -> BebopQuotePartial {
BebopQuotePartial {
status: "success".to_string(),
settlement_address: hex_to_bytes("0x9999999999999999999999999999999999999999"),
tx: TxData {
to: hex_to_bytes("0x8888888888888888888888888888888888888888"),
data: hex_to_bytes("0x1234"),
value: "0".to_string(),
from: hex_to_bytes("0x7777777777777777777777777777777777777777"),
gas: 21000,
gas_price: 100,
},
to_sign: BebopOrderToSign::Single(Box::new(single_order())),
partial_fill_offset: 0,
}
}
fn quote_partial_aggregate() -> BebopQuotePartial {
BebopQuotePartial {
status: "success".to_string(),
settlement_address: hex_to_bytes("0x9999999999999999999999999999999999999999"),
tx: TxData {
to: hex_to_bytes("0x8888888888888888888888888888888888888888"),
data: hex_to_bytes("0x1234"),
value: "0".to_string(),
from: hex_to_bytes("0x7777777777777777777777777777777777777777"),
gas: 21000,
gas_price: 100,
},
to_sign: BebopOrderToSign::Aggregate(Box::new(aggregate_order())),
partial_fill_offset: 0,
}
}
#[test]
fn test_validate_single_success() {
let quote = quote_partial_single();
let params = params();
assert!(quote.validate(¶ms).is_ok());
}
#[test]
fn test_validate_single_base_token_mismatch() {
let mut quote = quote_partial_single();
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
single.taker_token = hex_to_bytes("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Base token mismatch"));
}
#[test]
fn test_validate_single_quote_token_mismatch() {
let mut quote = quote_partial_single();
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
single.maker_token = hex_to_bytes("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Quote token mismatch"));
}
#[test]
fn test_validate_single_taker_address_mismatch() {
let mut quote = quote_partial_single();
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
single.taker_address = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Taker address mismatch"));
}
#[test]
fn test_validate_single_receiver_mismatch() {
let mut quote = quote_partial_single();
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
single.receiver = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Receiver address mismatch"));
}
#[test]
fn test_validate_single_base_token_amount_mismatch() {
let mut quote = quote_partial_single();
if let BebopOrderToSign::Single(ref mut single) = quote.to_sign {
single.taker_amount = "9999".to_string();
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Base token amount mismatch"));
}
#[test]
fn test_validate_aggregate_success() {
let quote = quote_partial_aggregate();
let params = params();
assert!(quote.validate(¶ms).is_ok());
}
#[test]
fn test_validate_aggregate_taker_address_mismatch() {
let mut quote = quote_partial_aggregate();
if let BebopOrderToSign::Aggregate(ref mut agg) = quote.to_sign {
agg.taker_address = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Taker address mismatch"));
}
#[test]
fn test_validate_aggregate_receiver_mismatch() {
let mut quote = quote_partial_aggregate();
if let BebopOrderToSign::Aggregate(ref mut agg) = quote.to_sign {
agg.receiver = hex_to_bytes("0xabcdefabcdefabcdefabcdefabcdefabcdefabcd");
}
let params = params();
let err = quote.validate(¶ms).unwrap_err();
assert!(format!("{err:?}").contains("Receiver address mismatch"));
}
}
}