tycho-simulation 0.331.0

Provides tools for interacting with protocol states, calculating spot prices, and quoting token swaps.
Documentation
use std::str::FromStr;

use alloy::primitives::Address;
use num_bigint::BigUint;
use num_traits::ToPrimitive;
use serde::{Deserialize, Serialize};
use tycho_common::{models::Chain, Bytes};

use crate::rfq::errors::RFQError;

const Q64_FLOAT: f64 = 18_446_744_073_709_551_616.0;

pub const ORACLE_UPDATE_POLICY_ATTR: &str = "oracle_update_policy";

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[repr(u8)]
pub enum MetricOracleUpdatePolicy {
    #[default]
    Never = 0,
    Always = 1,
    RetryOnRevert = 2,
}

impl MetricOracleUpdatePolicy {
    pub fn default_for_chain(chain: Chain) -> Self {
        match chain {
            Chain::Ethereum => Self::RetryOnRevert,
            _ => Self::Never,
        }
    }

    pub fn as_attribute_value(self) -> Bytes {
        vec![self as u8].into()
    }
}

#[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,
}

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")]
    pub bid_adj: String,
    #[serde(rename = "askAdj")]
    pub ask_adj: String,
    #[serde(rename = "quoteAvailable")]
    pub quote_available: bool,
    #[serde(rename = "totalToken0Available")]
    pub total_token0_available: String,
    #[serde(rename = "totalToken1Available")]
    pub total_token1_available: String,
    #[serde(rename = "latestBlock")]
    pub latest_block: u64,
    #[serde(default)]
    pub depth: MetricDepth,
}

#[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,
    pub price: String,
    #[serde(rename = "cumulativeVolume")]
    pub cumulative_volume: String,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricSignedOracleUpdateResponse {
    #[serde(rename = "feedCreator", deserialize_with = "deserialize_address")]
    pub feed_creator: Bytes,
    #[serde(default)]
    pub slots: Vec<MetricSignedOracleUpdateSlot>,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MetricSignedOracleUpdateSlot {
    pub deadline: u64,
    #[serde(rename = "newSlotValue")]
    pub new_slot_value: String,
    pub signature: Bytes,
}

impl MetricBidAskResponse {
    pub fn bid_price(&self) -> Result<f64, RFQError> {
        q64_decimal_to_f64(&self.bid_adj)
    }

    pub fn ask_price(&self) -> Result<f64, RFQError> {
        q64_decimal_to_f64(&self.ask_adj)
    }

    pub fn total_token0_available(&self) -> Result<BigUint, RFQError> {
        parse_biguint(&self.total_token0_available, "totalToken0Available")
    }

    pub fn total_token1_available(&self) -> Result<BigUint, RFQError> {
        parse_biguint(&self.total_token1_available, "totalToken1Available")
    }
}

impl MetricDepthBin {
    pub fn price(&self) -> Result<f64, RFQError> {
        q64_decimal_to_f64(&self.price)
    }

    pub fn cumulative_volume(&self) -> Result<BigUint, RFQError> {
        parse_biguint(&self.cumulative_volume, "depth.cumulativeVolume")
    }
}

// Metric's APIs return Q64 values as decimal strings. Convert only when pricing.
pub fn q64_decimal_to_f64(value: &str) -> Result<f64, RFQError> {
    let raw = parse_biguint(value, "Q64 price")?;
    let raw = raw
        .to_f64()
        .ok_or_else(|| RFQError::ParsingError(format!("Q64 price does not fit in f64: {value}")))?;
    Ok(raw / Q64_FLOAT)
}

fn parse_biguint(value: &str, field: &str) -> Result<BigUint, RFQError> {
    BigUint::from_str(value)
        .map_err(|_| RFQError::ParsingError(format!("Failed to parse {field}: {value}")))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_q64_decimal_to_f64() {
        let one = "18446744073709551616";
        assert_eq!(q64_decimal_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",
            "quoteAvailable": true,
            "totalToken0Available": "1000000000000000000",
            "totalToken1Available": "3000000000",
            "latestBlock": 0,
            "depth": {
                "asks": [{
                    "binIdx": 0,
                    "price": "57184906628499610009600",
                    "cumulativeVolume": "1000000000000000000",
                    "priceImpactE6": "33333"
                }],
                "bids": [{
                    "binIdx": -1,
                    "price": "53495557813757699686400",
                    "cumulativeVolume": "3000000000",
                    "priceImpactE6": "33333"
                }]
            }
        }))
        .unwrap();

        assert_eq!(response.depth.asks.len(), 1);
        assert_eq!(response.depth.bids[0].bin_idx, -1);
        assert_eq!(response.depth.asks[0].price().unwrap(), 3100.0);
        assert_eq!(
            response.depth.bids[0]
                .cumulative_volume()
                .unwrap(),
            BigUint::from(3_000_000_000u64)
        );
    }
}