1use alloy::primitives::U256;
17use nautilus_model::types::{
18 fixed::FIXED_PRECISION,
19 price::{PRICE_RAW_MAX, Price, PriceRaw},
20 quantity::{QUANTITY_RAW_MAX, Quantity, QuantityRaw},
21};
22
23pub fn u256_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
34 if decimals == 18 {
35 check_raw_range(
36 amount,
37 U256::from(QUANTITY_RAW_MAX),
38 "Quantity",
39 "QUANTITY_RAW_MAX",
40 )?;
41 return Ok(Quantity::from_wei(amount));
42 }
43
44 let precision = decimals.min(FIXED_PRECISION);
45 let raw = scale_u256_to_raw(
46 amount,
47 decimals,
48 FIXED_PRECISION,
49 U256::from(QUANTITY_RAW_MAX),
50 "Quantity",
51 "QUANTITY_RAW_MAX",
52 )?;
53 let raw = QuantityRaw::try_from(raw)
54 .map_err(|e| anyhow::anyhow!("Failed to convert Quantity raw value: {e}"))?;
55 Ok(Quantity::from_raw_checked(raw, precision)?)
56}
57
58pub fn u256_to_price(amount: U256, decimals: u8) -> anyhow::Result<Price> {
69 if decimals == 18 {
70 check_raw_range(amount, U256::from(PRICE_RAW_MAX), "Price", "PRICE_RAW_MAX")?;
71 return Ok(Price::from_wei(amount));
72 }
73
74 let precision = decimals.min(FIXED_PRECISION);
75 let raw = scale_u256_to_raw(
76 amount,
77 decimals,
78 FIXED_PRECISION,
79 U256::from(PRICE_RAW_MAX),
80 "Price",
81 "PRICE_RAW_MAX",
82 )?;
83 let raw = PriceRaw::try_from(raw)
84 .map_err(|e| anyhow::anyhow!("Failed to convert Price raw value: {e}"))?;
85 Ok(Price::from_raw_checked(raw, precision)?)
86}
87
88fn scale_u256_to_raw(
89 amount: U256,
90 decimals: u8,
91 fixed_precision: u8,
92 raw_max: U256,
93 type_name: &str,
94 raw_max_name: &str,
95) -> anyhow::Result<U256> {
96 let raw = if decimals < fixed_precision {
97 let scale = U256::from(10)
98 .checked_pow(U256::from(fixed_precision - decimals))
99 .ok_or_else(|| {
100 anyhow::anyhow!(
101 "Scale 10^{} exceeds U256 while converting {type_name}",
102 fixed_precision - decimals
103 )
104 })?;
105 amount.checked_mul(scale).ok_or_else(|| {
106 anyhow::anyhow!(
107 "{type_name} amount {amount} overflows U256 while scaling from {decimals} to {fixed_precision} decimals"
108 )
109 })?
110 } else if decimals > fixed_precision {
111 round_u256_half_even(amount, decimals - fixed_precision, type_name)?
112 } else {
113 amount
114 };
115
116 check_raw_range(raw, raw_max, type_name, raw_max_name)?;
117 Ok(raw)
118}
119
120fn check_raw_range(
121 raw: U256,
122 raw_max: U256,
123 type_name: &str,
124 raw_max_name: &str,
125) -> anyhow::Result<()> {
126 if raw > raw_max {
127 anyhow::bail!("{type_name} raw value {raw} exceeds {raw_max_name}={raw_max}");
128 }
129
130 Ok(())
131}
132
133fn round_u256_half_even(amount: U256, excess: u8, type_name: &str) -> anyhow::Result<U256> {
134 let Some(divisor) = U256::from(10).checked_pow(U256::from(excess)) else {
135 return Ok(U256::ZERO);
137 };
138 let quotient = amount / divisor;
139 let remainder = amount % divisor;
140 let half = divisor / U256::from(2);
141
142 if remainder > half || (remainder == half && quotient.bit(0)) {
143 quotient.checked_add(U256::from(1)).ok_or_else(|| {
144 anyhow::anyhow!("{type_name} raw value overflows U256 while rounding half to even")
145 })
146 } else {
147 Ok(quotient)
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use alloy::primitives::U256;
154 use rstest::rstest;
155
156 use super::*;
157
158 #[rstest]
159 #[case::zero(U256::ZERO, 6, 0, 6)]
160 #[case::one(U256::from(1), 6, 10_000_000_000, 6)]
161 #[case::above_f64_integer_limit(
162 U256::from(9_007_199_254_740_993_u64),
163 6,
164 90_071_992_547_409_930_000_000_000,
165 6
166 )]
167 #[case::precision_zero(U256::from(37), 0, 370_000_000_000_000_000, 0)]
168 #[case::half_even_down(U256::from(25), 17, 2, FIXED_PRECISION)]
169 #[case::half_even_up(U256::from(35), 17, 4, FIXED_PRECISION)]
170 #[case::below_retained_unit(U256::MAX, u8::MAX, 0, FIXED_PRECISION)]
171 fn test_u256_conversions_store_exact_raw_values(
172 #[case] amount: U256,
173 #[case] decimals: u8,
174 #[case] expected_raw: u128,
175 #[case] expected_precision: u8,
176 ) {
177 let quantity = u256_to_quantity(amount, decimals).unwrap();
178 let price = u256_to_price(amount, decimals).unwrap();
179
180 assert_eq!(quantity.raw, expected_raw);
181 assert_eq!(quantity.precision, expected_precision);
182 assert_eq!(price.raw, expected_raw.cast_signed());
183 assert_eq!(price.precision, expected_precision);
184 }
185
186 #[rstest]
187 fn test_u256_conversions_preserve_wei_raw_values() {
188 let amount = U256::from(9_007_199_254_740_993_u64);
189
190 let quantity = u256_to_quantity(amount, 18).unwrap();
191 let price = u256_to_price(amount, 18).unwrap();
192
193 assert_eq!(quantity.raw, 9_007_199_254_740_993);
194 assert_eq!(quantity.precision, 18);
195 assert_eq!(price.raw, 9_007_199_254_740_993);
196 assert_eq!(price.precision, 18);
197 }
198
199 #[rstest]
200 fn test_u256_conversions_accept_domain_raw_maximums() {
201 let quantity = u256_to_quantity(U256::from(QUANTITY_RAW_MAX), FIXED_PRECISION).unwrap();
202 let price = u256_to_price(U256::from(PRICE_RAW_MAX), FIXED_PRECISION).unwrap();
203
204 assert_eq!(quantity.raw, QUANTITY_RAW_MAX);
205 assert_eq!(quantity.precision, FIXED_PRECISION);
206 assert_eq!(price.raw, PRICE_RAW_MAX);
207 assert_eq!(price.precision, FIXED_PRECISION);
208 }
209
210 #[rstest]
211 fn test_u256_conversions_reject_domain_raw_overflow() {
212 let quantity_raw = U256::from(QUANTITY_RAW_MAX) + U256::from(1);
213 let price_raw = U256::from(PRICE_RAW_MAX) + U256::from(1);
214
215 let quantity_error = u256_to_quantity(quantity_raw, FIXED_PRECISION).unwrap_err();
216 let price_error = u256_to_price(price_raw, FIXED_PRECISION).unwrap_err();
217
218 assert_eq!(
219 quantity_error.to_string(),
220 format!(
221 "Quantity raw value {quantity_raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
222 )
223 );
224 assert_eq!(
225 price_error.to_string(),
226 format!("Price raw value {price_raw} exceeds PRICE_RAW_MAX={PRICE_RAW_MAX}")
227 );
228 }
229
230 #[rstest]
231 fn test_u256_conversions_reject_scaling_overflow() {
232 let quantity_error = u256_to_quantity(U256::MAX, 0).unwrap_err();
233 let price_error = u256_to_price(U256::MAX, 0).unwrap_err();
234
235 assert_eq!(
236 quantity_error.to_string(),
237 format!(
238 "Quantity amount {} overflows U256 while scaling from 0 to {FIXED_PRECISION} decimals",
239 U256::MAX
240 )
241 );
242 assert_eq!(
243 price_error.to_string(),
244 format!(
245 "Price amount {} overflows U256 while scaling from 0 to {FIXED_PRECISION} decimals",
246 U256::MAX
247 )
248 );
249 }
250
251 #[rstest]
252 fn test_u256_conversions_reject_wei_overflow() {
253 let quantity_raw = U256::from(QUANTITY_RAW_MAX) + U256::from(1);
254 let price_raw = U256::from(PRICE_RAW_MAX) + U256::from(1);
255
256 let quantity_error = u256_to_quantity(quantity_raw, 18).unwrap_err();
257 let price_error = u256_to_price(price_raw, 18).unwrap_err();
258
259 assert_eq!(
260 quantity_error.to_string(),
261 format!(
262 "Quantity raw value {quantity_raw} exceeds QUANTITY_RAW_MAX={QUANTITY_RAW_MAX}"
263 )
264 );
265 assert_eq!(
266 price_error.to_string(),
267 format!("Price raw value {price_raw} exceeds PRICE_RAW_MAX={PRICE_RAW_MAX}")
268 );
269 }
270
271 #[rstest]
272 #[case::standard(9, 9_007_199_254_740_993_000_u128)]
273 #[case::high(16, 90_071_992_547_409_930_000_000_000_u128)]
274 fn test_scaling_at_fixed_precision(#[case] fixed_precision: u8, #[case] expected_raw: u128) {
275 let amount = U256::from(9_007_199_254_740_993_u64);
276
277 let raw =
278 scale_u256_to_raw(amount, 6, fixed_precision, U256::MAX, "Domain", "RAW_MAX").unwrap();
279
280 assert_eq!(raw, U256::from(expected_raw));
281 }
282}