nautilus_polymarket/positions/
amounts.rs1use 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
28pub 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
62pub 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}