use std::fmt;
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
#[derive(
BorshSerialize,
BorshDeserialize,
Clone,
Copy,
Debug,
Default,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
Serialize,
Deserialize,
)]
#[serde(transparent)]
#[repr(transparent)]
pub struct BlockHeight(u64);
impl BlockHeight {
pub const ZERO: Self = Self(0);
pub const MAX: Self = Self(u64::MAX);
pub const BORSH_SERIALIZED_SIZE: usize = size_of::<u64>();
pub const fn new(height: u64) -> Self {
Self(height)
}
pub const fn as_u64(self) -> u64 {
self.0
}
pub const fn to_le_bytes(self) -> [u8; 8] {
self.0.to_le_bytes()
}
pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
Self(u64::from_le_bytes(bytes))
}
pub const fn saturating_add(self, n: u64) -> Self {
Self(self.0.saturating_add(n))
}
pub const fn saturating_sub(self, n: u64) -> Self {
Self(self.0.saturating_sub(n))
}
}
impl fmt::Display for BlockHeight {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl std::str::FromStr for BlockHeight {
type Err = std::num::ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse::<u64>().map(Self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_as_u64_roundtrip() {
assert_eq!(BlockHeight::new(42).as_u64(), 42);
assert_eq!(BlockHeight::new(7).as_u64(), 7);
}
#[test]
fn saturating_arithmetic_clamps() {
assert_eq!(BlockHeight::MAX.saturating_add(1), BlockHeight::MAX);
assert_eq!(BlockHeight::ZERO.saturating_sub(1), BlockHeight::ZERO);
assert_eq!(BlockHeight::new(10).saturating_add(5), BlockHeight::new(15));
}
#[test]
fn display_forwards_format_flags() {
assert_eq!(format!("{}", BlockHeight::new(42)), "42");
assert_eq!(format!("{:012}", BlockHeight::new(42)), "000000000042");
}
#[test]
fn serde_is_transparent() {
assert_eq!(serde_json::to_string(&BlockHeight::new(9)).unwrap(), "9");
assert_eq!(
serde_json::from_str::<BlockHeight>("9").unwrap(),
BlockHeight::new(9)
);
}
#[test]
fn borsh_roundtrips_like_u64() {
let h = BlockHeight::new(123);
let bytes = borsh::to_vec(&h).unwrap();
assert_eq!(bytes, 123u64.to_le_bytes());
assert_eq!(borsh::from_slice::<BlockHeight>(&bytes).unwrap(), h);
}
#[test]
fn bincode_matches_u64() {
let raw: u64 = 0xdead_beef;
let h = BlockHeight::new(raw);
assert_eq!(
bincode::serialize(&h).unwrap(),
bincode::serialize(&raw).unwrap()
);
assert_eq!(
bincode::deserialize::<BlockHeight>(&bincode::serialize(&raw).unwrap()).unwrap(),
h
);
}
#[test]
fn to_le_bytes_matches_u64() {
let raw: u64 = 0xdead_beef;
let h = BlockHeight::new(raw);
assert_eq!(h.to_le_bytes(), raw.to_le_bytes());
assert_eq!(BlockHeight::from_le_bytes(h.to_le_bytes()), h);
}
#[test]
fn bcs_matches_u64() {
let raw: u64 = 0xdead_beef;
let h = BlockHeight::new(raw);
assert_eq!(bcs::to_bytes(&h).unwrap(), bcs::to_bytes(&raw).unwrap());
assert_eq!(
bcs::from_bytes::<BlockHeight>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
h
);
#[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
struct WrappedHeight {
height: BlockHeight,
}
#[derive(serde::Serialize)]
struct WrappedRaw {
height: u64,
}
let wrapped = WrappedHeight { height: h };
assert_eq!(
bcs::to_bytes(&wrapped).unwrap(),
bcs::to_bytes(&WrappedRaw { height: raw }).unwrap()
);
assert_eq!(
bcs::from_bytes::<WrappedHeight>(&bcs::to_bytes(&wrapped).unwrap()).unwrap(),
wrapped
);
}
}