units: Add fmt traits for simple wrapper types
What changed, and why it matters
This commit adds new ways to print or display several simple numeric wrapper types in the rust-bitcoin library, such as block heights, amounts, and timestamps. It does not change any security-sensitive logic, parsing, arithmetic, or network behavior. It is a routine API enhancement with no apparent security relevance.
No security action required. Review as a normal API change.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch introduces a macro impl_fmt_traits_for_u32_wrapper! that implements LowerHex, UpperHex, Octal, and Binary for integer newtypes in units. It wires these traits into Amount, SignedAmount, BlockHeight, BlockHeightInterval, BlockMtp, BlockMtpInterval, BlockTime, Sequence, Weight, absolute::Height, absolute::MedianTimePast, relative::NumberOfBlocks, and relative::NumberOf512Seconds. FeeRate is explicitly excluded. Existing Display implementations are preserved, and Sequence has its manual LowerHex/UpperHex implementations replaced by the macro. The change is purely additive formatting surface area.
Changed components
units/src/amount/signed.rsunits/src/amount/unsigned.rsunits/src/block.rsunits/src/fee_rate/mod.rsunits/src/internal_macros.rsunits/src/locktime/absolute/mod.rsunits/src/locktime/relative/mod.rsunits/src/sequence.rsunits/src/time.rsunits/src/weight.rsunits/tests/api.rsInspect captured patch +164 / −11
diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs
index 337dbb3b..8c4e8813 100644
--- a/units/src/amount/signed.rs
+++ b/units/src/amount/signed.rs
@@ -472,6 +472,8 @@ impl SignedAmount {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(SignedAmount, to_sat);
+
impl default::Default for SignedAmount {
fn default() -> Self { Self::ZERO }
}
diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs
index 114df54f..5035fa5c 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -508,6 +508,8 @@ impl Amount {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(Amount, to_sat);
+
impl default::Default for Amount {
fn default() -> Self { Self::ZERO }
}
diff --git a/units/src/block.rs b/units/src/block.rs
index aa81f50f..623f8004 100644
--- a/units/src/block.rs
+++ b/units/src/block.rs
@@ -143,6 +143,8 @@ impl BlockHeight {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(BlockHeight);
+
impl From<absolute::Height> for BlockHeight {
/// Converts a [`locktime::absolute::Height`] to a [`BlockHeight`].
///
@@ -280,6 +282,8 @@ impl BlockHeightInterval {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(BlockHeightInterval);
+
impl From<relative::NumberOfBlocks> for BlockHeightInterval {
/// Converts a [`locktime::relative::NumberOfBlocks`] to a [`BlockHeightInterval`].
///
@@ -355,6 +359,8 @@ impl BlockMtp {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(BlockMtp);
+
impl From<absolute::MedianTimePast> for BlockMtp {
/// Converts a [`locktime::absolute::MedianTimePast`] to a [`BlockMtp`].
///
@@ -445,6 +451,8 @@ impl BlockMtpInterval {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(BlockMtpInterval);
+
impl From<relative::NumberOf512Seconds> for BlockMtpInterval {
/// Converts a [`locktime::relative::NumberOf512Seconds`] to a [`BlockMtpInterval `].
///
diff --git a/units/src/fee_rate/mod.rs b/units/src/fee_rate/mod.rs
index 7f4b71a3..5b1efd5f 100644
--- a/units/src/fee_rate/mod.rs
+++ b/units/src/fee_rate/mod.rs
@@ -20,6 +20,10 @@ mod encapsulate {
///
/// This is an integer newtype representing fee rate. It provides protection
/// against mixing up the types, conversion functions, and basic formatting.
+ ///
+ /// NOTE: `FeeRate` explicitly does not have any format/display trait implementations, as it
+ /// doesn't have a standard unit for measure. Users are expected to format it on their own by
+ /// extracting values in desired units with `from_sat_per*` functions.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct FeeRate(u64);
diff --git a/units/src/internal_macros.rs b/units/src/internal_macros.rs
index ac88c218..3d25ac9c 100644
--- a/units/src/internal_macros.rs
+++ b/units/src/internal_macros.rs
@@ -178,3 +178,61 @@ macro_rules! impl_div_assign {
};
}
pub(crate) use impl_div_assign;
+
+/// Implements Lower/UpperHex, Octal and Binary for a new-type `$ty`.
+///
+/// This macro can be used on raw new-types (e.g. `BlockHeight`), or those encapsulated
+/// per the privacy rules by accessing the inner value with a method `$fn`.
+macro_rules! impl_fmt_traits_for_u32_wrapper {
+ ($ty:ident) => {
+ impl core::fmt::LowerHex for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::LowerHex::fmt(&self.0, f)
+ }
+ }
+
+ impl core::fmt::UpperHex for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::UpperHex::fmt(&self.0, f)
+ }
+ }
+
+ impl core::fmt::Octal for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::Octal::fmt(&self.0, f)
+ }
+ }
+
+ impl core::fmt::Binary for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::Binary::fmt(&self.0, f)
+ }
+ }
+ };
+ ($ty:ident, $fn:ident) => {
+ impl core::fmt::LowerHex for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::LowerHex::fmt(&self.$fn(), f)
+ }
+ }
+
+ impl core::fmt::UpperHex for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::UpperHex::fmt(&self.$fn(), f)
+ }
+ }
+
+ impl core::fmt::Octal for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::Octal::fmt(&self.$fn(), f)
+ }
+ }
+
+ impl core::fmt::Binary for $ty {
+ fn fmt(&self, f: &mut core::fmt::Formatter) -> Result<(), core::fmt::Error> {
+ core::fmt::Binary::fmt(&self.$fn(), f)
+ }
+ }
+ };
+}
+pub(crate) use impl_fmt_traits_for_u32_wrapper;
diff --git a/units/src/locktime/absolute/mod.rs b/units/src/locktime/absolute/mod.rs
index 360ccdf9..084883a1 100644
--- a/units/src/locktime/absolute/mod.rs
+++ b/units/src/locktime/absolute/mod.rs
@@ -591,6 +591,8 @@ impl Height {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(Height);
+
impl fmt::Display for Height {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
}
@@ -706,6 +708,8 @@ impl MedianTimePast {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(MedianTimePast);
+
impl fmt::Display for MedianTimePast {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
}
diff --git a/units/src/locktime/relative/mod.rs b/units/src/locktime/relative/mod.rs
index c55ab802..f1ffa79b 100644
--- a/units/src/locktime/relative/mod.rs
+++ b/units/src/locktime/relative/mod.rs
@@ -454,6 +454,8 @@ impl NumberOfBlocks {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(NumberOfBlocks);
+
impl From<u16> for NumberOfBlocks {
#[inline]
fn from(value: u16) -> Self { Self(value) }
@@ -573,6 +575,8 @@ impl NumberOf512Seconds {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(NumberOf512Seconds);
+
parse_int::impl_parse_str_from_int_infallible!(NumberOf512Seconds, u16, from_512_second_intervals);
impl fmt::Display for NumberOf512Seconds {
diff --git a/units/src/sequence.rs b/units/src/sequence.rs
index 0e1839ea..ba73189d 100644
--- a/units/src/sequence.rs
+++ b/units/src/sequence.rs
@@ -229,6 +229,8 @@ impl Sequence {
const fn low_u16(self) -> u16 { self.0 as u16 }
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(Sequence);
+
impl Default for Sequence {
/// The default value of sequence is 0xffffffff.
#[inline]
@@ -245,16 +247,6 @@ impl fmt::Display for Sequence {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
}
-impl fmt::LowerHex for Sequence {
- #[inline]
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::LowerHex::fmt(&self.0, f) }
-}
-
-impl fmt::UpperHex for Sequence {
- #[inline]
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::UpperHex::fmt(&self.0, f) }
-}
-
impl fmt::Debug for Sequence {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
diff --git a/units/src/time.rs b/units/src/time.rs
index 1d3a0ffb..71a68dd8 100644
--- a/units/src/time.rs
+++ b/units/src/time.rs
@@ -9,7 +9,6 @@
#[cfg(feature = "encoding")]
use core::convert::Infallible;
-#[cfg(feature = "encoding")]
use core::fmt;
#[cfg(feature = "arbitrary")]
@@ -48,6 +47,12 @@ mod encapsulate {
#[doc(inline)]
pub use encapsulate::BlockTime;
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(BlockTime, to_u32);
+
+impl fmt::Display for BlockTime {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.to_u32(), f) }
+}
+
impl From<u32> for BlockTime {
#[inline]
fn from(t: u32) -> Self { Self::from_u32(t) }
@@ -170,6 +175,9 @@ impl<'a> Arbitrary<'a> for BlockTime {
#[cfg(test)]
mod tests {
+ #[cfg(feature = "alloc")]
+ use alloc::string::ToString;
+
#[cfg(feature = "encoding")]
use encoding::Decoder as _;
#[cfg(all(feature = "encoding", feature = "alloc"))]
@@ -206,4 +214,23 @@ mod tests {
let error = decoder.end().unwrap_err();
assert!(matches!(error, BlockTimeDecoderError(UnexpectedEofError { .. })));
}
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ fn time_module_display() {
+ assert_eq!(BlockTime::from(1_765_364_400).to_string(), "1765364400");
+ }
+
+ #[test]
+ #[cfg(feature = "alloc")]
+ #[cfg(feature = "encoding")]
+ fn time_module_error_display() {
+ // BlockTimeDecoderError
+ let bytes = [0xb0, 0x52, 0x39]; // 3 bytes is an EOF error
+
+ let mut decoder = BlockTimeDecoder::default();
+ assert!(decoder.push_bytes(&mut bytes.as_slice()).unwrap());
+
+ assert_ne!(decoder.end().unwrap_err().to_string(), "");
+ }
}
diff --git a/units/src/weight.rs b/units/src/weight.rs
index b2d27e12..8fc6f4bc 100644
--- a/units/src/weight.rs
+++ b/units/src/weight.rs
@@ -179,6 +179,8 @@ impl Weight {
}
}
+crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(Weight, to_wu);
+
/// Alternative will display the unit.
impl fmt::Display for Weight {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
diff --git a/units/tests/api.rs b/units/tests/api.rs
index 6c5ddae4..9244e47c 100644
--- a/units/tests/api.rs
+++ b/units/tests/api.rs
@@ -336,6 +336,56 @@ fn api_all_non_error_types_have_non_empty_debug() {
assert!(!debug.is_empty());
}
+macro_rules! assert_format_matches {
+ ($type:expr, $num:expr) => {
+ let got = format!("{:o}", $type);
+ let want = format!("{:o}", $num);
+ assert_eq!(got, want);
+
+ let got = format!("{:b}", $type);
+ let want = format!("{:b}", $num);
+ assert_eq!(got, want);
+
+ let got = format!("{:x}", $type);
+ let want = format!("{:x}", $num);
+ assert_eq!(got, want);
+
+ let got = format!("{:X}", $type);
+ let want = format!("{:X}", $num);
+ assert_eq!(got, want);
+ };
+}
+#[test]
+fn api_all_wrapper_types_fmt_as_inner() {
+ // Confirm that for a set of pseudo-random numbers, formatting is equivalent to the inner value
+ let mut rand_num = 10;
+ for _ in 0..50 {
+ assert_format_matches!(Amount::from_sat_u32(rand_num), rand_num);
+ assert_format_matches!(BlockHeight::from(rand_num), rand_num);
+ assert_format_matches!(BlockHeightInterval::from(rand_num), rand_num);
+ assert_format_matches!(BlockMtp::from(rand_num), rand_num);
+ assert_format_matches!(BlockMtpInterval::from(rand_num), rand_num);
+ assert_format_matches!(BlockTime::from(rand_num), rand_num);
+ assert_format_matches!(relative::NumberOfBlocks::from_height(rand_num as u16), rand_num as u16);
+ assert_format_matches!(relative::NumberOf512Seconds::from_512_second_intervals(rand_num as u16), rand_num as u16);
+ assert_format_matches!(Sequence::from_consensus(rand_num), rand_num);
+ assert_format_matches!(Weight::from_wu(rand_num.into()), u64::from(rand_num));
+
+ if let Ok(height) = absolute::Height::from_u32(rand_num) {
+ assert_format_matches!(height, rand_num);
+ }
+ if let Ok(mtp) = absolute::MedianTimePast::from_u32(rand_num) {
+ assert_format_matches!(mtp, rand_num);
+ }
+ if let Ok(ssat) = SignedAmount::from_sat(i64::from(rand_num)) {
+ assert_format_matches!(ssat, rand_num);
+ assert_format_matches!(-ssat, -i64::from(rand_num));
+ }
+
+ rand_num = rand_num.wrapping_mul(1039).wrapping_add(677);
+ }
+}
+
#[test]
fn all_types_implement_send_sync() {
fn assert_send<T: Send>() {}
Why this scored 19/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.