Skip to main content

nautilus_model/defi/types/
quantity.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//! DeFi-specific extensions for the [`Quantity`] type.
17
18use alloy_primitives::U256;
19
20use crate::types::quantity::Quantity;
21
22impl Quantity {
23    /// Constructs a [`Quantity`] from a raw amount expressed in wei (18-decimal fixed-point).
24    ///
25    /// The resulting [`Quantity`] will always have `precision` equal to `18`.
26    ///
27    /// # Panics
28    ///
29    /// Panics if the supplied `raw_wei` cannot fit into an **unsigned** 128-bit integer (this
30    /// would exceed the numeric range of the internal `QuantityRaw` representation).
31    #[must_use]
32    pub fn from_wei<U>(raw_wei: U) -> Self
33    where
34        U: Into<U256>,
35    {
36        let raw_u256: U256 = raw_wei.into();
37        let raw_u128: u128 = raw_u256
38            .try_into()
39            .expect("raw wei value exceeds unsigned 128-bit range");
40
41        Self::from_raw(raw_u128, 18)
42    }
43
44    /// Converts this [`Quantity`] to a wei amount (U256).
45    ///
46    /// Only valid for prices with precision 18. For other precisions convert to precision 18 first.
47    ///
48    /// # Panics
49    ///
50    /// Panics if the quantity has precision other than 18.
51    #[must_use]
52    pub fn as_wei(&self) -> U256 {
53        assert!(
54            self.precision == 18,
55            "Failed to convert quantity with precision {} to wei (requires precision 18)",
56            self.precision
57        );
58
59        U256::from(self.raw)
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use rstest::rstest;
66    use rust_decimal_macros::dec;
67
68    use super::*;
69
70    #[rstest]
71    fn test_from_wei_basic() {
72        let quantity = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1 token in wei
73        assert_eq!(quantity.precision, 18);
74        assert_eq!(quantity.as_decimal(), dec!(1.0));
75    }
76
77    #[rstest]
78    fn test_as_wei_basic() {
79        let quantity = Quantity::from_raw(1_000_000_000_000_000_000_u128, 18);
80        let wei = quantity.as_wei();
81        assert_eq!(wei, U256::from(1_000_000_000_000_000_000_u128));
82    }
83
84    #[rstest]
85    #[should_panic(
86        expected = "Failed to convert quantity with precision 2 to wei (requires precision 18)"
87    )]
88    fn test_as_wei_wrong_precision() {
89        let quantity = Quantity::new(1.23, 2);
90        let _ = quantity.as_wei();
91    }
92
93    #[rstest]
94    fn test_wei_round_trip() {
95        let original_wei = U256::from(1_500_000_000_000_000_000_u128); // 1.5 tokens
96        let quantity = Quantity::from_wei(original_wei);
97        let converted_wei = quantity.as_wei();
98        assert_eq!(original_wei, converted_wei);
99        assert_eq!(quantity.as_decimal(), dec!(1.5));
100    }
101
102    #[rstest]
103    fn test_checked_arith_accepts_wei_precision() {
104        let a = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128));
105        let b = Quantity::from_wei(U256::from(2_000_000_000_000_000_000_u128));
106        let sum = a
107            .checked_add(b)
108            .expect("checked_add must accept wei quantities");
109        assert_eq!(sum.as_decimal(), dec!(3));
110        let diff = b
111            .checked_sub(a)
112            .expect("checked_sub must accept wei quantities");
113        assert_eq!(diff.as_decimal(), dec!(1));
114    }
115
116    #[rstest]
117    fn test_checked_arith_rejects_mixed_scale() {
118        // Wei (precision 18, raw at 10^18 scale) and standard (precision 0, raw at
119        // FIXED_SCALAR scale) cannot be added with raw arithmetic without rescaling.
120        let wei = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128));
121        let standard = Quantity::new(1.0, 0);
122        assert_eq!(wei.checked_add(standard), None);
123        assert_eq!(standard.checked_add(wei), None);
124        assert_eq!(wei.checked_sub(standard), None);
125        assert_eq!(standard.checked_sub(wei), None);
126    }
127
128    #[rstest]
129    fn test_checked_arith_rejects_mixed_defi_scales() {
130        // Both above FIXED_PRECISION but at different native scales:
131        // precision 17 stores raw at 10^17, precision 18 stores raw at 10^18.
132        let q17 = Quantity::from_raw(100_000_000_000_000_000_u128, 17);
133        let q18 = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128));
134        assert_eq!(q17.checked_add(q18), None);
135        assert_eq!(q18.checked_add(q17), None);
136        assert_eq!(q17.checked_sub(q18), None);
137        assert_eq!(q18.checked_sub(q17), None);
138    }
139
140    #[rstest]
141    fn test_from_wei_large_value() {
142        // Test with a large but valid wei amount
143        let large_wei = U256::from(1_000_000_000_000_000_000_000_u128); // 1000 tokens
144        let quantity = Quantity::from_wei(large_wei);
145        assert_eq!(quantity.precision, 18);
146        assert_eq!(quantity.as_decimal(), dec!(1000.0));
147    }
148
149    #[rstest]
150    fn test_from_wei_small_value() {
151        // Test with a small but representable wei amount (1 million wei = 1e-12)
152        // Very small values like 1 wei (1e-18) are at the edge of f64 precision
153        let small_wei = U256::from(1_000_000_u128);
154        let quantity = Quantity::from_wei(small_wei);
155        assert_eq!(quantity.precision, 18);
156        assert_eq!(quantity.as_decimal(), dec!(0.000000000001));
157    }
158
159    #[rstest]
160    fn test_from_wei_zero() {
161        let quantity = Quantity::from_wei(U256::ZERO);
162        assert_eq!(quantity.precision, 18);
163        assert_eq!(quantity.as_decimal(), dec!(0));
164        assert_eq!(quantity.as_wei(), U256::ZERO);
165    }
166
167    #[rstest]
168    fn test_from_wei_very_large_value() {
169        // Test with a very large but valid wei amount (1 billion tokens)
170        let large_wei = U256::from(1_000_000_000_000_000_000_000_000_000_u128);
171        let quantity = Quantity::from_wei(large_wei);
172        assert_eq!(quantity.precision, 18);
173        assert_eq!(quantity.as_wei(), large_wei);
174        assert_eq!(quantity.as_decimal(), dec!(1000000000));
175    }
176
177    #[rstest]
178    #[should_panic(expected = "raw wei value exceeds unsigned 128-bit range")]
179    fn test_from_wei_overflow() {
180        let overflow_wei = U256::from(u128::MAX) + U256::from(1_u64);
181        let _ = Quantity::from_wei(overflow_wei);
182    }
183
184    #[rstest]
185    fn test_from_wei_various_amounts() {
186        // Test various wei amounts and their decimal equivalents
187        let test_cases = vec![
188            (1_u128, dec!(0.000000000000000001)),        // 1 wei
189            (1000_u128, dec!(0.000000000000001)),        // 1 thousand wei
190            (1_000_000_u128, dec!(0.000000000001)),      // 1 million wei
191            (1_000_000_000_u128, dec!(0.000000001)),     // 1 gwei
192            (1_000_000_000_000_u128, dec!(0.000001)),    // 1 microtoken
193            (1_000_000_000_000_000_u128, dec!(0.001)),   // 1 millitoken
194            (1_000_000_000_000_000_000_u128, dec!(1)),   // 1 token
195            (10_000_000_000_000_000_000_u128, dec!(10)), // 10 tokens
196        ];
197
198        for (wei_amount, expected_decimal) in test_cases {
199            let quantity = Quantity::from_wei(U256::from(wei_amount));
200            assert_eq!(quantity.precision, 18);
201            assert_eq!(quantity.as_decimal(), expected_decimal);
202            assert_eq!(quantity.as_wei(), U256::from(wei_amount));
203        }
204    }
205
206    #[rstest]
207    fn test_as_wei_precision_validation() {
208        // Test that as_wei() requires exactly precision 18
209        for precision in [2, 6, 8, 16] {
210            let quantity = Quantity::new(123.45, precision);
211            let result = std::panic::catch_unwind(|| quantity.as_wei());
212            assert!(
213                result.is_err(),
214                "as_wei() should panic for precision {precision}"
215            );
216        }
217    }
218
219    #[rstest]
220    fn test_arithmetic_operations_with_wei() {
221        let quantity1 = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
222        let quantity2 = Quantity::from_wei(U256::from(500_000_000_000_000_000_u128)); // 0.5
223
224        // Test addition
225        let sum = quantity1 + quantity2;
226        assert_eq!(sum.precision, 18);
227        assert_eq!(sum.as_decimal(), dec!(1.5));
228        assert_eq!(sum.as_wei(), U256::from(1_500_000_000_000_000_000_u128));
229
230        // Test subtraction
231        let diff = quantity1 - quantity2;
232        assert_eq!(diff.precision, 18);
233        assert_eq!(diff.as_decimal(), dec!(0.5));
234        assert_eq!(diff.as_wei(), U256::from(500_000_000_000_000_000_u128));
235    }
236
237    #[rstest]
238    fn test_comparison_operations_with_wei() {
239        let quantity1 = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
240        let quantity2 = Quantity::from_wei(U256::from(2_000_000_000_000_000_000_u128)); // 2.0
241        let quantity3 = Quantity::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
242
243        assert!(quantity1 < quantity2);
244        assert!(quantity2 > quantity1);
245        assert_eq!(quantity1, quantity3);
246        assert!(quantity1 <= quantity3);
247        assert!(quantity1 >= quantity3);
248    }
249}