Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion hypersync-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hypersync-client"
version = "0.18.4"
version = "0.18.5"
edition = "2021"
description = "client library for hypersync"
license = "MPL-2.0"
Expand Down
2 changes: 1 addition & 1 deletion hypersync-format/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hypersync-format"
version = "0.5.3"
version = "0.5.5"
edition = "2021"
description = "evm format library"
license = "MPL-2.0"
Expand Down
61 changes: 40 additions & 21 deletions hypersync-format/src/types/quantity.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use super::{util::canonicalize_bytes, Hex};
use crate::{Error, Result};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Cow;
use std::fmt;
Expand Down Expand Up @@ -89,31 +88,36 @@ impl<const N: usize> From<[u8; N]> for Quantity {
}
}

struct QuantityVisitor;

impl Visitor<'_> for QuantityVisitor {
type Value = Quantity;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("hex string for a quantity")
}

fn visit_str<E>(self, value: &str) -> StdResult<Self::Value, E>
where
E: de::Error,
{
let buf: Vec<u8> = decode_hex(value).map_err(|e| E::custom(e.to_string()))?;

Ok(Quantity::from(buf))
}
}

impl<'de> Deserialize<'de> for Quantity {
fn deserialize<D>(deserializer: D) -> StdResult<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(QuantityVisitor)
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
Str(String),
Int(i64),
}
match StringOrInt::deserialize(deserializer)? {
StringOrInt::Str(value) => match decode_hex(&value) {
Ok(buf) => Ok(Quantity::from(buf)),
Err(e) => Err(serde::de::Error::custom(format!(
"invalid hex string: {}",
e
))),
},
StringOrInt::Int(value) => {
if value < 0 {
return Err(serde::de::Error::custom(
"negative int quantity not allowed",
));
}
// Convert the integer to big-endian bytes and canonicalize
let buf = canonicalize_bytes(value.to_be_bytes().to_vec());
Ok(Quantity::from(buf))
}
}
Comment on lines +96 to +120
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bug: U64 values > i64::MAX are rejected; handle u64 explicitly.

The untagged enum only has i64, so Token::U64 above i64::MAX will fail to deserialize. This contradicts the PR goal to accept numeric quantities (including full u64 range).

Apply:

-        #[derive(Deserialize)]
-        #[serde(untagged)]
-        enum StringOrInt {
-            Str(String),
-            Int(i64),
-        }
+        #[derive(Deserialize)]
+        #[serde(untagged)]
+        enum StringOrInt {
+            Str(String),
+            UInt(u64),
+            Int(i64),
+        }
@@
-            StringOrInt::Str(value) => match decode_hex(&value) {
+            StringOrInt::Str(value) => match decode_hex(&value) {
                 Ok(buf) => Ok(Quantity::from(buf)),
-                Err(e) => Err(serde::de::Error::custom(format!(
-                    "invalid hex string: {}",
-                    e
-                ))),
+                Err(e) => Err(serde::de::Error::custom(e)),
             },
+            StringOrInt::UInt(value) => {
+                let buf = canonicalize_bytes(value.to_be_bytes().to_vec());
+                Ok(Quantity::from(buf))
+            }
             StringOrInt::Int(value) => {
                 if value < 0 {
                     return Err(serde::de::Error::custom(
                         "negative int quantity not allowed",
                     ));
                 }
-                // Convert the integer to big-endian bytes and canonicalize
-                let buf = canonicalize_bytes(value.to_be_bytes().to_vec());
+                // Convert to u64 first to avoid sign-extension concerns.
+                let buf = canonicalize_bytes((value as u64).to_be_bytes().to_vec());
                 Ok(Quantity::from(buf))
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
Str(String),
Int(i64),
}
match StringOrInt::deserialize(deserializer)? {
StringOrInt::Str(value) => match decode_hex(&value) {
Ok(buf) => Ok(Quantity::from(buf)),
Err(e) => Err(serde::de::Error::custom(format!(
"invalid hex string: {}",
e
))),
},
StringOrInt::Int(value) => {
if value < 0 {
return Err(serde::de::Error::custom(
"negative int quantity not allowed",
));
}
// Convert the integer to big-endian bytes and canonicalize
let buf = canonicalize_bytes(value.to_be_bytes().to_vec());
Ok(Quantity::from(buf))
}
}
#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrInt {
Str(String),
UInt(u64),
Int(i64),
}
match StringOrInt::deserialize(deserializer)? {
StringOrInt::Str(value) => match decode_hex(&value) {
Ok(buf) => Ok(Quantity::from(buf)),
Err(e) => Err(serde::de::Error::custom(e)),
},
StringOrInt::UInt(value) => {
let buf = canonicalize_bytes(value.to_be_bytes().to_vec());
Ok(Quantity::from(buf))
}
StringOrInt::Int(value) => {
if value < 0 {
return Err(serde::de::Error::custom(
"negative int quantity not allowed",
));
}
// Convert to u64 first to avoid sign-extension concerns.
let buf = canonicalize_bytes((value as u64).to_be_bytes().to_vec());
Ok(Quantity::from(buf))
}
}
🤖 Prompt for AI Agents
In hypersync-format/src/types/quantity.rs around lines 96 to 120, the untagged
enum currently uses only i64 so deserialization fails for numeric Token::U64
values > i64::MAX; update the enum to include a U64(u64) variant, add a match
arm for StringOrInt::U64 that canonicalizes the u64 value into big-endian bytes
(no negative check) and returns Quantity::from(those bytes), and keep the
existing String and Int(i64) arms with the Int arm still rejecting negative
values before canonicalizing; ensure conversion uses to_be_bytes() for u64 and
to_be_bytes() for i64 as before and reuse canonicalize_bytes for both paths.

}
}

Expand Down Expand Up @@ -267,4 +271,19 @@ mod tests {
assert_de_tokens(&Quantity::from(hex!("00")), &[Token::Str("0x00")]);
assert_de_tokens(&Quantity::from(hex!("00")), &[Token::Str("0x0000")]);
}

#[test]
fn test_deserialize_numeric_u64() {
// Numeric JSON values should be accepted (e.g., Sonic timestamps)
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::U64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::U64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::U64(1)]);
}

#[test]
fn test_deserialize_numeric_i64() {
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::I64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::I64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::I64(1)]);
}
Comment on lines +275 to +288
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add coverage: u64 above i64::MAX and negative i64 rejection.

Current tests don't catch the u64 overflow case or negative ints. Add:

     #[test]
     fn test_deserialize_numeric_u64() {
         // Numeric JSON values should be accepted (e.g., Sonic timestamps)
         assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::U64(0x66a7c725)]);
         assert_de_tokens(&Quantity::from(vec![0]), &[Token::U64(0)]);
         assert_de_tokens(&Quantity::from(hex!("01")), &[Token::U64(1)]);
     }
 
     #[test]
     fn test_deserialize_numeric_i64() {
         assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::I64(0x66a7c725)]);
         assert_de_tokens(&Quantity::from(vec![0]), &[Token::I64(0)]);
         assert_de_tokens(&Quantity::from(hex!("01")), &[Token::I64(1)]);
     }
+
+    #[test]
+    fn test_deserialize_numeric_u64_over_i64_max() {
+        // 0x8000_0000_0000_0000 = i64::MAX + 1
+        assert_de_tokens(
+            &Quantity::from(hex!("8000000000000000")),
+            &[Token::U64(0x8000_0000_0000_0000)],
+        );
+    }
+
+    #[test]
+    #[should_panic(expected = "negative int quantity not allowed")]
+    fn test_deserialize_negative_i64_rejected() {
+        // Ensure negatives are rejected
+        let _ = assert_de_tokens(&Quantity::default(), &[Token::I64(-1)]);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
#[test]
fn test_deserialize_numeric_u64() {
// Numeric JSON values should be accepted (e.g., Sonic timestamps)
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::U64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::U64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::U64(1)]);
}
#[test]
fn test_deserialize_numeric_i64() {
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::I64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::I64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::I64(1)]);
}
#[test]
fn test_deserialize_numeric_u64() {
// Numeric JSON values should be accepted (e.g., Sonic timestamps)
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::U64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::U64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::U64(1)]);
}
#[test]
fn test_deserialize_numeric_i64() {
assert_de_tokens(&Quantity::from(hex!("66a7c725")), &[Token::I64(0x66a7c725)]);
assert_de_tokens(&Quantity::from(vec![0]), &[Token::I64(0)]);
assert_de_tokens(&Quantity::from(hex!("01")), &[Token::I64(1)]);
}
#[test]
fn test_deserialize_numeric_u64_over_i64_max() {
// 0x8000_0000_0000_0000 = i64::MAX + 1
assert_de_tokens(
&Quantity::from(hex!("8000000000000000")),
&[Token::U64(0x8000_0000_0000_0000)],
);
}
#[test]
#[should_panic(expected = "negative int quantity not allowed")]
fn test_deserialize_negative_i64_rejected() {
// Ensure negatives are rejected
let _ = assert_de_tokens(&Quantity::default(), &[Token::I64(-1)]);
}

}
Loading