Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
3 changes: 3 additions & 0 deletions crates/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ alloy-rlp = { workspace = true, optional = true }

# serde
serde = { workspace = true, optional = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }

# schemars
schemars = { workspace = true, optional = true, default-features = false }
Expand Down Expand Up @@ -134,6 +135,7 @@ std = [
"rand?/thread_rng",
"rustc-hash/std",
"serde?/std",
"serde_json?/std",
"sha3/std",
]
nightly = [
Expand Down Expand Up @@ -175,6 +177,7 @@ rkyv = ["dep:rkyv", "ruint/rkyv"]
rlp = ["dep:alloy-rlp", "ruint/alloy-rlp"]
serde = [
"dep:serde",
"dep:serde_json",
"bytes/serde",
"hex/serde",
"ruint/serde",
Expand Down
2 changes: 2 additions & 0 deletions crates/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ pub use {
#[cfg(feature = "serde")]
#[doc(no_inline)]
pub use ::hex::serde as serde_hex;
#[cfg(feature = "serde")]
pub mod serde;

// Not public API.
#[doc(hidden)]
Expand Down
46 changes: 46 additions & 0 deletions crates/primitives/src/serde/checksum.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//! Serde functions for (de)serializing EIP-55 checksummed addresses.
//!
//! Can also be used for rejecting non checksummend addresses during deserialization.
//!
//! # Example
//! ```
//! use alloy_primitives::{Address, address};
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
//! pub struct Container {
//! #[serde(with = "alloy_primitives::serde::checksum")]
//! value: Address,
//! }
//!
//! let val = Container { value: address!("0xdadB0d80178819F2319190D340ce9A924f783711") };
//! let s = serde_json::to_string(&val).unwrap();
//! assert_eq!(s, "{\"value\":\"0xdadB0d80178819F2319190D340ce9A924f783711\"}");
//!
//! let deserialized: Container = serde_json::from_str(&s).unwrap();
//! assert_eq!(val, deserialized);
//!
//! let invalid = "{\"value\":\"0xdadb0d80178819F2319190D340ce9A924f783711\"}";
//! serde_json::from_str::<Container>(&invalid).unwrap_err();
//! ```

use crate::Address;
use alloc::string::String;
use serde::{Deserialize, Deserializer, Serializer};

/// Serialize an [Address] with EIP-55 checksum.
pub fn serialize<S>(value: &Address, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(&value.to_checksum(None))
}

/// Deserialize an [Address] only if it has EIP-55 checksum.
pub fn deserialize<'de, D>(deserializer: D) -> Result<Address, D::Error>
where
D: Deserializer<'de>,
{
let str = String::deserialize(deserializer)?;
Address::parse_checksummed(str, None).map_err(serde::de::Error::custom)
}
45 changes: 45 additions & 0 deletions crates/primitives/src/serde/displayfromstr.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//! Serde functions for (de)serializing using FromStr and Display
//!
//! Useful for example in encoding SSZ `uintN` primitives using the "canonical JSON mapping"
//! described in the consensus-specs here: <https://github.com/ethereum/consensus-specs/blob/dev/ssz/simple-serialize.md#json-mapping>
//!
//! # Example
//! ```
//! use serde::{Deserialize, Serialize};
//!
//! #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
//! pub struct Container {
//! #[serde(with = "alloy_primitives::serde::displayfromstr")]
//! value: u64,
//! }
//!
//! let val = Container { value: 18112749083033600 };
//! let s = serde_json::to_string(&val).unwrap();
//! assert_eq!(s, "{\"value\":\"18112749083033600\"}");
//!
//! let deserialized: Container = serde_json::from_str(&s).unwrap();
//! assert_eq!(val, deserialized);
//! ```

use alloc::string::String;
use core::{fmt, str::FromStr};
use serde::{Deserialize, Deserializer, Serializer};

/// Serialize a type `T` that implements [fmt::Display] as a quoted string.
pub fn serialize<T, S>(value: &T, serializer: S) -> Result<S::Ok, S::Error>
where
T: fmt::Display,
S: Serializer,
{
serializer.collect_str(value)
}

/// Deserialize a quoted string to a type `T` using [FromStr].
pub fn deserialize<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
D: Deserializer<'de>,
T: FromStr,
T::Err: fmt::Display,
{
String::deserialize(deserializer)?.parse().map_err(serde::de::Error::custom)
}
42 changes: 42 additions & 0 deletions crates/primitives/src/serde/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//! Serde-related utilities for Alloy.

use crate::{B256, hex};
use serde::Serializer;

pub mod displayfromstr;

pub mod checksum;

mod optional;
pub use self::optional::*;

pub mod quantity;

/// Storage related helpers.
pub mod storage;
pub use storage::JsonStorageKey;

pub mod ttd;
pub use ttd::*;

mod other;
pub use other::{OtherFields, WithOtherFields};

/// Serialize a byte vec as a hex string _without_ the "0x" prefix.
///
/// This behaves the same as [`hex::encode`].
pub fn serialize_hex_string_no_prefix<S, T>(x: T, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
T: AsRef<[u8]>,
{
s.serialize_str(&hex::encode(x.as_ref()))
}

/// Serialize a [B256] as a hex string _without_ the "0x" prefix.
pub fn serialize_b256_hex_string_no_prefix<S>(x: &B256, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.collect_str(&format_args!("{x:x}"))
}
78 changes: 78 additions & 0 deletions crates/primitives/src/serde/optional.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//! Serde functions for encoding optional values.

use serde::{Deserialize, Deserializer};

/// For use with serde's `deserialize_with` on a sequence that must be
/// deserialized as a single but optional (i.e. possibly `null`) value.
pub fn null_as_default<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + Default,
D: Deserializer<'de>,
{
Option::<T>::deserialize(deserializer).map(Option::unwrap_or_default)
}

/// For use with serde's `deserialize_with` on a field that must be missing.
pub fn reject_if_some<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: Deserialize<'de>,
D: Deserializer<'de>,
{
let value = Option::<T>::deserialize(deserializer)?;

if value.is_some() {
return Err(serde::de::Error::custom("unexpected value"));
}

Ok(value)
}

#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;

#[derive(Debug, Deserialize, PartialEq)]
struct TestStruct {
#[serde(default, deserialize_with = "null_as_default")]
value: Vec<i32>,

#[serde(default, deserialize_with = "reject_if_some")]
should_be_none: Option<String>,
}

#[test]
fn test_null_as_default_with_null() {
let json_data = json!({ "value": null });
let result: TestStruct = serde_json::from_value(json_data).unwrap();
assert_eq!(result.value, Vec::<i32>::new());
}

#[test]
fn test_null_as_default_with_value() {
let json_data = json!({ "value": [1, 2, 3] });
let result: TestStruct = serde_json::from_value(json_data).unwrap();
assert_eq!(result.value, vec![1, 2, 3]);
}

#[test]
fn test_null_as_default_with_missing_field() {
let json_data = json!({});
let result: TestStruct = serde_json::from_value(json_data).unwrap();
assert_eq!(result.value, Vec::<i32>::new());
}

#[test]
fn test_reject_if_some_with_none() {
let json_data = json!({});
let result: TestStruct = serde_json::from_value(json_data).unwrap();
assert_eq!(result.should_be_none, None);
}

#[test]
fn test_reject_if_some_with_some() {
let json_data = json!({ "should_be_none": "unexpected value" });
let result: Result<TestStruct, _> = serde_json::from_value(json_data);
assert!(result.is_err());
}
}
40 changes: 40 additions & 0 deletions crates/primitives/src/serde/other/arbitrary_.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use super::OtherFields;
use alloc::{collections::BTreeMap, string::String, vec::Vec};

impl arbitrary::Arbitrary<'_> for OtherFields {
fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
let mut inner = BTreeMap::new();
for _ in 0usize..u.int_in_range(0usize..=15)? {
inner.insert(u.arbitrary()?, u.arbitrary::<ArbitraryValue>()?.into_json_value());
}
Ok(Self { inner })
}
}

/// Redefinition of `serde_json::Value` for the purpose of implementing `Arbitrary`.
#[derive(Clone, Debug, arbitrary::Arbitrary)]
enum ArbitraryValue {
Null,
Bool(bool),
Number(u64),
String(String),
Array(Vec<Self>),
Object(BTreeMap<String, Self>),
}

impl ArbitraryValue {
fn into_json_value(self) -> serde_json::Value {
match self {
Self::Null => serde_json::Value::Null,
Self::Bool(b) => serde_json::Value::Bool(b),
Self::Number(n) => serde_json::Value::Number(n.into()),
Self::String(s) => serde_json::Value::String(s),
Self::Array(a) => {
serde_json::Value::Array(a.into_iter().map(Self::into_json_value).collect())
}
Self::Object(o) => serde_json::Value::Object(
o.into_iter().map(|(k, v)| (k, v.into_json_value())).collect(),
),
}
}
}
Loading