1use std::fmt;
21
22use borsh::{BorshDeserialize, BorshSerialize};
23use serde::{Deserialize, Serialize};
24
25#[derive(
27 BorshSerialize,
28 BorshDeserialize,
29 Clone,
30 Copy,
31 Debug,
32 Default,
33 Eq,
34 Hash,
35 Ord,
36 PartialEq,
37 PartialOrd,
38 Serialize,
39 Deserialize,
40)]
41#[serde(transparent)]
42#[repr(transparent)]
43pub struct BlockHeight(u64);
44
45impl BlockHeight {
46 pub const ZERO: Self = Self(0);
48 pub const MAX: Self = Self(u64::MAX);
50
51 pub const BORSH_SERIALIZED_SIZE: usize = size_of::<u64>();
53
54 pub const fn new(height: u64) -> Self {
56 Self(height)
57 }
58
59 pub const fn as_u64(self) -> u64 {
61 self.0
62 }
63
64 pub const fn to_le_bytes(self) -> [u8; 8] {
66 self.0.to_le_bytes()
67 }
68
69 pub const fn from_le_bytes(bytes: [u8; 8]) -> Self {
71 Self(u64::from_le_bytes(bytes))
72 }
73
74 pub const fn saturating_add(self, n: u64) -> Self {
76 Self(self.0.saturating_add(n))
77 }
78
79 pub const fn saturating_sub(self, n: u64) -> Self {
81 Self(self.0.saturating_sub(n))
82 }
83}
84
85impl fmt::Display for BlockHeight {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 fmt::Display::fmt(&self.0, f)
90 }
91}
92
93impl std::str::FromStr for BlockHeight {
94 type Err = std::num::ParseIntError;
95 fn from_str(s: &str) -> Result<Self, Self::Err> {
96 s.parse::<u64>().map(Self)
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn new_as_u64_roundtrip() {
106 assert_eq!(BlockHeight::new(42).as_u64(), 42);
107 assert_eq!(BlockHeight::new(7).as_u64(), 7);
108 }
109
110 #[test]
111 fn saturating_arithmetic_clamps() {
112 assert_eq!(BlockHeight::MAX.saturating_add(1), BlockHeight::MAX);
113 assert_eq!(BlockHeight::ZERO.saturating_sub(1), BlockHeight::ZERO);
114 assert_eq!(BlockHeight::new(10).saturating_add(5), BlockHeight::new(15));
115 }
116
117 #[test]
121 fn display_forwards_format_flags() {
122 assert_eq!(format!("{}", BlockHeight::new(42)), "42");
123 assert_eq!(format!("{:012}", BlockHeight::new(42)), "000000000042");
124 }
125
126 #[test]
127 fn serde_is_transparent() {
128 assert_eq!(serde_json::to_string(&BlockHeight::new(9)).unwrap(), "9");
129 assert_eq!(
130 serde_json::from_str::<BlockHeight>("9").unwrap(),
131 BlockHeight::new(9)
132 );
133 }
134
135 #[test]
136 fn borsh_roundtrips_like_u64() {
137 let h = BlockHeight::new(123);
138 let bytes = borsh::to_vec(&h).unwrap();
139 assert_eq!(bytes, 123u64.to_le_bytes());
140 assert_eq!(borsh::from_slice::<BlockHeight>(&bytes).unwrap(), h);
141 }
142
143 #[test]
147 fn bincode_matches_u64() {
148 let raw: u64 = 0xdead_beef;
149 let h = BlockHeight::new(raw);
150 assert_eq!(
151 bincode::serialize(&h).unwrap(),
152 bincode::serialize(&raw).unwrap()
153 );
154 assert_eq!(
155 bincode::deserialize::<BlockHeight>(&bincode::serialize(&raw).unwrap()).unwrap(),
156 h
157 );
158 }
159
160 #[test]
166 fn to_le_bytes_matches_u64() {
167 let raw: u64 = 0xdead_beef;
168 let h = BlockHeight::new(raw);
169 assert_eq!(h.to_le_bytes(), raw.to_le_bytes());
170 assert_eq!(BlockHeight::from_le_bytes(h.to_le_bytes()), h);
171 }
172
173 #[test]
178 fn bcs_matches_u64() {
179 let raw: u64 = 0xdead_beef;
180 let h = BlockHeight::new(raw);
181 assert_eq!(bcs::to_bytes(&h).unwrap(), bcs::to_bytes(&raw).unwrap());
182 assert_eq!(
183 bcs::from_bytes::<BlockHeight>(&bcs::to_bytes(&raw).unwrap()).unwrap(),
184 h
185 );
186
187 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
188 struct WrappedHeight {
189 height: BlockHeight,
190 }
191 #[derive(serde::Serialize)]
192 struct WrappedRaw {
193 height: u64,
194 }
195 let wrapped = WrappedHeight { height: h };
196 assert_eq!(
197 bcs::to_bytes(&wrapped).unwrap(),
198 bcs::to_bytes(&WrappedRaw { height: raw }).unwrap()
199 );
200 assert_eq!(
201 bcs::from_bytes::<WrappedHeight>(&bcs::to_bytes(&wrapped).unwrap()).unwrap(),
202 wrapped
203 );
204 }
205}