use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct HashHex(
pub [u8; 32],
);
impl HashHex {
pub const fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
}
impl fmt::Display for HashHex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
}
}
impl FromStr for HashHex {
type Err = HexParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = parse_hex_fixed::<32>(s)?;
Ok(Self(bytes))
}
}
impl Serialize for HashHex {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for HashHex {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
String::deserialize(d)?
.parse()
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct PubkeyHex(
pub [u8; 48],
);
impl PubkeyHex {
pub const fn new(bytes: [u8; 48]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 48] {
&self.0
}
}
impl fmt::Display for PubkeyHex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
}
}
impl FromStr for PubkeyHex {
type Err = HexParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = parse_hex_fixed::<48>(s)?;
Ok(Self(bytes))
}
}
impl Serialize for PubkeyHex {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for PubkeyHex {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
String::deserialize(d)?
.parse()
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SignatureHex(
pub [u8; 96],
);
impl SignatureHex {
pub const fn new(bytes: [u8; 96]) -> Self {
Self(bytes)
}
pub fn as_bytes(&self) -> &[u8; 96] {
&self.0
}
}
impl fmt::Display for SignatureHex {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "0x{}", hex::encode(self.0))
}
}
impl FromStr for SignatureHex {
type Err = HexParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let bytes = parse_hex_fixed::<96>(s)?;
Ok(Self(bytes))
}
}
impl Serialize for SignatureHex {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(self)
}
}
impl<'de> Deserialize<'de> for SignatureHex {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
String::deserialize(d)?
.parse()
.map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct Amount(
pub u64,
);
impl Amount {
pub const ZERO: Self = Self(0);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BlockSummary {
pub height: u64,
pub hash: HashHex,
pub parent_hash: HashHex,
pub timestamp: u64,
pub proposer: PubkeyHex,
pub tx_count: u32,
pub weight: u64,
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ValidatorStatus {
PendingRegister,
Active,
ExitingVoluntary,
ExitingForced,
Exited,
WithdrawalPending,
Withdrawn,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidatorSummary {
pub pubkey: PubkeyHex,
pub status: ValidatorStatus,
pub validator_index: Option<u32>,
pub effective_balance: Amount,
pub slashed_amount: Amount,
pub activation_epoch: Option<u64>,
pub exit_epoch: Option<u64>,
}
#[derive(Debug, thiserror::Error)]
pub enum HexParseError {
#[error(
"wrong hex length: expected {expected} bytes ({expected_hex_chars} hex chars), got {got}"
)]
WrongLength {
expected: usize,
expected_hex_chars: usize,
got: usize,
},
#[error("invalid hex: {0}")]
InvalidHex(#[from] hex::FromHexError),
}
fn parse_hex_fixed<const N: usize>(s: &str) -> Result<[u8; N], HexParseError> {
let stripped = s
.strip_prefix("0x")
.or_else(|| s.strip_prefix("0X"))
.unwrap_or(s);
if stripped.len() != N * 2 {
return Err(HexParseError::WrongLength {
expected: N,
expected_hex_chars: N * 2,
got: stripped.len(),
});
}
let mut out = [0u8; N];
hex::decode_to_slice(stripped, &mut out)?;
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_hex_display() {
let h = HashHex::new([0xAB; 32]);
let s = h.to_string();
assert_eq!(s.len(), 2 + 64);
assert!(s.starts_with("0x"));
assert_eq!(s, format!("0x{}", "ab".repeat(32)));
}
#[test]
fn hash_hex_parse_accepts_case_and_prefix_variations() {
let want = HashHex::new([0xAB; 32]);
let inputs = [
format!("0x{}", "ab".repeat(32)),
format!("0X{}", "AB".repeat(32)),
"ab".repeat(32),
"AB".repeat(32),
];
for s in inputs {
let parsed: HashHex = s.parse().expect(&s);
assert_eq!(parsed, want, "parse of {s:?} mismatched");
}
}
#[test]
fn hash_hex_parse_rejects_wrong_length() {
let short = format!("0x{}", "a".repeat(62));
let err = short.parse::<HashHex>().unwrap_err();
assert!(matches!(err, HexParseError::WrongLength { .. }));
}
#[test]
fn fixed_hex_types_roundtrip_via_serde() {
let h = HashHex::new([1u8; 32]);
let j = serde_json::to_string(&h).unwrap();
let back: HashHex = serde_json::from_str(&j).unwrap();
assert_eq!(h, back);
let p = PubkeyHex::new([2u8; 48]);
let j = serde_json::to_string(&p).unwrap();
let back: PubkeyHex = serde_json::from_str(&j).unwrap();
assert_eq!(p, back);
let s = SignatureHex::new([3u8; 96]);
let j = serde_json::to_string(&s).unwrap();
let back: SignatureHex = serde_json::from_str(&j).unwrap();
assert_eq!(s, back);
}
#[test]
fn amount_transparent_serde() {
let a = Amount(42);
let s = serde_json::to_string(&a).unwrap();
assert_eq!(s, "42");
}
#[test]
fn validator_status_serialises_snake_case() {
let s = serde_json::to_string(&ValidatorStatus::PendingRegister).unwrap();
assert_eq!(s, "\"pending_register\"");
let s = serde_json::to_string(&ValidatorStatus::WithdrawalPending).unwrap();
assert_eq!(s, "\"withdrawal_pending\"");
}
#[test]
fn summaries_roundtrip() {
let b = BlockSummary {
height: 123,
hash: HashHex::new([1u8; 32]),
parent_hash: HashHex::new([0u8; 32]),
timestamp: 1_700_000_000,
proposer: PubkeyHex::new([9u8; 48]),
tx_count: 7,
weight: 10_000,
};
let j = serde_json::to_string(&b).unwrap();
let back: BlockSummary = serde_json::from_str(&j).unwrap();
assert_eq!(b.height, back.height);
assert_eq!(b.hash, back.hash);
let v = ValidatorSummary {
pubkey: PubkeyHex::new([5u8; 48]),
status: ValidatorStatus::Active,
validator_index: Some(42),
effective_balance: Amount(32_000_000_000_000),
slashed_amount: Amount::ZERO,
activation_epoch: Some(7),
exit_epoch: None,
};
let j = serde_json::to_string(&v).unwrap();
let back: ValidatorSummary = serde_json::from_str(&j).unwrap();
assert_eq!(back.pubkey, v.pubkey);
assert_eq!(back.status, v.status);
}
}