use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tycho_common::simulation::errors::SimulationError;
use super::enums::FeeAmount;
const FEE_DENOMINATOR: u32 = 1_000_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct FeeTier {
fee: u32,
tick_spacing: u16,
}
impl FeeTier {
pub fn new(fee: u32, tick_spacing: u16) -> Result<Self, SimulationError> {
if fee >= FEE_DENOMINATOR {
return Err(SimulationError::InvalidInput(
format!("Fee {fee} must be below {FEE_DENOMINATOR}"),
None,
));
}
if tick_spacing == 0 {
return Err(SimulationError::InvalidInput("Tick spacing must be positive".into(), None));
}
Ok(FeeTier { fee, tick_spacing })
}
pub fn fee(&self) -> u32 {
self.fee
}
pub fn tick_spacing(&self) -> u16 {
self.tick_spacing
}
}
impl From<FeeAmount> for FeeTier {
fn from(fee: FeeAmount) -> Self {
let tick_spacing = match fee {
FeeAmount::Lowest => 1,
FeeAmount::Lowest2 => 2,
FeeAmount::Lowest3 => 3,
FeeAmount::Lowest4 => 4,
FeeAmount::Low => 10,
FeeAmount::MediumLow => 50,
FeeAmount::Medium => 60,
FeeAmount::MediumHigh => 100,
FeeAmount::High => 200,
};
FeeTier { fee: fee as u32, tick_spacing }
}
}
pub(super) mod fee_serde {
use super::*;
#[derive(Serialize, Deserialize)]
#[serde(untagged)]
enum SerializedFee {
Named(FeeAmount),
Pips(u32),
}
pub(in crate::evm::protocol::uniswap_v3) fn serialize<S: Serializer>(
fee: &u32,
serializer: S,
) -> Result<S::Ok, S::Error> {
let named = i32::try_from(*fee)
.ok()
.and_then(|fee| FeeAmount::try_from(fee).ok());
match named {
Some(fee) => SerializedFee::Named(fee),
None => SerializedFee::Pips(*fee),
}
.serialize(serializer)
}
pub(in crate::evm::protocol::uniswap_v3) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<u32, D::Error> {
match SerializedFee::deserialize(deserializer)? {
SerializedFee::Named(fee) => Ok(fee as u32),
SerializedFee::Pips(fee) => Ok(fee),
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case::lowest(FeeAmount::Lowest, 100, 1)]
#[case::lowest2(FeeAmount::Lowest2, 200, 2)]
#[case::low(FeeAmount::Low, 500, 10)]
#[case::medium_low(FeeAmount::MediumLow, 2500, 50)]
#[case::medium(FeeAmount::Medium, 3000, 60)]
#[case::medium_high(FeeAmount::MediumHigh, 5000, 100)]
#[case::high(FeeAmount::High, 10_000, 200)]
fn test_fee_amount_keeps_legacy_spacing(
#[case] fee: FeeAmount,
#[case] expected_fee: u32,
#[case] expected_spacing: u16,
) {
let tier = FeeTier::from(fee);
assert_eq!(tier.fee(), expected_fee);
assert_eq!(tier.tick_spacing(), expected_spacing);
}
#[rstest]
#[case::half_bip(50, 10)]
#[case::zero_fee(0, 1)]
#[case::largest_fee(999_999, 1)]
fn test_new_accepts_fee_outside_fee_amount(#[case] fee: u32, #[case] tick_spacing: u16) {
let tier = FeeTier::new(fee, tick_spacing).unwrap();
assert_eq!(tier.fee(), fee);
assert_eq!(tier.tick_spacing(), tick_spacing);
}
#[rstest]
#[case::full_fee(1_000_000, 1)]
#[case::zero_spacing(500, 0)]
fn test_new_rejects_out_of_range(#[case] fee: u32, #[case] tick_spacing: u16) {
assert!(matches!(
FeeTier::new(fee, tick_spacing),
Err(SimulationError::InvalidInput(_, None))
));
}
}