chio-link 0.1.2

Oracle runtime for Chio cross-currency budget enforcement
Documentation
use crate::{ExchangeRate, PriceOracleError};

const BPS_DENOMINATOR: u128 = 10_000;

pub fn minor_units_for_currency(currency: &str) -> Result<u64, PriceOracleError> {
    match currency.trim().to_ascii_uppercase().as_str() {
        "USD" | "EUR" | "GBP" => Ok(100),
        "JPY" => Ok(1),
        "USDC" | "USDT" => Ok(1_000_000),
        "BTC" => Ok(100_000_000),
        "ETH" | "LINK" => Ok(1_000_000_000_000_000_000),
        other => Err(PriceOracleError::InvalidConfiguration(format!(
            "no default minor-unit scale is pinned for currency {other}"
        ))),
    }
}

pub fn convert_supported_units(
    original_units: u64,
    rate: &ExchangeRate,
    margin_bps: u32,
) -> Result<u64, PriceOracleError> {
    convert_units(
        original_units,
        minor_units_for_currency(&rate.base)?,
        minor_units_for_currency(&rate.quote)?,
        rate,
        margin_bps,
    )
}

pub fn convert_units(
    original_units: u64,
    base_minor_units_per_unit: u64,
    quote_minor_units_per_unit: u64,
    rate: &ExchangeRate,
    margin_bps: u32,
) -> Result<u64, PriceOracleError> {
    if base_minor_units_per_unit == 0 || quote_minor_units_per_unit == 0 {
        return Err(PriceOracleError::InvalidConfiguration(
            "currency scales must be non-zero".to_string(),
        ));
    }
    if rate.rate_denominator == 0 {
        return Err(PriceOracleError::InvalidFeed(format!(
            "{} returned a zero rate denominator",
            rate.pair()
        )));
    }
    let numerator = u128::from(original_units)
        .checked_mul(rate.rate_numerator)
        .and_then(|value| value.checked_mul(u128::from(quote_minor_units_per_unit)))
        .ok_or_else(|| {
            PriceOracleError::ArithmeticOverflow(format!(
                "conversion numerator overflowed for {}",
                rate.pair()
            ))
        })?;
    let denominator = u128::from(base_minor_units_per_unit)
        .checked_mul(rate.rate_denominator)
        .ok_or_else(|| {
            PriceOracleError::ArithmeticOverflow(format!(
                "conversion denominator overflowed for {}",
                rate.pair()
            ))
        })?;
    let converted = numerator.div_ceil(denominator);
    let with_margin = if margin_bps == 0 {
        converted
    } else {
        converted
            .checked_mul(BPS_DENOMINATOR + u128::from(margin_bps))
            .ok_or_else(|| {
                PriceOracleError::ArithmeticOverflow(format!(
                    "margin application overflowed for {}",
                    rate.pair()
                ))
            })?
            .div_ceil(BPS_DENOMINATOR)
    };
    u64::try_from(with_margin).map_err(|_| {
        PriceOracleError::ArithmeticOverflow(format!(
            "converted units exceeded u64 for {}",
            rate.pair()
        ))
    })
}

#[cfg(test)]
mod tests {
    use crate::test_support::TestUnwrap;
    use crate::ExchangeRate;

    use super::{convert_supported_units, convert_units, minor_units_for_currency};

    fn sample_rate() -> ExchangeRate {
        ExchangeRate {
            base: "ETH".to_string(),
            quote: "USD".to_string(),
            rate_numerator: 300_000,
            rate_denominator: 100,
            updated_at: 1_743_292_740,
            fetched_at: 1_743_292_785,
            source: "chainlink".to_string(),
            feed_reference: "0x71041dddad3595F9CEd3DcCFBe3D1F4b0a16Bb70".to_string(),
            max_age_seconds: 600,
            conversion_margin_bps: 200,
            confidence_numerator: None,
            confidence_denominator: None,
        }
    }

    #[test]
    fn converts_with_ceiling_rounding() {
        let converted = convert_units(
            1_000_000_000_000_000,
            1_000_000_000_000_000_000,
            100,
            &sample_rate(),
            0,
        )
        .test_unwrap("converted");
        assert_eq!(converted, 300);
    }

    #[test]
    fn applies_margin_conservatively() {
        let converted = convert_units(
            1_000_000_000_000_000,
            1_000_000_000_000_000_000,
            100,
            &sample_rate(),
            200,
        )
        .test_unwrap("converted");
        assert_eq!(converted, 306);
    }

    #[test]
    fn resolves_supported_currency_scales() {
        assert_eq!(minor_units_for_currency("USD").test_unwrap("usd"), 100);
        assert_eq!(
            minor_units_for_currency("ETH").test_unwrap("eth"),
            10_u64.pow(18)
        );
    }

    #[test]
    fn converts_with_default_supported_scales() {
        let converted = convert_supported_units(1_000_000_000_000_000, &sample_rate(), 0)
            .test_unwrap("converted");
        assert_eq!(converted, 300);
    }
}