nautilus_blockchain/
math.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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//! Mathematical utilities for blockchain value conversion.
17//!
18//! This module provides functions for converting large integer types (U256, I256)
19//! used in blockchain applications to floating-point values, accounting for
20//! token decimal places and precision requirements.
21
22use alloy::primitives::{I256, U256};
23
24/// Convert an alloy's I256 value to f64, accounting for token decimals.
25///
26/// # Errors
27///
28/// Returns an error if the I256 value cannot be parsed to f64.
29pub fn convert_i256_to_f64(amount: I256, decimals: u8) -> anyhow::Result<f64> {
30    // Handle the sign separately
31    let is_negative = amount.is_negative();
32    let abs_amount = if is_negative { -amount } else { amount };
33
34    // Convert to string to avoid precision loss for large numbers
35    let amount_str = abs_amount.to_string();
36    let mut amount_f64: f64 = amount_str
37        .parse()
38        .map_err(|e| anyhow::anyhow!("Failed to parse I256 to f64: {}", e))?;
39
40    // Apply sign
41    if is_negative {
42        amount_f64 = -amount_f64;
43    }
44
45    // Apply decimal scaling
46    let factor = 10f64.powi(i32::from(decimals));
47    Ok(amount_f64 / factor)
48}
49
50/// Convert an alloy's U256 value to f64, accounting for token decimals.
51///
52/// # Errors
53///
54/// Returns an error if the U256 value cannot be parsed to f64.
55pub fn convert_u256_to_f64(amount: U256, decimals: u8) -> anyhow::Result<f64> {
56    // Convert to string to avoid precision loss for large numbers
57    let amount_str = amount.to_string();
58    let amount_f64: f64 = amount_str
59        .parse()
60        .map_err(|e| anyhow::anyhow!("Failed to parse U256 to f64: {}", e))?;
61
62    // Apply decimal scaling
63    let factor = 10f64.powi(i32::from(decimals));
64    Ok(amount_f64 / factor)
65}
66
67#[cfg(test)]
68mod tests {
69    use std::str::FromStr;
70
71    use alloy::primitives::{I256, U256};
72    use rstest::rstest;
73
74    use super::*;
75
76    #[rstest]
77    fn test_convert_positive_i256_to_f64() {
78        // Test with 6 decimals (USDC-like)
79        let amount = I256::from_str("1000000").unwrap();
80        let result = convert_i256_to_f64(amount, 6).unwrap();
81        assert_eq!(result, 1.0);
82
83        // Test with 18 decimals (ETH-like)
84        let amount = I256::from_str("1000000000000000000").unwrap();
85        let result = convert_i256_to_f64(amount, 18).unwrap();
86        assert_eq!(result, 1.0);
87    }
88
89    #[rstest]
90    fn test_convert_negative_i256_to_f64() {
91        // Test negative value with 6 decimals
92        let amount = I256::from_str("-1000000").unwrap();
93        let result = convert_i256_to_f64(amount, 6).unwrap();
94        assert_eq!(result, -1.0);
95
96        // Test negative value with 18 decimals
97        let amount = I256::from_str("-2500000000000000000").unwrap();
98        let result = convert_i256_to_f64(amount, 18).unwrap();
99        assert_eq!(result, -2.5);
100    }
101
102    #[rstest]
103    fn test_convert_zero_i256_to_f64() {
104        let amount = I256::ZERO;
105        let result = convert_i256_to_f64(amount, 6).unwrap();
106        assert_eq!(result, 0.0);
107
108        let result = convert_i256_to_f64(amount, 18).unwrap();
109        assert_eq!(result, 0.0);
110    }
111
112    #[rstest]
113    fn test_convert_fractional_amounts() {
114        // Test 0.5 with 6 decimals
115        let amount = I256::from_str("500000").unwrap();
116        let result = convert_i256_to_f64(amount, 6).unwrap();
117        assert_eq!(result, 0.5);
118
119        // Test 0.123456 with 6 decimals
120        let amount = I256::from_str("123456").unwrap();
121        let result = convert_i256_to_f64(amount, 6).unwrap();
122        assert_eq!(result, 0.123456);
123
124        // Test negative fractional
125        let amount = I256::from_str("-123456").unwrap();
126        let result = convert_i256_to_f64(amount, 6).unwrap();
127        assert_eq!(result, -0.123456);
128    }
129
130    #[rstest]
131    fn test_convert_large_i256_values() {
132        // Test very large positive value
133        let large_value = U256::from(10).pow(U256::from(30)); // 10^30
134        let amount = I256::try_from(large_value).unwrap();
135        let result = convert_i256_to_f64(amount, 18).unwrap();
136        assert_eq!(result, 1e12); // 10^30 / 10^18 = 10^12
137
138        // Test maximum safe integer range
139        let amount = I256::from_str("9007199254740991").unwrap(); // MAX_SAFE_INTEGER
140        let result = convert_i256_to_f64(amount, 0).unwrap();
141        assert_eq!(result, 9_007_199_254_740_991.0);
142    }
143
144    #[rstest]
145    fn test_convert_with_different_decimals() {
146        let amount = I256::from_str("1000000000").unwrap();
147
148        // 0 decimals
149        let result = convert_i256_to_f64(amount, 0).unwrap();
150        assert_eq!(result, 1_000_000_000.0);
151
152        // 9 decimals
153        let result = convert_i256_to_f64(amount, 9).unwrap();
154        assert_eq!(result, 1.0);
155
156        // 12 decimals
157        let result = convert_i256_to_f64(amount, 12).unwrap();
158        assert_eq!(result, 0.001);
159    }
160
161    #[rstest]
162    fn test_convert_edge_cases() {
163        // Test very small positive amount with high decimals
164        let amount = I256::from_str("1").unwrap();
165        let result = convert_i256_to_f64(amount, 18).unwrap();
166        assert_eq!(result, 1e-18);
167
168        // Test amount smaller than decimal places
169        let amount = I256::from_str("100").unwrap();
170        let result = convert_i256_to_f64(amount, 6).unwrap();
171        assert_eq!(result, 0.0001);
172    }
173
174    #[rstest]
175    fn test_convert_real_world_examples() {
176        // Example: 1234.567890 USDC (6 decimals)
177        let amount = I256::from_str("1234567890").unwrap();
178        let result = convert_i256_to_f64(amount, 6).unwrap();
179        assert!((result - 1234.567890).abs() < f64::EPSILON);
180
181        // Example: -0.005 ETH (18 decimals)
182        let amount = I256::from_str("-5000000000000000").unwrap();
183        let result = convert_i256_to_f64(amount, 18).unwrap();
184        assert_eq!(result, -0.005);
185
186        // Example: Large swap amount - 100,000 tokens with 8 decimals
187        let amount = I256::from_str("10000000000000").unwrap();
188        let result = convert_i256_to_f64(amount, 8).unwrap();
189        assert_eq!(result, 100_000.0);
190    }
191
192    #[rstest]
193    fn test_precision_boundaries() {
194        // Test precision near f64 boundaries
195        // f64 can accurately represent integers up to 2^53
196        let max_safe = I256::from_str("9007199254740992").unwrap(); // 2^53
197        let result = convert_i256_to_f64(max_safe, 0).unwrap();
198        assert_eq!(result, 9_007_199_254_740_992.0);
199
200        // Test with scientific notation result
201        let amount = I256::from_str("1234567890123456789").unwrap();
202        let result = convert_i256_to_f64(amount, 9).unwrap();
203        assert!((result - 1_234_567_890.123_456_7).abs() < 1.0); // Some precision loss expected
204    }
205
206    // U256 Tests
207    #[rstest]
208    fn test_convert_positive_u256_to_f64() {
209        // Test with 6 decimals (USDC-like)
210        let amount = U256::from_str("1000000").unwrap();
211        let result = convert_u256_to_f64(amount, 6).unwrap();
212        assert_eq!(result, 1.0);
213
214        // Test with 18 decimals (ETH-like)
215        let amount = U256::from_str("1000000000000000000").unwrap();
216        let result = convert_u256_to_f64(amount, 18).unwrap();
217        assert_eq!(result, 1.0);
218    }
219
220    #[rstest]
221    fn test_convert_zero_u256_to_f64() {
222        let amount = U256::ZERO;
223        let result = convert_u256_to_f64(amount, 6).unwrap();
224        assert_eq!(result, 0.0);
225
226        let result = convert_u256_to_f64(amount, 18).unwrap();
227        assert_eq!(result, 0.0);
228    }
229
230    #[rstest]
231    fn test_convert_fractional_u256_amounts() {
232        // Test 0.5 with 6 decimals
233        let amount = U256::from_str("500000").unwrap();
234        let result = convert_u256_to_f64(amount, 6).unwrap();
235        assert_eq!(result, 0.5);
236
237        // Test 0.123456 with 6 decimals
238        let amount = U256::from_str("123456").unwrap();
239        let result = convert_u256_to_f64(amount, 6).unwrap();
240        assert_eq!(result, 0.123456);
241    }
242
243    #[rstest]
244    fn test_convert_large_u256_values() {
245        // Test very large positive value
246        let large_value = U256::from(10).pow(U256::from(30)); // 10^30
247        let result = convert_u256_to_f64(large_value, 18).unwrap();
248        assert_eq!(result, 1e12); // 10^30 / 10^18 = 10^12
249
250        // Test maximum safe integer range
251        let amount = U256::from_str("9007199254740991").unwrap(); // MAX_SAFE_INTEGER
252        let result = convert_u256_to_f64(amount, 0).unwrap();
253        assert_eq!(result, 9_007_199_254_740_991.0);
254    }
255
256    #[rstest]
257    fn test_convert_u256_with_different_decimals() {
258        let amount = U256::from_str("1000000000").unwrap();
259
260        // 0 decimals
261        let result = convert_u256_to_f64(amount, 0).unwrap();
262        assert_eq!(result, 1_000_000_000.0);
263
264        // 9 decimals
265        let result = convert_u256_to_f64(amount, 9).unwrap();
266        assert_eq!(result, 1.0);
267
268        // 12 decimals
269        let result = convert_u256_to_f64(amount, 12).unwrap();
270        assert_eq!(result, 0.001);
271    }
272
273    #[rstest]
274    fn test_convert_u256_edge_cases() {
275        // Test very small positive amount with high decimals
276        let amount = U256::from_str("1").unwrap();
277        let result = convert_u256_to_f64(amount, 18).unwrap();
278        assert_eq!(result, 1e-18);
279
280        // Test amount smaller than decimal places
281        let amount = U256::from_str("100").unwrap();
282        let result = convert_u256_to_f64(amount, 6).unwrap();
283        assert_eq!(result, 0.0001);
284    }
285
286    #[rstest]
287    fn test_convert_u256_real_world_examples() {
288        // Example: 1234.567890 USDC (6 decimals)
289        let amount = U256::from_str("1234567890").unwrap();
290        let result = convert_u256_to_f64(amount, 6).unwrap();
291        assert!((result - 1234.567890).abs() < f64::EPSILON);
292
293        // Example: Large liquidity amount - 100,000 tokens with 8 decimals
294        let amount = U256::from_str("10000000000000").unwrap();
295        let result = convert_u256_to_f64(amount, 8).unwrap();
296        assert_eq!(result, 100_000.0);
297
298        // Example: Very large supply - 1 trillion tokens with 18 decimals
299        let amount = U256::from_str("1000000000000000000000000000000").unwrap(); // 10^30
300        let result = convert_u256_to_f64(amount, 18).unwrap();
301        assert_eq!(result, 1e12);
302    }
303
304    #[rstest]
305    fn test_convert_u256_precision_boundaries() {
306        // Test precision near f64 boundaries
307        // f64 can accurately represent integers up to 2^53
308        let max_safe = U256::from_str("9007199254740992").unwrap(); // 2^53
309        let result = convert_u256_to_f64(max_safe, 0).unwrap();
310        assert_eq!(result, 9_007_199_254_740_992.0);
311
312        // Test with scientific notation result
313        let amount = U256::from_str("1234567890123456789").unwrap();
314        let result = convert_u256_to_f64(amount, 9).unwrap();
315        assert!((result - 1_234_567_890.123_456_7).abs() < 1.0); // Some precision loss expected
316    }
317
318    #[rstest]
319    fn test_convert_u256_vs_i256_consistency() {
320        // Test that positive values give same results for U256 and I256
321        let u256_amount = U256::from_str("1000000000000000000").unwrap();
322        let i256_amount = I256::from_str("1000000000000000000").unwrap();
323
324        let u256_result = convert_u256_to_f64(u256_amount, 18).unwrap();
325        let i256_result = convert_i256_to_f64(i256_amount, 18).unwrap();
326
327        assert_eq!(u256_result, i256_result);
328        assert_eq!(u256_result, 1.0);
329    }
330
331    #[rstest]
332    fn test_convert_u256_max_values() {
333        // Test very large U256 values that wouldn't fit in I256
334        let large_u256 = U256::from(2).pow(U256::from(255)); // Close to U256::MAX
335        let result = convert_u256_to_f64(large_u256, 0).unwrap();
336        // Should be a very large number but not infinite
337        assert!(result.is_finite());
338        assert!(result > 0.0);
339
340        // Test with decimals to bring it down to reasonable range
341        let large_u256_with_decimals = U256::from(2).pow(U256::from(60)); // 2^60
342        let result = convert_u256_to_f64(large_u256_with_decimals, 18).unwrap();
343        // 2^60 ≈ 1.15e18, so 2^60 / 10^18 ≈ 1.15
344        assert!(result.is_finite());
345        assert!(result > 1.0);
346        assert!(result < 2.0); // Should be around 1.15
347    }
348}