bolt-cw-sdk 1.0.0

SDK for the BOLT protocol, providing utilities for interacting with the BOLT contracts.
Documentation
use cosmwasm_std::{Decimal256, Uint256};
use num_bigint::BigUint;
use num_rational::{Ratio, Rational64};
use num_traits::{FromBytes, ToBytes, ToPrimitive, Zero};
use std::fmt::{Debug, Display};
use std::str::FromStr;
use thiserror::Error as thiserrorError;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fraction(Ratio<BigUint>);

impl Fraction {
    pub fn new(numerator: BigUint, denominator: BigUint) -> Result<Self, FractionError> {
        if denominator.is_zero() {
            return Err(FractionError::ZeroDenominator);
        }

        Ok(Self::new_raw(numerator, denominator))
    }

    pub fn new_raw(numerator: BigUint, denominator: BigUint) -> Self {
        Self(Ratio::new_raw(numerator, denominator))
    }

    pub fn into_raw(self) -> (BigUint, BigUint) {
        self.0.into_raw()
    }

    pub fn from_decimal_string(decimal_str: &str) -> Result<Fraction, FractionError> {
        let dec = Decimal256::from_str(decimal_str)?;
        Ok(dec.into())
    }

    pub fn ratio(&self) -> &Ratio<BigUint> {
        &self.0
    }

    pub fn numerator(&self) -> &BigUint {
        self.ratio().numer()
    }

    pub fn denominator(&self) -> &BigUint {
        self.ratio().denom()
    }

    pub fn reduced(self) -> Self {
        Self(self.0.reduced())
    }

    /// Converts blockchain prices to human readable, aarch/uusdt -> ARCH/USDT
    pub fn to_human_precision(self, base_precision: u8, quote_precision: u8) -> Self {
        let ratio = Ratio::new(
            BigUint::from(10u64.pow(base_precision as u32)),
            BigUint::from(10u64.pow(quote_precision as u32)),
        )
        .reduced();

        self * ratio.into()
    }
}

#[derive(Debug, thiserrorError, PartialEq)]
pub enum FractionError {
    #[error("Cosmwasm Error: {0}")]
    CosmwasmError(#[from] cosmwasm_std::StdError),
    #[error("Numerator too large to fit in f64")]
    NumeratorOverflow,
    #[error("Denominator too large to fit in f64")]
    DenominatorOverflow,
    #[error("Denominator is zero")]
    ZeroDenominator,
}

impl Display for Fraction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        std::fmt::Display::fmt(&self.ratio(), f)
    }
}

impl From<Fraction> for Ratio<BigUint> {
    fn from(value: Fraction) -> Self {
        value.0
    }
}

impl From<Ratio<BigUint>> for Fraction {
    fn from(value: Ratio<BigUint>) -> Self {
        Self(value)
    }
}

impl std::ops::Mul for Fraction {
    type Output = Fraction;

    fn mul(self, rhs: Self) -> Self::Output {
        let self_ratio: Ratio<BigUint> = self.into();
        let rhs_ratio: Ratio<BigUint> = rhs.into();

        (self_ratio * rhs_ratio).into()
    }
}

impl From<Rational64> for Fraction {
    fn from(value: Rational64) -> Self {
        let numerator = value.numer().unsigned_abs();
        let denominator = value.denom().unsigned_abs();
        // The given rational is assumed to be validated already
        Fraction::new_raw(BigUint::from(numerator), BigUint::from(denominator))
    }
}

impl From<Fraction> for Decimal256 {
    fn from(fraction: Fraction) -> Self {
        let mut n_bytes = fraction.numerator().to_le_bytes();
        n_bytes.resize(32, 0);
        let numerator = Uint256::from_le_bytes(n_bytes.try_into().unwrap());
        let mut d_bytes = fraction.denominator().to_le_bytes();
        d_bytes.resize(32, 0);
        let denominator = Uint256::from_le_bytes(d_bytes.try_into().unwrap());
        Decimal256::from_ratio(numerator, denominator)
    }
}

impl TryFrom<Fraction> for f64 {
    type Error = FractionError;

    fn try_from(value: Fraction) -> Result<Self, Self::Error> {
        let numerator = value
            .numerator()
            .to_f64()
            .ok_or(FractionError::NumeratorOverflow)?;
        let denominator = value
            .denominator()
            .to_f64()
            .ok_or(FractionError::DenominatorOverflow)?;
        if denominator == 0.0 {
            return Err(FractionError::ZeroDenominator);
        }
        Ok(numerator / denominator)
    }
}

impl From<Decimal256> for Fraction {
    fn from(value: Decimal256) -> Self {
        let numerator = BigUint::from_le_bytes(&value.atomics().to_le_bytes());
        let denominator = BigUint::from(10u8).pow(value.decimal_places());
        Fraction::new_raw(numerator, denominator).reduced()
    }
}

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

    #[test]
    fn test_fraction_to_decimal256() {
        let fraction = Fraction::new_raw(BigUint::from(100u8), BigUint::from(200u8));
        let decimal: Decimal256 = fraction.into();
        assert_eq!(decimal, Decimal256::percent(50));

        let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
        let decimal: Decimal256 = fraction.into();
        assert_eq!(decimal, Decimal256::percent(75));
    }

    #[test]
    fn test_decimal256_to_fraction() {
        let decimal = Decimal256::percent(50);
        let fraction: Fraction = decimal.into();
        assert_eq!(*fraction.numerator(), BigUint::from(1u8));
        assert_eq!(*fraction.denominator(), BigUint::from(2u8));

        let decimal = Decimal256::percent(75);
        let fraction: Fraction = decimal.into();
        assert_eq!(*fraction.numerator(), BigUint::from(3u8));
        assert_eq!(*fraction.denominator(), BigUint::from(4u8));
    }

    #[test]
    fn test_rational64_to_fraction() {
        let rational = Rational64::new(3, 4);
        let fraction: Fraction = rational.into();
        assert_eq!(*fraction.numerator(), BigUint::from(3u8));
        assert_eq!(*fraction.denominator(), BigUint::from(4u8));

        let rational = Rational64::new(-5, 10);
        let fraction: Fraction = rational.into();
        assert_eq!(*fraction.numerator(), BigUint::from(1u8));
        assert_eq!(*fraction.denominator(), BigUint::from(2u8));
    }

    #[test]
    fn test_fraction_to_f64() {
        let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
        let result: f64 = fraction
            .try_into()
            .expect("Failed to convert Fraction to f64");
        assert_eq!(result, 0.75);

        let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(3u8));
        let result: f64 = fraction
            .try_into()
            .expect("Failed to convert Fraction to f64");
        assert_eq!(result, 0.3333333333333333);

        let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(0u8));
        let result: Result<f64, FractionError> = fraction.try_into();
        assert!(result.is_err());
        if let Err(e) = result {
            assert_eq!(e, FractionError::ZeroDenominator);
        }
    }

    #[test]
    fn test_reduce() {
        let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(10u64)).reduced();

        assert_eq!(
            price,
            Fraction::new_raw(BigUint::from(10u64), BigUint::from(1u64))
        )
    }

    #[test]
    fn test_precision_changing() {
        let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(1u64));

        let base_precision = 2;
        let quote_precision = 4;

        assert_eq!(
            price.to_human_precision(base_precision, quote_precision),
            Fraction::new_raw(BigUint::from(1u64), BigUint::from(1u64))
        );
    }
}