Skip to main content

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