Skip to main content

precision_core/
oracle.rs

1//! Oracle decimal conversion utilities.
2//!
3//! Different oracle providers use different decimal precisions:
4//! - Chainlink: 8 decimals for most feeds, 18 for some
5//! - Pyth: Variable precision (exponent-based)
6//! - RedStone: 8 decimals
7//! - Band Protocol: 18 decimals
8//!
9//! This module provides utilities for normalizing and converting between
10//! different oracle decimal formats.
11
12use crate::{ArithmeticError, Decimal, RoundingMode};
13
14/// Standard oracle decimal formats.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum OracleDecimals {
17    /// USDC, USDT on many chains (6 decimals)
18    Six,
19    /// Chainlink default (8 decimals)
20    Eight,
21    /// ETH, most ERC-20 tokens (18 decimals)
22    Eighteen,
23    /// Custom decimal count
24    Custom(u8),
25}
26
27impl OracleDecimals {
28    /// Get the decimal count.
29    pub const fn value(self) -> u8 {
30        match self {
31            Self::Six => 6,
32            Self::Eight => 8,
33            Self::Eighteen => 18,
34            Self::Custom(n) => n,
35        }
36    }
37
38    /// Get the scale factor (10^decimals).
39    pub fn scale_factor(self) -> Decimal {
40        let decimals = self.value();
41        Decimal::from(10i64)
42            .powi(decimals as i32)
43            .unwrap_or(Decimal::MAX)
44    }
45}
46
47impl From<u8> for OracleDecimals {
48    fn from(n: u8) -> Self {
49        match n {
50            6 => Self::Six,
51            8 => Self::Eight,
52            18 => Self::Eighteen,
53            _ => Self::Custom(n),
54        }
55    }
56}
57
58/// Normalize a raw oracle value to a Decimal.
59///
60/// Takes a raw integer value from an oracle and converts it to a Decimal
61/// using the specified decimal precision.
62///
63/// # Example
64///
65/// ```
66/// use precision_core::oracle::{normalize_oracle_price, OracleDecimals};
67///
68/// // Chainlink ETH/USD price: $2500.12345678 (8 decimals)
69/// let raw_price = 250012345678i64;
70/// let price = normalize_oracle_price(raw_price, OracleDecimals::Eight).unwrap();
71/// assert_eq!(price.to_string(), "2500.12345678");
72/// ```
73pub fn normalize_oracle_price(
74    raw_value: i64,
75    decimals: OracleDecimals,
76) -> Result<Decimal, ArithmeticError> {
77    let scale = decimals.scale_factor();
78    Decimal::from(raw_value)
79        .checked_div(scale)
80        .ok_or(ArithmeticError::DivisionByZero)
81}
82
83/// Normalize a large raw oracle value to a Decimal.
84///
85/// Similar to [`normalize_oracle_price`] but accepts i128 for values
86/// that exceed i64 range (common with 18-decimal token amounts).
87pub fn normalize_oracle_price_i128(
88    raw_value: i128,
89    decimals: OracleDecimals,
90) -> Result<Decimal, ArithmeticError> {
91    let scale = decimals.scale_factor();
92    Decimal::try_from_i128(raw_value)?
93        .checked_div(scale)
94        .ok_or(ArithmeticError::DivisionByZero)
95}
96
97/// Convert a Decimal to a raw oracle integer value.
98///
99/// Converts a Decimal to the raw integer format expected by an oracle
100/// with the specified decimal precision.
101///
102/// # Example
103///
104/// ```
105/// use precision_core::oracle::{denormalize_oracle_price, OracleDecimals};
106/// use precision_core::Decimal;
107/// use core::str::FromStr;
108///
109/// let price = Decimal::from_str("2500.12345678").unwrap();
110/// let raw = denormalize_oracle_price(price, OracleDecimals::Eight).unwrap();
111/// assert_eq!(raw, 250012345678);
112/// ```
113pub fn denormalize_oracle_price(
114    value: Decimal,
115    decimals: OracleDecimals,
116) -> Result<i64, ArithmeticError> {
117    let scale = decimals.scale_factor();
118    let scaled = value
119        .checked_mul(scale)
120        .ok_or(ArithmeticError::Overflow)?
121        .round(0, RoundingMode::TowardZero);
122    let (mantissa, _) = scaled.to_parts();
123    i64::try_from(mantissa).map_err(|_| ArithmeticError::Overflow)
124}
125
126/// Convert a Decimal to a raw oracle i128 value.
127///
128/// Similar to [`denormalize_oracle_price`] but returns i128 for values
129/// that exceed i64 range.
130pub fn denormalize_oracle_price_i128(
131    value: Decimal,
132    decimals: OracleDecimals,
133) -> Result<i128, ArithmeticError> {
134    let scale = decimals.scale_factor();
135    let scaled = value
136        .checked_mul(scale)
137        .ok_or(ArithmeticError::Overflow)?
138        .round(0, RoundingMode::TowardZero);
139    let (mantissa, _) = scaled.to_parts();
140    Ok(mantissa)
141}
142
143/// Convert a price between two different decimal precisions.
144///
145/// # Example
146///
147/// ```
148/// use precision_core::oracle::{convert_decimals, OracleDecimals};
149///
150/// // Convert from 8 decimals (Chainlink) to 6 decimals (USDC)
151/// let chainlink_price = 250012345678i64;  // $2500.12345678
152/// let usdc_price = convert_decimals(
153///     chainlink_price,
154///     OracleDecimals::Eight,
155///     OracleDecimals::Six
156/// ).unwrap();
157/// assert_eq!(usdc_price, 2500123456);  // $2500.123456
158/// ```
159pub fn convert_decimals(
160    value: i64,
161    from: OracleDecimals,
162    to: OracleDecimals,
163) -> Result<i64, ArithmeticError> {
164    let from_decimals = from.value() as i32;
165    let to_decimals = to.value() as i32;
166    let diff = to_decimals - from_decimals;
167
168    if diff == 0 {
169        return Ok(value);
170    }
171
172    let factor = 10i64
173        .checked_pow(diff.unsigned_abs())
174        .ok_or(ArithmeticError::Overflow)?;
175
176    if diff > 0 {
177        value.checked_mul(factor).ok_or(ArithmeticError::Overflow)
178    } else {
179        Ok(value / factor)
180    }
181}
182
183/// Convert a price between decimal precisions, returning i128.
184///
185/// Use this when converting to higher decimals where the result may exceed i64.
186///
187/// # Example
188///
189/// ```
190/// use precision_core::oracle::{convert_decimals_i128, OracleDecimals};
191///
192/// // Convert from 8 decimals (Chainlink) to 18 decimals (on-chain)
193/// let chainlink_price = 250012345678i64;  // $2500.12345678
194/// let onchain_price = convert_decimals_i128(
195///     chainlink_price,
196///     OracleDecimals::Eight,
197///     OracleDecimals::Eighteen
198/// ).unwrap();
199/// assert_eq!(onchain_price, 2500123456780000000000i128);
200/// ```
201pub fn convert_decimals_i128(
202    value: i64,
203    from: OracleDecimals,
204    to: OracleDecimals,
205) -> Result<i128, ArithmeticError> {
206    let from_decimals = from.value() as i32;
207    let to_decimals = to.value() as i32;
208    let diff = to_decimals - from_decimals;
209
210    if diff == 0 {
211        return Ok(value as i128);
212    }
213
214    let factor = 10i128
215        .checked_pow(diff.unsigned_abs())
216        .ok_or(ArithmeticError::Overflow)?;
217
218    if diff > 0 {
219        (value as i128)
220            .checked_mul(factor)
221            .ok_or(ArithmeticError::Overflow)
222    } else {
223        Ok((value as i128) / factor)
224    }
225}
226
227/// Scale a token amount between different decimal precisions.
228///
229/// Useful for converting between tokens with different decimals
230/// (e.g., USDC with 6 decimals to DAI with 18 decimals).
231///
232/// # Example
233///
234/// ```
235/// use precision_core::oracle::{scale_token_amount, OracleDecimals};
236///
237/// // Convert 1000 USDC (6 decimals) representation to 8 decimals
238/// let usdc_amount = 1_000_000_000i64;  // 1000 USDC (6 decimals)
239/// let scaled = scale_token_amount(
240///     usdc_amount,
241///     OracleDecimals::Six,
242///     OracleDecimals::Eight
243/// ).unwrap();
244/// assert_eq!(scaled, 100_000_000_000);  // 1000 * 10^8
245/// ```
246pub fn scale_token_amount(
247    amount: i64,
248    from_decimals: OracleDecimals,
249    to_decimals: OracleDecimals,
250) -> Result<i64, ArithmeticError> {
251    convert_decimals(amount, from_decimals, to_decimals)
252}
253
254/// Scale a token amount using i128 for large values.
255///
256/// # Example
257///
258/// ```
259/// use precision_core::oracle::{scale_token_amount_i128, OracleDecimals};
260///
261/// // Convert 1000 USDC (6 decimals) to 18 decimal representation
262/// let usdc_amount = 1_000_000_000i64;  // 1000 USDC
263/// let scaled = scale_token_amount_i128(
264///     usdc_amount,
265///     OracleDecimals::Six,
266///     OracleDecimals::Eighteen
267/// ).unwrap();
268/// assert_eq!(scaled, 1_000_000_000_000_000_000_000i128);  // 1000 * 10^18
269/// ```
270pub fn scale_token_amount_i128(
271    amount: i64,
272    from_decimals: OracleDecimals,
273    to_decimals: OracleDecimals,
274) -> Result<i128, ArithmeticError> {
275    convert_decimals_i128(amount, from_decimals, to_decimals)
276}
277
278/// Calculate the value of tokens in a quote currency.
279///
280/// Computes: amount * price, handling decimal conversions.
281/// Uses Decimal internally for precision, returns result in specified decimals.
282///
283/// # Arguments
284///
285/// * `amount` - Token amount in its native decimals
286/// * `amount_decimals` - Decimal precision of the token
287/// * `price` - Price per token in quote currency
288/// * `price_decimals` - Decimal precision of the price feed
289/// * `result_decimals` - Desired decimal precision for the result
290///
291/// # Example
292///
293/// ```
294/// use precision_core::oracle::{calculate_value, OracleDecimals};
295///
296/// // Calculate value of 1000 USDC at $1.00 per USDC
297/// let usdc_amount = 1_000_000_000i64;  // 1000 USDC (6 decimals)
298/// let usdc_price = 100000000i64;  // $1.00 (8 decimals from Chainlink)
299///
300/// let value = calculate_value(
301///     usdc_amount,
302///     OracleDecimals::Six,
303///     usdc_price,
304///     OracleDecimals::Eight,
305///     OracleDecimals::Six  // Result in 6 decimals
306/// ).unwrap();
307///
308/// assert_eq!(value, 1_000_000_000);  // $1000 in 6 decimals
309/// ```
310pub fn calculate_value(
311    amount: i64,
312    amount_decimals: OracleDecimals,
313    price: i64,
314    price_decimals: OracleDecimals,
315    result_decimals: OracleDecimals,
316) -> Result<i64, ArithmeticError> {
317    let amount_dec = normalize_oracle_price(amount, amount_decimals)?;
318    let price_dec = normalize_oracle_price(price, price_decimals)?;
319
320    let value = amount_dec
321        .checked_mul(price_dec)
322        .ok_or(ArithmeticError::Overflow)?;
323
324    denormalize_oracle_price(value, result_decimals)
325}
326
327/// Calculate value and return as i128 for large results.
328pub fn calculate_value_i128(
329    amount: i64,
330    amount_decimals: OracleDecimals,
331    price: i64,
332    price_decimals: OracleDecimals,
333    result_decimals: OracleDecimals,
334) -> Result<i128, ArithmeticError> {
335    let amount_dec = normalize_oracle_price(amount, amount_decimals)?;
336    let price_dec = normalize_oracle_price(price, price_decimals)?;
337
338    let value = amount_dec
339        .checked_mul(price_dec)
340        .ok_or(ArithmeticError::Overflow)?;
341
342    denormalize_oracle_price_i128(value, result_decimals)
343}
344
345/// Normalize a Pyth-style price with exponent.
346///
347/// Pyth prices are returned as (price, exponent) where the actual price
348/// is price * 10^exponent.
349///
350/// # Example
351///
352/// ```
353/// use precision_core::oracle::normalize_pyth_price;
354///
355/// // Pyth ETH/USD price: 250012345678 with exponent -8
356/// let price = 250012345678i64;
357/// let exponent = -8i32;
358/// let normalized = normalize_pyth_price(price, exponent).unwrap();
359/// assert_eq!(normalized.to_string(), "2500.12345678");
360/// ```
361pub fn normalize_pyth_price(price: i64, exponent: i32) -> Result<Decimal, ArithmeticError> {
362    let price_dec = Decimal::from(price);
363
364    if exponent == 0 {
365        return Ok(price_dec);
366    }
367
368    let scale = Decimal::from(10i64)
369        .powi(exponent.abs())
370        .ok_or(ArithmeticError::Overflow)?;
371
372    if exponent > 0 {
373        price_dec
374            .checked_mul(scale)
375            .ok_or(ArithmeticError::Overflow)
376    } else {
377        price_dec
378            .checked_div(scale)
379            .ok_or(ArithmeticError::DivisionByZero)
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    extern crate alloc;
386
387    use super::*;
388    use alloc::string::ToString;
389    use core::str::FromStr;
390
391    #[test]
392    fn test_normalize_chainlink_price() {
393        let raw = 250012345678i64;
394        let price = normalize_oracle_price(raw, OracleDecimals::Eight).unwrap();
395        assert_eq!(price.to_string(), "2500.12345678");
396    }
397
398    #[test]
399    fn test_denormalize_price() {
400        let price = Decimal::from_str("2500.12345678").unwrap();
401        let raw = denormalize_oracle_price(price, OracleDecimals::Eight).unwrap();
402        assert_eq!(raw, 250012345678);
403    }
404
405    #[test]
406    fn test_convert_8_to_6_decimals() {
407        let chainlink = 250012345678i64;
408        let usdc = convert_decimals(chainlink, OracleDecimals::Eight, OracleDecimals::Six).unwrap();
409        assert_eq!(usdc, 2500123456);
410    }
411
412    #[test]
413    fn test_convert_8_to_18_decimals_i128() {
414        let chainlink = 250012345678i64;
415        let onchain =
416            convert_decimals_i128(chainlink, OracleDecimals::Eight, OracleDecimals::Eighteen)
417                .unwrap();
418        assert_eq!(onchain, 2500123456780000000000i128);
419    }
420
421    #[test]
422    fn test_convert_18_to_8_decimals_via_normalize() {
423        // Test round-trip: normalize a Chainlink price, then convert back
424        let original = 250012345678i64;
425        let normalized = normalize_oracle_price(original, OracleDecimals::Eight).unwrap();
426        let recovered = denormalize_oracle_price(normalized, OracleDecimals::Eight).unwrap();
427        assert_eq!(recovered, original);
428    }
429
430    #[test]
431    fn test_scale_usdc_to_8_decimals() {
432        let usdc = 1_000_000_000i64; // 1000 USDC (6 decimals)
433        let scaled = scale_token_amount(usdc, OracleDecimals::Six, OracleDecimals::Eight).unwrap();
434        assert_eq!(scaled, 100_000_000_000);
435    }
436
437    #[test]
438    fn test_scale_usdc_to_18_decimals_i128() {
439        let usdc = 1_000_000_000i64; // 1000 USDC
440        let scaled =
441            scale_token_amount_i128(usdc, OracleDecimals::Six, OracleDecimals::Eighteen).unwrap();
442        assert_eq!(scaled, 1_000_000_000_000_000_000_000i128);
443    }
444
445    #[test]
446    fn test_pyth_positive_exponent() {
447        let price = normalize_pyth_price(25, 2).unwrap();
448        assert_eq!(price.to_string(), "2500");
449    }
450
451    #[test]
452    fn test_pyth_negative_exponent() {
453        let price = normalize_pyth_price(250012345678, -8).unwrap();
454        assert_eq!(price.to_string(), "2500.12345678");
455    }
456
457    #[test]
458    fn test_pyth_zero_exponent() {
459        let price = normalize_pyth_price(2500, 0).unwrap();
460        assert_eq!(price.to_string(), "2500");
461    }
462
463    #[test]
464    fn test_calculate_usdc_value() {
465        // Calculate value of 1000 USDC at $1.00 per USDC
466        let usdc_amount = 1_000_000_000i64; // 1000 USDC (6 decimals)
467        let usdc_price = 100000000i64; // $1.00 (8 decimals from Chainlink)
468
469        let value = calculate_value(
470            usdc_amount,
471            OracleDecimals::Six,
472            usdc_price,
473            OracleDecimals::Eight,
474            OracleDecimals::Six,
475        )
476        .unwrap();
477
478        assert_eq!(value, 1_000_000_000); // $1000 in 6 decimals
479    }
480
481    #[test]
482    fn test_calculate_btc_value() {
483        // Calculate value of 0.1 BTC at $50000 per BTC
484        // Using 8 decimal representation for BTC amount
485        let btc_amount = 10_000_000i64; // 0.1 BTC (8 decimals)
486        let btc_price = 5000000000000i64; // $50000 (8 decimals)
487
488        let value = calculate_value(
489            btc_amount,
490            OracleDecimals::Eight,
491            btc_price,
492            OracleDecimals::Eight,
493            OracleDecimals::Six,
494        )
495        .unwrap();
496
497        assert_eq!(value, 5_000_000_000); // $5000 in 6 decimals
498    }
499
500    #[test]
501    fn test_oracle_decimals_from_u8() {
502        assert_eq!(OracleDecimals::from(6), OracleDecimals::Six);
503        assert_eq!(OracleDecimals::from(8), OracleDecimals::Eight);
504        assert_eq!(OracleDecimals::from(18), OracleDecimals::Eighteen);
505        assert_eq!(OracleDecimals::from(12), OracleDecimals::Custom(12));
506    }
507}