Skip to main content

bolt_cw_sdk/
fraction.rs

1use cosmwasm_std::{Decimal256, Uint256};
2use num_bigint::BigUint;
3use num_rational::{Ratio, Rational64};
4use num_traits::{FromBytes, ToBytes, ToPrimitive, Zero};
5use std::fmt::{Debug, Display};
6use std::str::FromStr;
7use thiserror::Error as thiserrorError;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Fraction(Ratio<BigUint>);
11
12impl Fraction {
13    pub fn new(numerator: BigUint, denominator: BigUint) -> Result<Self, FractionError> {
14        if denominator.is_zero() {
15            return Err(FractionError::ZeroDenominator);
16        }
17
18        Ok(Self::new_raw(numerator, denominator))
19    }
20
21    pub fn new_raw(numerator: BigUint, denominator: BigUint) -> Self {
22        Self(Ratio::new_raw(numerator, denominator))
23    }
24
25    pub fn into_raw(self) -> (BigUint, BigUint) {
26        self.0.into_raw()
27    }
28
29    pub fn from_decimal_string(decimal_str: &str) -> Result<Fraction, FractionError> {
30        let dec = Decimal256::from_str(decimal_str)?;
31        Ok(dec.into())
32    }
33
34    pub fn ratio(&self) -> &Ratio<BigUint> {
35        &self.0
36    }
37
38    pub fn numerator(&self) -> &BigUint {
39        self.ratio().numer()
40    }
41
42    pub fn denominator(&self) -> &BigUint {
43        self.ratio().denom()
44    }
45
46    pub fn reduced(self) -> Self {
47        Self(self.0.reduced())
48    }
49
50    /// Converts blockchain prices to human readable, aarch/uusdt -> ARCH/USDT
51    pub fn to_human_precision(self, base_precision: u8, quote_precision: u8) -> Self {
52        let ratio = Ratio::new(
53            BigUint::from(10u64.pow(base_precision as u32)),
54            BigUint::from(10u64.pow(quote_precision as u32)),
55        )
56        .reduced();
57
58        self * ratio.into()
59    }
60}
61
62#[derive(Debug, thiserrorError, PartialEq)]
63pub enum FractionError {
64    #[error("Cosmwasm Error: {0}")]
65    CosmwasmError(#[from] cosmwasm_std::StdError),
66    #[error("Numerator too large to fit in f64")]
67    NumeratorOverflow,
68    #[error("Denominator too large to fit in f64")]
69    DenominatorOverflow,
70    #[error("Denominator is zero")]
71    ZeroDenominator,
72}
73
74impl Display for Fraction {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        std::fmt::Display::fmt(&self.ratio(), f)
77    }
78}
79
80impl From<Fraction> for Ratio<BigUint> {
81    fn from(value: Fraction) -> Self {
82        value.0
83    }
84}
85
86impl From<Ratio<BigUint>> for Fraction {
87    fn from(value: Ratio<BigUint>) -> Self {
88        Self(value)
89    }
90}
91
92impl std::ops::Mul for Fraction {
93    type Output = Fraction;
94
95    fn mul(self, rhs: Self) -> Self::Output {
96        let self_ratio: Ratio<BigUint> = self.into();
97        let rhs_ratio: Ratio<BigUint> = rhs.into();
98
99        (self_ratio * rhs_ratio).into()
100    }
101}
102
103impl From<Rational64> for Fraction {
104    fn from(value: Rational64) -> Self {
105        let numerator = value.numer().unsigned_abs();
106        let denominator = value.denom().unsigned_abs();
107        // The given rational is assumed to be validated already
108        Fraction::new_raw(BigUint::from(numerator), BigUint::from(denominator))
109    }
110}
111
112impl From<Fraction> for Decimal256 {
113    fn from(fraction: Fraction) -> Self {
114        let mut n_bytes = fraction.numerator().to_le_bytes();
115        n_bytes.resize(32, 0);
116        let numerator = Uint256::from_le_bytes(n_bytes.try_into().unwrap());
117        let mut d_bytes = fraction.denominator().to_le_bytes();
118        d_bytes.resize(32, 0);
119        let denominator = Uint256::from_le_bytes(d_bytes.try_into().unwrap());
120        Decimal256::from_ratio(numerator, denominator)
121    }
122}
123
124impl TryFrom<Fraction> for f64 {
125    type Error = FractionError;
126
127    fn try_from(value: Fraction) -> Result<Self, Self::Error> {
128        let numerator = value
129            .numerator()
130            .to_f64()
131            .ok_or(FractionError::NumeratorOverflow)?;
132        let denominator = value
133            .denominator()
134            .to_f64()
135            .ok_or(FractionError::DenominatorOverflow)?;
136        if denominator == 0.0 {
137            return Err(FractionError::ZeroDenominator);
138        }
139        Ok(numerator / denominator)
140    }
141}
142
143impl From<Decimal256> for Fraction {
144    fn from(value: Decimal256) -> Self {
145        let numerator = BigUint::from_le_bytes(&value.atomics().to_le_bytes());
146        let denominator = BigUint::from(10u8).pow(value.decimal_places());
147        Fraction::new_raw(numerator, denominator).reduced()
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_fraction_to_decimal256() {
157        let fraction = Fraction::new_raw(BigUint::from(100u8), BigUint::from(200u8));
158        let decimal: Decimal256 = fraction.into();
159        assert_eq!(decimal, Decimal256::percent(50));
160
161        let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
162        let decimal: Decimal256 = fraction.into();
163        assert_eq!(decimal, Decimal256::percent(75));
164    }
165
166    #[test]
167    fn test_decimal256_to_fraction() {
168        let decimal = Decimal256::percent(50);
169        let fraction: Fraction = decimal.into();
170        assert_eq!(*fraction.numerator(), BigUint::from(1u8));
171        assert_eq!(*fraction.denominator(), BigUint::from(2u8));
172
173        let decimal = Decimal256::percent(75);
174        let fraction: Fraction = decimal.into();
175        assert_eq!(*fraction.numerator(), BigUint::from(3u8));
176        assert_eq!(*fraction.denominator(), BigUint::from(4u8));
177    }
178
179    #[test]
180    fn test_rational64_to_fraction() {
181        let rational = Rational64::new(3, 4);
182        let fraction: Fraction = rational.into();
183        assert_eq!(*fraction.numerator(), BigUint::from(3u8));
184        assert_eq!(*fraction.denominator(), BigUint::from(4u8));
185
186        let rational = Rational64::new(-5, 10);
187        let fraction: Fraction = rational.into();
188        assert_eq!(*fraction.numerator(), BigUint::from(1u8));
189        assert_eq!(*fraction.denominator(), BigUint::from(2u8));
190    }
191
192    #[test]
193    fn test_fraction_to_f64() {
194        let fraction = Fraction::new_raw(BigUint::from(3u8), BigUint::from(4u8));
195        let result: f64 = fraction
196            .try_into()
197            .expect("Failed to convert Fraction to f64");
198        assert_eq!(result, 0.75);
199
200        let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(3u8));
201        let result: f64 = fraction
202            .try_into()
203            .expect("Failed to convert Fraction to f64");
204        assert_eq!(result, 0.3333333333333333);
205
206        let fraction = Fraction::new_raw(BigUint::from(1u8), BigUint::from(0u8));
207        let result: Result<f64, FractionError> = fraction.try_into();
208        assert!(result.is_err());
209        if let Err(e) = result {
210            assert_eq!(e, FractionError::ZeroDenominator);
211        }
212    }
213
214    #[test]
215    fn test_reduce() {
216        let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(10u64)).reduced();
217
218        assert_eq!(
219            price,
220            Fraction::new_raw(BigUint::from(10u64), BigUint::from(1u64))
221        )
222    }
223
224    #[test]
225    fn test_precision_changing() {
226        let price = Fraction::new_raw(BigUint::from(100u64), BigUint::from(1u64));
227
228        let base_precision = 2;
229        let quote_precision = 4;
230
231        assert_eq!(
232            price.to_human_precision(base_precision, quote_precision),
233            Fraction::new_raw(BigUint::from(1u64), BigUint::from(1u64))
234        );
235    }
236}