use std::fmt;
use fynd_core::types::ComponentId;
use serde::{Deserialize, Serialize};
use tycho_simulation::tycho_common::models::Address;
use utoipa::{IntoParams, ToSchema};
const PRICE_DECIMAL_PRECISION: usize = 17;
#[derive(Debug, Default, Deserialize, IntoParams)]
pub struct PricesQuery {
#[param(example = "depths,spot_prices")]
pub include: Option<String>,
#[param(example = 1000)]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IncludeField {
Depths,
SpotPrices,
}
impl IncludeField {
pub fn parse_include(raw: &str) -> Result<Vec<Self>, String> {
let mut fields = Vec::new();
for part in raw.split(',') {
let trimmed = part.trim();
if trimmed.is_empty() {
continue;
}
match trimmed {
"depths" => fields.push(Self::Depths),
"spot_prices" => fields.push(Self::SpotPrices),
other => {
return Err(format!(
"unknown include field '{}'. Valid values: depths, spot_prices",
other,
));
}
}
}
Ok(fields)
}
}
impl fmt::Display for IncludeField {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Depths => write!(f, "depths"),
Self::SpotPrices => write!(f, "spot_prices"),
}
}
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ComputationBlocks {
pub token_prices: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub spot_prices: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component_depths: Option<u64>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct PricesResponse {
pub prices: Vec<TokenPriceEntry>,
#[schema(value_type = String, example = "0x0000000000000000000000000000000000000000")]
pub gas_token: Address,
pub blocks: ComputationBlocks,
#[serde(skip_serializing_if = "Option::is_none")]
pub spot_prices: Option<Vec<SpotPriceEntry>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub component_depths: Option<Vec<ComponentDepthEntry>>,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct TokenPriceEntry {
#[schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")]
pub token: Address,
#[schema(value_type = String, example = "0.000000003")]
pub price: String,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct SpotPriceEntry {
pub component_id: ComponentId,
#[schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")]
pub token_in: Address,
#[schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")]
pub token_out: Address,
pub price: f64,
}
#[derive(Debug, Serialize, ToSchema)]
pub struct ComponentDepthEntry {
pub component_id: ComponentId,
#[schema(value_type = String, example = "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2")]
pub token_in: Address,
#[schema(value_type = String, example = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")]
pub token_out: Address,
pub depth: String,
}
const MAX_PRICE_OPERAND_BITS: u64 = 1328;
pub fn price_to_decimal_string(
numerator: &num_bigint::BigUint,
denominator: &num_bigint::BigUint,
) -> Option<String> {
use num_traits::Zero;
if denominator.is_zero() || numerator.is_zero() {
return None;
}
if numerator.bits() > MAX_PRICE_OPERAND_BITS || denominator.bits() > MAX_PRICE_OPERAND_BITS {
return None;
}
Some(biguint_division_to_decimal_string(numerator, denominator, PRICE_DECIMAL_PRECISION))
}
fn biguint_division_to_decimal_string(
numerator: &num_bigint::BigUint,
denominator: &num_bigint::BigUint,
max_sig_digits: usize,
) -> String {
use num_bigint::BigUint;
use num_traits::{ToPrimitive, Zero};
let int_part = numerator / denominator;
let remainder = numerator % denominator;
let int_str = int_part.to_string();
if remainder.is_zero() {
return truncate_sig_digits(&int_str, max_sig_digits);
}
let int_sig = int_str.trim_start_matches('0').len();
let remaining_sig = max_sig_digits.saturating_sub(int_sig);
if remaining_sig == 0 {
return truncate_sig_digits(&int_str, max_sig_digits);
}
let mut frac_digits = String::new();
let mut rem = remainder;
let ten = BigUint::from(10u8);
let mut sig_count = 0usize;
let mut hit_nonzero = int_sig > 0;
while sig_count < remaining_sig {
if rem.is_zero() {
break;
}
rem *= &ten;
let digit = (&rem / denominator)
.to_u8()
.expect("long-division digit is < 10 because rem < denominator before the multiply");
rem = &rem % denominator;
frac_digits.push(char::from(b'0' + digit));
if digit != 0 {
hit_nonzero = true;
}
if hit_nonzero {
sig_count += 1;
}
}
let frac_trimmed = frac_digits.trim_end_matches('0');
if frac_trimmed.is_empty() {
int_str
} else {
format!("{int_str}.{frac_trimmed}")
}
}
fn truncate_sig_digits(digits: &str, max_sig: usize) -> String {
let trimmed = digits.trim_start_matches('0');
if trimmed.is_empty() {
return "0".to_string();
}
if trimmed.len() <= max_sig {
return trimmed.to_string();
}
let mut truncated = trimmed[..max_sig].to_string();
truncated.push_str(&"0".repeat(trimmed.len() - max_sig));
truncated
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use num_bigint::BigUint;
use super::*;
#[test]
fn test_parse_include_empty() {
assert_eq!(IncludeField::parse_include("").unwrap(), vec![]);
}
#[test]
fn test_parse_include_depths() {
let fields = IncludeField::parse_include("depths").unwrap();
assert_eq!(fields, vec![IncludeField::Depths]);
}
#[test]
fn test_parse_include_spot_prices() {
let fields = IncludeField::parse_include("spot_prices").unwrap();
assert_eq!(fields, vec![IncludeField::SpotPrices]);
}
#[test]
fn test_parse_include_both() {
let fields = IncludeField::parse_include("depths,spot_prices").unwrap();
assert_eq!(fields, vec![IncludeField::Depths, IncludeField::SpotPrices]);
}
#[test]
fn test_parse_include_with_whitespace() {
let fields = IncludeField::parse_include(" depths , spot_prices ").unwrap();
assert_eq!(fields, vec![IncludeField::Depths, IncludeField::SpotPrices]);
}
#[test]
fn test_parse_include_unknown_rejects() {
let err = IncludeField::parse_include("depths,foobar").unwrap_err();
assert!(err.contains("foobar"));
}
#[test]
fn test_decimal_string_exact_integer() {
let n = BigUint::from(1500u64);
let d = BigUint::from(1u8);
assert_eq!(price_to_decimal_string(&n, &d).unwrap(), "1500");
}
#[test]
fn test_decimal_string_small_fraction() {
let n = BigUint::from(3u64);
let d = BigUint::from(1_000_000_000u64);
assert_eq!(price_to_decimal_string(&n, &d).unwrap(), "0.000000003");
}
#[test]
fn test_decimal_string_one_half() {
let n = BigUint::from(1u64);
let d = BigUint::from(2u64);
assert_eq!(price_to_decimal_string(&n, &d).unwrap(), "0.5");
}
#[test]
fn test_decimal_string_trailing_zeros_trimmed() {
let n = BigUint::from(5u64);
let d = BigUint::from(10u64);
assert_eq!(price_to_decimal_string(&n, &d).unwrap(), "0.5");
}
#[test]
fn test_decimal_string_repeating_truncated() {
let n = BigUint::from(1u64);
let d = BigUint::from(3u64);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "0.33333333333333333");
}
#[test]
fn test_decimal_string_zero_numerator_returns_none() {
assert!(price_to_decimal_string(&BigUint::from(0u8), &BigUint::from(1u8)).is_none());
}
#[test]
fn test_decimal_string_zero_denominator_returns_none() {
assert!(price_to_decimal_string(&BigUint::from(1u8), &BigUint::from(0u8)).is_none());
}
#[test]
fn test_decimal_string_no_scientific_notation() {
let n = BigUint::from(10u8).pow(21);
let d = BigUint::from(1u8);
let s = price_to_decimal_string(&n, &d).unwrap();
assert!(!s.contains('e') && !s.contains('E'));
assert_eq!(s, format!("1{}", "0".repeat(21)));
}
#[test]
fn test_decimal_string_max_significant_digits() {
let n = BigUint::from(123_456_789_012_345_678_901u128);
let d = BigUint::from(1u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "123456789012345670000");
}
#[test]
fn test_decimal_string_large_integer_with_fraction() {
let n = BigUint::from(4501u64);
let d = BigUint::from(3u64);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "1500.3333333333333");
}
#[test]
fn test_decimal_string_truncate_preserves_trailing_zeros() {
let n = BigUint::from(10u64).pow(17);
let d = BigUint::from(1u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "100000000000000000");
}
#[test]
fn test_decimal_string_truncate_with_internal_zeros() {
let n = BigUint::from(12u64) * BigUint::from(10u64).pow(16);
let d = BigUint::from(1u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "120000000000000000");
}
#[test]
fn test_decimal_string_int_part_exactly_17_digits_with_fraction() {
let n = BigUint::from(3u64) * BigUint::from(10u64).pow(16) + BigUint::from(1u8);
let d = BigUint::from(3u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "10000000000000000");
}
#[test]
fn test_decimal_string_int_part_18_digits_with_fraction() {
let n = BigUint::from(3u64) * BigUint::from(10u64).pow(17) + BigUint::from(1u8);
let d = BigUint::from(3u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "100000000000000000");
}
#[test]
fn test_decimal_string_large_value_within_operand_bound() {
let n = BigUint::from(10u8).pow(100);
let d = BigUint::from(1u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, format!("1{}", "0".repeat(100)));
}
#[test]
fn test_decimal_string_small_nonzero_not_zero() {
let n = BigUint::from(1u64);
let d = BigUint::from(3u64) * BigUint::from(10u64).pow(18);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "0.00000000000000000033333333333333333");
}
#[test]
fn test_decimal_string_leading_frac_zeros_full_precision() {
let n = BigUint::from(1u64);
let d = BigUint::from(700u64);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "0.0014285714285714285");
}
#[test]
fn test_decimal_string_small_repeating_fraction() {
let n = BigUint::from(1u64);
let d = BigUint::from(3u64) * BigUint::from(10u64).pow(9);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "0.00000000033333333333333333");
}
#[test]
fn test_decimal_string_fractional_zeros_after_nonzero_int_are_significant() {
let n = BigUint::from(10u64).pow(30) + BigUint::from(1u8);
let d = BigUint::from(10u64).pow(30);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, "1");
}
#[test]
fn test_decimal_string_sig_digit_budget_invariant() {
let cases = [
(BigUint::from(1u8), BigUint::from(3u8)),
(BigUint::from(4501u64), BigUint::from(3u8)),
(BigUint::from(10u64).pow(17), BigUint::from(1u8)),
(BigUint::from(123_456_789_012_345_678_901u128), BigUint::from(1u8)),
(BigUint::from(10u8).pow(100), BigUint::from(1u8)),
(BigUint::from(10u64).pow(30) + BigUint::from(1u8), BigUint::from(10u64).pow(30)),
(BigUint::from(10u8).pow(399) + BigUint::from(1u8), BigUint::from(10u8).pow(399)),
(BigUint::from(1u8), BigUint::from(700u64)),
(BigUint::from(1u8), BigUint::from(3u64) * BigUint::from(10u64).pow(18)),
(BigUint::from_str("999999999999999999").unwrap(), BigUint::from(7u8)),
];
for (numerator, denominator) in cases {
let price = price_to_decimal_string(&numerator, &denominator).unwrap();
let digits: String = price
.chars()
.filter(|c| c.is_ascii_digit())
.collect();
let significant = digits
.trim_start_matches('0')
.trim_end_matches('0')
.len();
assert!(
significant <= PRICE_DECIMAL_PRECISION,
"{numerator}/{denominator} -> {price} has {significant} significant digits"
);
}
}
#[test]
fn test_decimal_string_max_operand_accepted() {
let n = BigUint::from(10u8).pow(399);
let d = BigUint::from(1u8);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, format!("1{}", "0".repeat(399)));
}
#[test]
fn test_decimal_string_max_denominator_accepted() {
let n = BigUint::from(1u8);
let d = BigUint::from(10u8).pow(399);
let result = price_to_decimal_string(&n, &d).unwrap();
assert_eq!(result, format!("0.{}1", "0".repeat(398)));
}
#[test]
fn test_decimal_string_pathological_size_rejected() {
let n = BigUint::from(10u8).pow(400);
let d = BigUint::from(1u8);
assert!(price_to_decimal_string(&n, &d).is_none());
}
#[test]
fn test_decimal_string_pathological_denominator_rejected() {
let n = BigUint::from(1u8);
let d = BigUint::from(10u8).pow(400);
assert!(price_to_decimal_string(&n, &d).is_none());
}
}