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}