units: Add hex parsing for Amount and SignedAmount
What changed, and why it matters
This commit adds new public functions that let users create Bitcoin amount values from hexadecimal strings. It is a feature addition, not a fix for a known bug or vulnerability. There is no evidence in the commit or supplied references that this change addresses a security issue.
No security action required. Treat as a normal feature addition. If consuming these new APIs, validate that callers handle ParseAmountError correctly and do not assume hex-parsed amounts are always valid.
Security signals we found
No security-relevant signals present in the diff
Feature addition: new hex parsing constructors
Existing range and parse errors reused
Evidence from the diff
The patch extends ParseAmountError with PrefixedHexError and UnprefixedHexError variants and adds from_sat_hex / from_sat_unprefixed_hex constructors to Amount and SignedAmount. These wrap existing parse_int::hex_u64_* helpers and enforce the same MAX_MONEY / i64 range checks already used elsewhere. No unsafe code, no changed behavior of existing APIs, and no security-relevant bug is corrected.
Changed components
units/src/amount/error.rsunits/src/amount/signed.rsunits/src/amount/unsigned.rsInspect captured patch +85 / −0
diff --git a/units/src/amount/error.rs b/units/src/amount/error.rs
index d0b5c4be..1b5f75b8 100644
--- a/units/src/amount/error.rs
+++ b/units/src/amount/error.rs
@@ -8,6 +8,8 @@ use core::fmt;
use internals::error::InputString;
use internals::write_err;
+use crate::parse_int::{PrefixedHexError, UnprefixedHexError};
+
use super::INPUT_STRING_LEN_LIMIT;
/// Error returned when parsing an amount with denomination fails.
@@ -106,6 +108,10 @@ pub(crate) enum ParseAmountErrorInner {
InvalidCharacter(InvalidCharacterError),
/// A valid character is in an invalid position.
BadPosition(BadPositionError),
+ /// An error parsing a prefixed hex amount.
+ PrefixedHex(PrefixedHexError),
+ /// An error parsing an unprefixed hex amount.
+ UnprefixedHex(UnprefixedHexError),
}
impl From<TooPreciseError> for ParseAmountError {
@@ -130,6 +136,14 @@ impl From<BadPositionError> for ParseAmountError {
fn from(value: BadPositionError) -> Self { Self(ParseAmountErrorInner::BadPosition(value)) }
}
+impl From<PrefixedHexError> for ParseAmountError {
+ fn from(value: PrefixedHexError) -> Self { Self(ParseAmountErrorInner::PrefixedHex(value)) }
+}
+
+impl From<UnprefixedHexError> for ParseAmountError {
+ fn from(value: UnprefixedHexError) -> Self { Self(ParseAmountErrorInner::UnprefixedHex(value)) }
+}
+
impl From<Infallible> for ParseAmountError {
fn from(never: Infallible) -> Self { match never {} }
}
@@ -151,6 +165,8 @@ impl fmt::Display for ParseAmountError {
write_err!(f, "invalid character in the input"; error)
}
E::BadPosition(ref error) => write_err!(f, "valid character in bad position"; error),
+ E::PrefixedHex(ref error) => write_err!(f, "prefixed hex is invalid"; error),
+ E::UnprefixedHex(ref error) => write_err!(f, "unprefixed hex is invalid"; error),
}
}
}
@@ -167,6 +183,8 @@ impl std::error::Error for ParseAmountError {
E::MissingDigits(ref error) => Some(error),
E::InvalidCharacter(ref error) => Some(error),
E::BadPosition(ref error) => Some(error),
+ E::PrefixedHex(ref error) => Some(error),
+ E::UnprefixedHex(ref error) => Some(error),
}
}
}
diff --git a/units/src/amount/signed.rs b/units/src/amount/signed.rs
index 8c4e8813..57317ba9 100644
--- a/units/src/amount/signed.rs
+++ b/units/src/amount/signed.rs
@@ -10,6 +10,7 @@ use core::{default, fmt};
#[cfg(feature = "arbitrary")]
use arbitrary::{Arbitrary, Unstructured};
+use crate::parse_int;
use super::error::ParseErrorInner;
use super::{
parse_signed_to_satoshi, split_amount_and_denomination, Amount, Denomination, Display,
@@ -122,6 +123,19 @@ impl SignedAmount {
}
}
+ /// Construct a [`SignedAmount`] value from a `u64` satoshi value.
+ ///
+ /// # Errors:
+ ///
+ /// Returns an [`OutOfRangeError`] if the satoshi value > [`Self::MAX_MONEY`].
+ #[allow(clippy::missing_panics_doc)]
+ fn from_sat_u64(satoshi: u64) -> Result<Self, ParseAmountError> {
+ // u64 -> i64 only fails if value is greater than i64::MAX, which is also > Self::MAX_MONEY.
+ let amount = i64::try_from(satoshi)
+ .map_err(|_| OutOfRangeError { is_signed: true, is_greater_than_max: true })?;
+ Ok(Self::from_sat(amount)?)
+ }
+
/// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
///
/// # Errors
@@ -216,6 +230,34 @@ impl SignedAmount {
self.to_string_in(denom).parse::<f64>().unwrap()
}
+ /// Constructs a new `SignedAmount` from a prefixed hex string.
+ ///
+ /// This can only parse an unsigned quantity.
+ ///
+ /// # Errors
+ ///
+ /// If the input string is not a valid hex representation of an amount in sats or it does not
+ /// include the `0x` prefix.
+ #[inline]
+ pub fn from_sat_hex(s: &str) -> Result<Self, ParseAmountError> {
+ let amount = parse_int::hex_u64_prefixed(s)?;
+ Self::from_sat_u64(amount)
+ }
+
+ /// Constructs a new `SignedAmount` from an unprefixed hex string.
+ ///
+ /// This can only parse an unsigned quantity.
+ ///
+ /// # Errors
+ ///
+ /// If the input string is not a valid hex representation of an amount in sats or if it
+ /// includes the `0x` prefix.
+ #[inline]
+ pub fn from_sat_unprefixed_hex(s: &str) -> Result<Self, ParseAmountError> {
+ let amount = parse_int::hex_u64_unprefixed(s)?;
+ Self::from_sat_u64(amount)
+ }
+
/// Expresses this [`SignedAmount`] as a floating-point value in Bitcoin.
///
/// Please be aware of the risk of using floating-point numbers.
diff --git a/units/src/amount/unsigned.rs b/units/src/amount/unsigned.rs
index 5035fa5c..36d814df 100644
--- a/units/src/amount/unsigned.rs
+++ b/units/src/amount/unsigned.rs
@@ -19,6 +19,7 @@ use super::{
parse_signed_to_satoshi, split_amount_and_denomination, Denomination, Display, DisplayStyle,
OutOfRangeError, ParseAmountError, ParseError, SignedAmount,
};
+use crate::parse_int;
use crate::result::{MathOp, NumOpError as E, NumOpResult};
use crate::{FeeRate, Weight};
@@ -259,6 +260,30 @@ impl Amount {
Self::from_str_in(&value.to_string(), denom)
}
+ /// Constructs a new `Amount` from a prefixed hex string.
+ ///
+ /// # Errors
+ ///
+ /// If the input string is not a valid hex representation of an amount in sats or it does not
+ /// include the `0x` prefix.
+ #[inline]
+ pub fn from_sat_hex(s: &str) -> Result<Self, ParseAmountError> {
+ let amount = parse_int::hex_u64_prefixed(s)?;
+ Ok(Self::from_sat(amount)?)
+ }
+
+ /// Constructs a new `Amount` from an unprefixed hex string.
+ ///
+ /// # Errors
+ ///
+ /// If the input string is not a valid hex representation of an amount in sats or if it
+ /// includes the `0x` prefix.
+ #[inline]
+ pub fn from_sat_unprefixed_hex(s: &str) -> Result<Self, ParseAmountError> {
+ let amount = parse_int::hex_u64_unprefixed(s)?;
+ Ok(Self::from_sat(amount)?)
+ }
+
/// Constructs a new object that implements [`fmt::Display`] in the given [`Denomination`].
///
/// This function is useful if you do not wish to allocate. See also [`Self::to_string_in`].
Why this scored 12/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.