Skip to main content

nautilus_polymarket/positions/
amounts.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Exact pUSD amount conversion for Conditional Token operations.
17
18use alloy_primitives::U256;
19use rust_decimal::Decimal;
20
21use crate::{
22    common::consts::USDC_DECIMALS,
23    http::error::{Error, Result},
24};
25
26const PUSD_SCALE: u32 = USDC_DECIMALS;
27
28/// Converts an exact pUSD amount into six-decimal base units.
29///
30/// # Errors
31///
32/// Returns an error if `amount` is not positive or is not exactly representable
33/// at six decimal places.
34pub fn pusd_to_base_units(amount: Decimal) -> Result<U256> {
35    if amount.is_sign_negative() || amount.is_zero() {
36        return Err(Error::bad_request(format!(
37            "pUSD amount must be positive, was {amount}"
38        )));
39    }
40
41    let scale = Decimal::from(10u32.pow(PUSD_SCALE));
42
43    let scaled = amount.checked_mul(scale).ok_or_else(|| {
44        Error::bad_request(format!(
45            "pUSD amount overflowed six-decimal conversion, was {amount}"
46        ))
47    })?;
48
49    let normalized = scaled.normalize();
50    if normalized.scale() != 0 {
51        return Err(Error::bad_request(format!(
52            "pUSD amount must be exactly representable at {PUSD_SCALE} decimal places, was {amount}"
53        )));
54    }
55
56    let mantissa = normalized.mantissa();
57    u128::try_from(mantissa)
58        .map(U256::from)
59        .map_err(|_| Error::bad_request(format!("pUSD amount overflowed base units, was {amount}")))
60}
61
62/// Converts six-decimal pUSD base units back to a decimal amount.
63///
64/// # Errors
65///
66/// Returns an error if `base_units` cannot be represented as a decimal at six
67/// decimal places.
68pub fn base_units_to_pusd(base_units: U256) -> Result<Decimal> {
69    let mut mantissa = base_units;
70    let mut scale = PUSD_SCALE;
71    let ten = U256::from(10u8);
72    while scale > 0 && mantissa % ten == U256::ZERO {
73        mantissa /= ten;
74        scale -= 1;
75    }
76
77    let mantissa = i128::try_from(mantissa).map_err(|_| {
78        Error::bad_request(format!(
79            "pUSD base units overflowed decimal conversion, was {base_units}"
80        ))
81    })?;
82
83    Decimal::try_from_i128_with_scale(mantissa, scale).map_err(|_| {
84        Error::bad_request(format!(
85            "pUSD base units overflowed decimal conversion, was {base_units}"
86        ))
87    })
88}
89
90#[cfg(test)]
91mod tests {
92    use rstest::rstest;
93    use rust_decimal_macros::dec;
94
95    use super::*;
96
97    #[rstest]
98    #[case(dec!(1), 1_000_000u64)]
99    #[case(dec!(1.5), 1_500_000u64)]
100    #[case(dec!(0.000001), 1u64)]
101    #[case(dec!(1.000000), 1_000_000u64)]
102    fn test_pusd_to_base_units_exact(#[case] amount: Decimal, #[case] expected: u64) {
103        assert_eq!(pusd_to_base_units(amount).unwrap(), U256::from(expected));
104    }
105
106    #[rstest]
107    #[case(dec!(0))]
108    #[case(dec!(-1))]
109    #[case(dec!(-0.000001))]
110    fn test_pusd_to_base_units_rejects_non_positive(#[case] amount: Decimal) {
111        let err = pusd_to_base_units(amount).unwrap_err();
112        assert!(err.to_string().contains("pUSD amount must be positive"));
113        assert!(err.to_string().contains(&format!("was {amount}")));
114    }
115
116    #[rstest]
117    fn test_pusd_to_base_units_rejects_excess_precision() {
118        let amount = dec!(1.0000001);
119        let err = pusd_to_base_units(amount).unwrap_err();
120        assert!(
121            err.to_string()
122                .contains("exactly representable at 6 decimal places")
123        );
124        assert!(err.to_string().contains("was 1.0000001"));
125    }
126
127    #[rstest]
128    #[case(U256::from(1u128 << 96))]
129    #[case(U256::from(u128::MAX))]
130    #[case(U256::MAX)]
131    fn test_base_units_to_pusd_rejects_overflow(#[case] amount: U256) {
132        assert!(base_units_to_pusd(amount).is_err());
133    }
134
135    #[rstest]
136    fn test_base_units_to_pusd_decimal_boundaries() {
137        let max = U256::from((1u128 << 96) - 1);
138        assert_eq!(
139            base_units_to_pusd(max).unwrap(),
140            dec!(79228162514264337593543.950335)
141        );
142        assert_eq!(
143            base_units_to_pusd(max * U256::from(1_000_000u64)).unwrap(),
144            Decimal::MAX
145        );
146        assert_eq!(base_units_to_pusd(U256::ZERO).unwrap(), Decimal::ZERO);
147    }
148
149    #[rstest]
150    fn test_base_units_to_pusd_round_trip() {
151        let amount = dec!(12.345678);
152        let base_units = pusd_to_base_units(amount).unwrap();
153        assert_eq!(base_units_to_pusd(base_units).unwrap(), amount);
154    }
155}