Skip to main content

nautilus_blockchain/
decode.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
16use 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
23/// Convert a `U256` amount to [`Quantity`].
24///
25/// - If `decimals == 18`, the value represents wei and uses the dedicated lossless
26///   `Quantity::from_wei` constructor.
27/// - Other precisions use checked integer scaling and clamp `decimals` to
28///   [`FIXED_PRECISION`]. Discarded source digits are rounded half to even.
29///
30/// # Errors
31///
32/// Returns an error if scaling overflows or the result exceeds [`QUANTITY_RAW_MAX`].
33pub 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
58/// Convert a `U256` amount to [`Price`].
59///
60/// - If `decimals == 18`, the value represents wei and uses the dedicated lossless
61///   `Price::from_wei` constructor.
62/// - Other precisions use checked integer scaling and clamp `decimals` to
63///   [`FIXED_PRECISION`]. Discarded source digits are rounded half to even.
64///
65/// # Errors
66///
67/// Returns an error if scaling overflows or the result exceeds [`PRICE_RAW_MAX`].
68pub 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        // The divisor exceeds U256::MAX, so every U256 amount is below half a retained unit
136        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}