use std::str::FromStr;
use alloy::primitives::Address;
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use tycho_common::Bytes;
use crate::rfq::errors::RFQError;
const Q64_FLOAT: f64 = 18_446_744_073_709_551_616.0;
mod biguint_string {
use super::*;
pub fn serialize<S: Serializer>(value: &BigUint, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(value)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<BigUint, D::Error> {
let raw = String::deserialize(deserializer)?;
BigUint::from_str(&raw).map_err(serde::de::Error::custom)
}
}
mod option_biguint_string {
use super::*;
pub fn serialize<S: Serializer>(
value: &Option<BigUint>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match value {
Some(value) => serializer.collect_str(value),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<BigUint>, D::Error> {
Option::<String>::deserialize(deserializer)?
.map(|raw| BigUint::from_str(&raw).map_err(serde::de::Error::custom))
.transpose()
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct PaginatedMetadataResponse {
pub data: Vec<MetricMetadata>,
#[serde(rename = "nextOffset", default)]
pub next_offset: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricMetadata {
#[serde(rename = "poolAddress", deserialize_with = "deserialize_address")]
pub pool_address: Bytes,
#[serde(deserialize_with = "deserialize_address")]
pub token0: Bytes,
#[serde(deserialize_with = "deserialize_address")]
pub token1: Bytes,
#[serde(rename = "tvlFiat", default)]
pub tvl_fiat: Option<f64>,
}
fn deserialize_address<'de, D>(deserializer: D) -> Result<Bytes, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let address = Address::from_str(&s).map_err(serde::de::Error::custom)?;
Bytes::from_str(&address.to_checksum(None)).map_err(serde::de::Error::custom)
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricBidAskResponse {
#[serde(rename = "bidAdj", with = "biguint_string")]
pub bid_adj: BigUint,
#[serde(rename = "askAdj", with = "biguint_string")]
pub ask_adj: BigUint,
#[serde(rename = "totalToken0Available", default, with = "option_biguint_string")]
pub total_token0_available: Option<BigUint>,
#[serde(rename = "totalToken1Available", default, with = "option_biguint_string")]
pub total_token1_available: Option<BigUint>,
#[serde(rename = "serverTs")]
pub server_ts: u64,
#[serde(rename = "priceProviderStatus", default)]
pub price_provider_status: Option<String>,
#[serde(default, deserialize_with = "null_as_default")]
pub depth: MetricDepth,
}
fn null_as_default<'de, D, T>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: Default + Deserialize<'de>,
{
Ok(Option::<T>::deserialize(deserializer)?.unwrap_or_default())
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct MetricDepth {
#[serde(default)]
pub asks: Vec<MetricDepthBin>,
#[serde(default)]
pub bids: Vec<MetricDepthBin>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricDepthBin {
#[serde(rename = "binIdx")]
pub bin_idx: i64,
#[serde(with = "biguint_string")]
pub price: BigUint,
#[serde(rename = "cumulativeVolume", with = "biguint_string")]
pub cumulative_volume: BigUint,
#[serde(rename = "cumulativeInputVolume", with = "biguint_string")]
pub cumulative_input_volume: BigUint,
}
impl MetricBidAskResponse {
pub fn bid_price(&self) -> Result<f64, RFQError> {
q64_to_f64(&self.bid_adj)
}
pub fn ask_price(&self) -> Result<f64, RFQError> {
q64_to_f64(&self.ask_adj)
}
pub fn total_token0_available(&self) -> Result<BigUint, RFQError> {
self.total_token0_available
.clone()
.ok_or_else(|| RFQError::ParsingError("totalToken0Available is null".to_string()))
}
pub fn total_token1_available(&self) -> Result<BigUint, RFQError> {
self.total_token1_available
.clone()
.ok_or_else(|| RFQError::ParsingError("totalToken1Available is null".to_string()))
}
pub fn is_quotable(&self) -> bool {
if let Some(status) = self.price_provider_status.as_deref() {
if status != "healthy" {
return false;
}
}
self.total_token0_available.is_some() &&
self.total_token1_available.is_some() &&
!(self.depth.bids.is_empty() && self.depth.asks.is_empty())
}
}
pub fn q64_to_f64(value: &BigUint) -> Result<f64, RFQError> {
let raw = value
.to_f64()
.ok_or_else(|| RFQError::ParsingError(format!("Q64 price does not fit in f64: {value}")))?;
Ok(raw / Q64_FLOAT)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_q64_to_f64() {
let one = BigUint::from_str("18446744073709551616").unwrap();
assert_eq!(q64_to_f64(&one).unwrap(), 1.0);
}
#[test]
fn test_bid_ask_deserializes_depth_bins() {
let response: MetricBidAskResponse = serde_json::from_value(serde_json::json!({
"bidAdj": "55340232221128654848000",
"askAdj": "55524699661865750400000",
"totalToken0Available": "1000000000000000000",
"totalToken1Available": "3000000000",
"serverTs": 0,
"depth": {
"asks": [{
"binIdx": 0,
"price": "57184906628499610009600",
"cumulativeVolume": "1000000000000000000",
"cumulativeInputVolume": "3100000000",
"priceImpactE6": "33333"
}],
"bids": [{
"binIdx": -1,
"price": "53495557813757699686400",
"cumulativeVolume": "3000000000",
"cumulativeInputVolume": "1000000000000000000",
"priceImpactE6": "33333"
}]
}
}))
.unwrap();
assert_eq!(response.depth.asks.len(), 1);
assert_eq!(response.depth.bids[0].bin_idx, -1);
assert_eq!(q64_to_f64(&response.depth.asks[0].price).unwrap(), 3100.0);
assert_eq!(response.depth.bids[0].cumulative_volume, BigUint::from(3_000_000_000u64));
assert_eq!(
response.depth.bids[0].cumulative_input_volume,
BigUint::from(1_000_000_000_000_000_000u64)
);
let serialized = serde_json::to_value(&response).unwrap();
assert_eq!(serialized["bidAdj"], "55340232221128654848000");
assert_eq!(serialized["totalToken1Available"], "3000000000");
assert_eq!(serialized["depth"]["asks"][0]["cumulativeInputVolume"], "3100000000");
}
#[test]
fn test_bid_ask_null_totals_are_not_quotable() {
let response: MetricBidAskResponse = serde_json::from_value(serde_json::json!({
"bidAdj": "55340232221128654848000",
"askAdj": "55524699661865750400000",
"totalToken0Available": null,
"totalToken1Available": null,
"serverTs": 1_770_053_095u64,
}))
.unwrap();
assert_eq!(response.total_token0_available, None);
assert!(!response.is_quotable());
}
#[test]
fn test_bid_ask_null_depth_decodes_as_empty() {
let response: MetricBidAskResponse = serde_json::from_value(serde_json::json!({
"bidAdj": "55340232221128654848000",
"askAdj": "55524699661865750400000",
"totalToken0Available": "1000000000000000000",
"totalToken1Available": "3000000000",
"serverTs": 0,
"depth": null,
}))
.unwrap();
assert_eq!(response.depth, MetricDepth::default());
assert!(!response.is_quotable());
}
#[test]
fn test_bid_ask_empty_depth_is_not_quotable() {
let response: MetricBidAskResponse = serde_json::from_value(serde_json::json!({
"bidAdj": "0",
"askAdj": "340282366920938463463374607431768211455",
"totalToken0Available": "11419581536531814910",
"totalToken1Available": "12935676138",
"serverTs": 1_784_611_959u64,
}))
.unwrap();
assert!(response.depth.bids.is_empty() && response.depth.asks.is_empty());
assert!(!response.is_quotable());
}
#[test]
fn test_bid_ask_unhealthy_provider_status_is_not_quotable() {
let quotable = serde_json::json!({
"bidAdj": "55340232221128654848000",
"askAdj": "55524699661865750400000",
"totalToken0Available": "1000000000000000000",
"totalToken1Available": "3000000000",
"serverTs": 1_787_710_476u64,
"depth": {
"asks": [{
"binIdx": 0,
"price": "57184906628499610009600",
"cumulativeVolume": "1000000000000000000",
"cumulativeInputVolume": "3100000000"
}],
"bids": []
}
});
for (status, expected) in
[("healthy", true), ("feed_down", false), ("internal_error", false)]
{
let mut payload = quotable.clone();
payload["priceProviderStatus"] = serde_json::json!(status);
let response: MetricBidAskResponse = serde_json::from_value(payload).unwrap();
assert_eq!(response.is_quotable(), expected, "status {status}");
}
let response: MetricBidAskResponse = serde_json::from_value(quotable).unwrap();
assert_eq!(response.price_provider_status, None);
assert!(response.is_quotable());
}
#[test]
fn test_paginated_metadata_deserializes_tvl_fiat() {
let response: PaginatedMetadataResponse = serde_json::from_value(serde_json::json!({
"data": [{
"pair": "ethusdc",
"poolAddress": "0xbF48bCf474d57fF82A3215319229e0DE1476A557",
"token0": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"token1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"tvlFiat": 1234.56
}],
"total": 1,
"nextOffset": null
}))
.unwrap();
assert_eq!(response.next_offset, None);
assert_eq!(response.data[0].tvl_fiat, Some(1234.56));
}
}