nautilus_model/defi/
hex.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//! Hexadecimal string parsing utilities for blockchain data.
17//!
18//! This module provides functions for converting hexadecimal strings (commonly used in blockchain
19//! APIs and JSON-RPC responses) to native Rust types, with specialized support for timestamps.
20
21use alloy_primitives::U256;
22use nautilus_core::{UnixNanos, datetime::NANOSECONDS_IN_SECOND};
23use serde::{Deserialize, Deserializer};
24
25/// Converts a hexadecimal string to a u64 integer.
26///
27/// # Errors
28///
29/// Returns a `std::num::ParseIntError` if:
30/// - The input string contains non-hexadecimal characters.
31/// - The hexadecimal value is too large to fit in a u64.
32/// - The hex string is longer than 16 characters (excluding 0x prefix).
33pub fn from_str_hex_to_u64(hex_string: &str) -> Result<u64, std::num::ParseIntError> {
34    let without_prefix = if hex_string.starts_with("0x") || hex_string.starts_with("0X") {
35        &hex_string[2..]
36    } else {
37        hex_string
38    };
39
40    // A `u64` can hold 16 full hex characters (0xffff_ffff_ffff_ffff). Anything longer is a
41    // guaranteed overflow so we proactively short-circuit with the same error type that the
42    // native parser would return. We build this error once via an intentionally-overflowing
43    // parse call and reuse it whenever necessary (this avoids the `unwrap_err()` call in hot
44    // paths).
45    if without_prefix.len() > 16 {
46        // Force–generate the standard overflow error and return it. This keeps the public API
47        // identical to the branch that would have overflowed inside `from_str_radix`.
48        return Err(u64::from_str_radix("ffffffffffffffffffffffff", 16).unwrap_err());
49    }
50
51    u64::from_str_radix(without_prefix, 16)
52}
53
54/// Custom deserializer function for hex numbers.
55///
56/// # Errors
57///
58/// Returns an error if parsing the hex string to a number fails.
59pub fn deserialize_hex_number<'de, D>(deserializer: D) -> Result<u64, D::Error>
60where
61    D: Deserializer<'de>,
62{
63    let hex_string = String::deserialize(deserializer)?;
64    from_str_hex_to_u64(hex_string.as_str()).map_err(serde::de::Error::custom)
65}
66
67/// Custom deserializer that converts an optional hexadecimal string into an `Option<u64>`.
68///
69/// The field is treated as optional – if the JSON field is `null` or absent the function returns
70/// `Ok(None)`. When the value **is** present it is parsed via [`from_str_hex_to_u64`] and wrapped
71/// in `Some(..)`.
72///
73/// # Errors
74///
75/// Returns a [`serde::de::Error`] if the provided string is not valid hexadecimal or if the value
76/// is larger than the `u64` range.
77pub fn deserialize_opt_hex_u64<'de, D>(deserializer: D) -> Result<Option<u64>, D::Error>
78where
79    D: Deserializer<'de>,
80{
81    // We first deserialize the value into an `Option<String>` so that missing / null JSON keys
82    // gracefully map to `None`.
83    let opt = Option::<String>::deserialize(deserializer)?;
84
85    match opt {
86        None => Ok(None),
87        Some(hex_string) => from_str_hex_to_u64(hex_string.as_str())
88            .map(Some)
89            .map_err(serde::de::Error::custom),
90    }
91}
92
93/// Custom deserializer that converts an optional hexadecimal string into an `Option<U256>`.
94/// A `None` result indicates the field was absent or explicitly `null`.
95///
96/// # Errors
97///
98/// Returns a [`serde::de::Error`] if the string is not valid hex or cannot be parsed into `U256`.
99pub fn deserialize_opt_hex_u256<'de, D>(deserializer: D) -> Result<Option<U256>, D::Error>
100where
101    D: Deserializer<'de>,
102{
103    let opt = Option::<String>::deserialize(deserializer)?;
104
105    match opt {
106        None => Ok(None),
107        Some(hex_string) => {
108            let without_prefix = if hex_string.starts_with("0x") || hex_string.starts_with("0X") {
109                &hex_string[2..]
110            } else {
111                hex_string.as_str()
112            };
113
114            U256::from_str_radix(without_prefix, 16)
115                .map(Some)
116                .map_err(serde::de::Error::custom)
117        }
118    }
119}
120
121/// Custom deserializer function for hex timestamps to convert hex seconds to `UnixNanos`.
122///
123/// # Errors
124///
125/// Returns an error if parsing the hex string to a timestamp fails.
126pub fn deserialize_hex_timestamp<'de, D>(deserializer: D) -> Result<UnixNanos, D::Error>
127where
128    D: Deserializer<'de>,
129{
130    let hex_string = String::deserialize(deserializer)?;
131    let seconds = from_str_hex_to_u64(hex_string.as_str()).map_err(serde::de::Error::custom)?;
132
133    // Protect against multiplication overflow (extremely far future dates or malicious input).
134    seconds
135        .checked_mul(NANOSECONDS_IN_SECOND)
136        .map(UnixNanos::new)
137        .ok_or_else(|| serde::de::Error::custom("UnixNanos overflow when converting timestamp"))
138}
139
140#[cfg(test)]
141mod tests {
142    use alloy_primitives::U256;
143
144    use super::*;
145
146    #[test]
147    fn test_from_str_hex_to_u64_valid() {
148        assert_eq!(from_str_hex_to_u64("0x0").unwrap(), 0);
149        assert_eq!(from_str_hex_to_u64("0x1").unwrap(), 1);
150        // Upper-case prefix should also be accepted
151        assert_eq!(from_str_hex_to_u64("0XfF").unwrap(), 255);
152        assert_eq!(from_str_hex_to_u64("0xff").unwrap(), 255);
153        assert_eq!(from_str_hex_to_u64("0xffffffffffffffff").unwrap(), u64::MAX);
154        assert_eq!(from_str_hex_to_u64("1234abcd").unwrap(), 0x1234abcd);
155    }
156
157    #[test]
158    fn test_from_str_hex_to_u64_too_long() {
159        // 17 characters should fail (exceeds u64 max length)
160        let too_long = "0x1ffffffffffffffff";
161        assert!(from_str_hex_to_u64(too_long).is_err());
162
163        // Even longer should also fail
164        let very_long = "0x123456789abcdef123456789abcdef";
165        assert!(from_str_hex_to_u64(very_long).is_err());
166    }
167
168    #[test]
169    fn test_from_str_hex_to_u64_invalid_chars() {
170        assert!(from_str_hex_to_u64("0xzz").is_err());
171        assert!(from_str_hex_to_u64("0x123g").is_err());
172    }
173
174    #[test]
175    fn test_deserialize_hex_timestamp() {
176        // Test that hex timestamp conversion works
177        let timestamp_hex = "0x64b5f3bb"; // Some timestamp
178        let expected_nanos = 0x64b5f3bb * NANOSECONDS_IN_SECOND;
179
180        // This tests the conversion logic, though we can't easily test the deserializer directly
181        assert_eq!(
182            from_str_hex_to_u64(timestamp_hex).unwrap() * NANOSECONDS_IN_SECOND,
183            expected_nanos
184        );
185    }
186
187    #[test]
188    fn test_deserialize_opt_hex_u256_present() {
189        let json = "\"0x1a\"";
190        let value: Option<U256> = serde_json::from_str(json).unwrap();
191        assert_eq!(value, Some(U256::from(26u8)));
192    }
193
194    #[test]
195    fn test_deserialize_opt_hex_u256_null() {
196        let json = "null";
197        let value: Option<U256> = serde_json::from_str(json).unwrap();
198        assert!(value.is_none());
199    }
200}