Skip to main content

nautilus_model/defi/types/
price.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 [`Price`] type.
17
18use alloy_primitives::U256;
19
20use crate::types::price::Price;
21
22impl Price {
23    /// Constructs a [`Price`] from a raw amount expressed in wei (18-decimal fixed-point).
24    ///
25    /// The resulting [`Price`] will always have `precision` equal to `18`.
26    ///
27    /// # Panics
28    ///
29    /// Panics if the supplied `raw_wei` cannot fit into a signed 128-bit integer (this would
30    /// exceed the numeric range of the internal `PriceRaw` 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 128-bit range");
40
41        assert!(
42            raw_u128 <= i128::MAX as u128,
43            "raw wei value exceeds signed 128-bit range"
44        );
45
46        let raw_i128: i128 = raw_u128 as i128;
47        Self::from_raw(raw_i128, 18)
48    }
49
50    /// Converts this [`Price`] to a wei amount (U256).
51    ///
52    /// Only valid for prices with precision 18. For other precisions convert to precision 18 first.
53    ///
54    /// # Panics
55    ///
56    /// Panics if the price has precision other than 18 or if the raw value is negative.
57    #[must_use]
58    pub fn as_wei(&self) -> U256 {
59        assert!(
60            self.precision == 18,
61            "Failed to convert price with precision {} to wei (requires precision 18)",
62            self.precision
63        );
64
65        assert!(
66            !self.is_negative(),
67            "Failed to convert negative price to wei"
68        );
69
70        // We've checked that raw is non-negative, so casting to u128 is safe
71        U256::from(self.raw() as u128)
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use rstest::rstest;
78    use rust_decimal_macros::dec;
79
80    use super::*;
81
82    #[rstest]
83    fn test_from_wei_basic() {
84        let price = Price::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1 ETH in wei
85        assert_eq!(price.precision, 18);
86        assert_eq!(price.as_decimal(), dec!(1.0));
87    }
88
89    #[rstest]
90    fn test_precision_18_requires_from_wei() {
91        // Verify that precision 18 cannot be used with float-based constructor
92        let result = Price::new_checked(1.0, 18);
93        assert!(result.is_err());
94        assert!(
95            result
96                .unwrap_err()
97                .to_string()
98                .contains("use `Price::from_wei()` for wei values")
99        );
100
101        // But from_wei works fine with precision 18
102        let price = Price::from_wei(U256::from(1_000_000_000_000_000_000_u128));
103        assert_eq!(price.precision, 18);
104        assert_eq!(price.as_decimal(), dec!(1.0));
105    }
106
107    #[rstest]
108    fn test_as_wei_basic() {
109        let price = Price::from_raw(1_000_000_000_000_000_000_i128, 18);
110        let wei = price.as_wei();
111        assert_eq!(wei, U256::from(1_000_000_000_000_000_000_u128));
112    }
113
114    #[rstest]
115    #[should_panic(
116        expected = "Failed to convert price with precision 2 to wei (requires precision 18)"
117    )]
118    fn test_as_wei_wrong_precision() {
119        let price = Price::new(1.23, 2);
120        let _ = price.as_wei();
121    }
122
123    #[rstest]
124    #[should_panic(expected = "Failed to convert negative price")]
125    fn test_as_wei_negative_price() {
126        let price = Price::from_raw(-1_000_000_000_000_000_000_i128, 18);
127        let _ = price.as_wei();
128    }
129
130    #[rstest]
131    fn test_wei_round_trip() {
132        let original_wei = U256::from(1_500_000_000_000_000_000_u128); // 1.5 ETH
133        let price = Price::from_wei(original_wei);
134        let converted_wei = price.as_wei();
135        assert_eq!(original_wei, converted_wei);
136        assert_eq!(price.as_decimal(), dec!(1.5));
137    }
138
139    #[rstest]
140    fn test_from_wei_zero() {
141        let price = Price::from_wei(U256::ZERO);
142        assert_eq!(price.precision, 18);
143        assert_eq!(price.as_decimal(), dec!(0));
144        assert_eq!(price.as_wei(), U256::ZERO);
145    }
146
147    #[rstest]
148    fn test_from_wei_very_large_value() {
149        // Test with a very large but valid wei amount (1 billion ETH)
150        let large_wei = U256::from(1_000_000_000_000_000_000_000_000_000_u128);
151        let price = Price::from_wei(large_wei);
152        assert_eq!(price.precision, 18);
153        assert_eq!(price.as_wei(), large_wei);
154        assert_eq!(price.as_decimal(), dec!(1000000000));
155    }
156
157    #[rstest]
158    #[should_panic(expected = "raw wei value exceeds 128-bit range")]
159    fn test_from_wei_overflow() {
160        let overflow_wei = U256::from(u128::MAX) + U256::from(1u64);
161        let _ = Price::from_wei(overflow_wei);
162    }
163
164    #[rstest]
165    fn test_checked_arith_accepts_wei_precision() {
166        // Wei prices use precision 18 (> FIXED_PRECISION = 16) but are valid, not sentinels.
167        let a = Price::from_wei(U256::from(1_000_000_000_000_000_000u128));
168        let b = Price::from_wei(U256::from(2_000_000_000_000_000_000u128));
169        let sum = a
170            .checked_add(b)
171            .expect("checked_add must accept wei prices");
172        assert_eq!(sum.as_decimal(), dec!(3));
173        let diff = b
174            .checked_sub(a)
175            .expect("checked_sub must accept wei prices");
176        assert_eq!(diff.as_decimal(), dec!(1));
177    }
178
179    #[rstest]
180    fn test_checked_arith_rejects_mixed_scale() {
181        // Wei (precision 18, raw at 10^18 scale) and standard (precision 0, raw at
182        // FIXED_SCALAR scale) cannot be added with raw arithmetic without rescaling.
183        // checked_add / checked_sub must return None rather than produce a wrong result.
184        let wei = Price::from_wei(U256::from(1_000_000_000_000_000_000u128));
185        let standard = Price::new(1.0, 0);
186        assert_eq!(wei.checked_add(standard), None);
187        assert_eq!(standard.checked_add(wei), None);
188        assert_eq!(wei.checked_sub(standard), None);
189        assert_eq!(standard.checked_sub(wei), None);
190    }
191
192    #[rstest]
193    fn test_checked_arith_rejects_mixed_defi_scales() {
194        // Both above FIXED_PRECISION but at different native scales:
195        // precision 17 stores raw at 10^17, precision 18 stores raw at 10^18.
196        let p17 = Price::from_raw(100_000_000_000_000_000_i128, 17);
197        let p18 = Price::from_wei(U256::from(1_000_000_000_000_000_000u128));
198        assert_eq!(p17.checked_add(p18), None);
199        assert_eq!(p18.checked_add(p17), None);
200        assert_eq!(p17.checked_sub(p18), None);
201        assert_eq!(p18.checked_sub(p17), None);
202    }
203
204    #[rstest]
205    fn test_from_wei_small_amounts() {
206        // Test various small wei amounts
207        let test_cases = vec![
208            (1_u128, dec!(0.000000000000000001)),    // 1 wei
209            (1000_u128, dec!(0.000000000000001)),    // 1 picoether
210            (1_000_000_u128, dec!(0.000000000001)),  // 1 nanoether
211            (1_000_000_000_u128, dec!(0.000000001)), // 1 gwei
212        ];
213
214        for (wei_amount, expected_decimal) in test_cases {
215            let price = Price::from_wei(U256::from(wei_amount));
216            assert_eq!(price.precision, 18);
217            assert_eq!(price.as_decimal(), expected_decimal);
218            assert_eq!(price.as_wei(), U256::from(wei_amount));
219        }
220    }
221
222    #[rstest]
223    fn test_from_wei_large_amounts() {
224        // Test various large wei amounts
225        let test_cases = vec![
226            (1_000_000_000_000_000_000_u128, dec!(1)),        // 1 ETH
227            (10_000_000_000_000_000_000_u128, dec!(10)),      // 10 ETH
228            (100_000_000_000_000_000_000_u128, dec!(100)),    // 100 ETH
229            (1_000_000_000_000_000_000_000_u128, dec!(1000)), // 1000 ETH
230        ];
231
232        for (wei_amount, expected_decimal) in test_cases {
233            let price = Price::from_wei(U256::from(wei_amount));
234            assert_eq!(price.precision, 18);
235            assert_eq!(price.as_decimal(), expected_decimal);
236            assert_eq!(price.as_wei(), U256::from(wei_amount));
237        }
238    }
239
240    #[rstest]
241    fn test_as_wei_precision_validation() {
242        // Test that as_wei() requires exactly precision 18
243        for precision in [2, 6, 8, 16] {
244            let price = Price::new(123.45, precision);
245            let result = std::panic::catch_unwind(|| price.as_wei());
246            assert!(
247                result.is_err(),
248                "as_wei() should panic for precision {precision}"
249            );
250        }
251    }
252
253    #[rstest]
254    fn test_arithmetic_operations_with_wei() {
255        let price1 = Price::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
256        let price2 = Price::from_wei(U256::from(500_000_000_000_000_000_u128)); // 0.5
257
258        // Test addition
259        let sum = price1 + price2;
260        assert_eq!(sum.precision, 18);
261        assert_eq!(sum.as_decimal(), dec!(1.5));
262
263        // Test subtraction
264        let diff = price1 - price2;
265        assert_eq!(diff.precision, 18);
266        assert_eq!(diff.as_decimal(), dec!(0.5));
267    }
268
269    #[rstest]
270    fn test_comparison_operations_with_wei() {
271        let price1 = Price::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
272        let price2 = Price::from_wei(U256::from(2_000_000_000_000_000_000_u128)); // 2.0
273        let price3 = Price::from_wei(U256::from(1_000_000_000_000_000_000_u128)); // 1.0
274
275        assert!(price1 < price2);
276        assert!(price2 > price1);
277        assert_eq!(price1, price3);
278        assert!(price1 <= price3);
279        assert!(price1 >= price3);
280    }
281}