nautilus_blockchain/
math.rs

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