Repository: rust-bitcoin/rust-bitcoin
Pull request #2604 (2024-03-16 13:16:48)

Enforce displaying Amount with trailing zeros
It is common to display bitcoins using trailing zeros upto 8 decimals. This commit enforces:
- Displaying Amount in BTC with trailing zeroes by eight decimal places if a precision on the Amount is not specified.
- Displaying Amount in BTC upto the precision specified truncating the insignificant zeros.
- Displaying amount in BTC without any decimals if the remainder of the amount divided by the satoshis in 1 BTC is equal to zero using formula `satoshis.rem_euclid(Amount::ONE_BTC.to_sat()) != 0`

These are not breaking changes and all previous tests pass.

A testcase is added to for changes introduced.

Resolves: #2136

coveralls: ## Pull Request Test Coverage Report for [Build 8308063189](https://coveralls.io/builds/66331180)


### Details

* **39** of **39**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage increased (+**0.03%**) to **83.928%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/66331180/badge)](https://coveralls.io/builds/66331180) |
| :-- | --: |
| Change from base [Build 8307227520](https://coveralls.io/builds/66330762): |  0.03% |
| Covered Lines: | 19734 |
| Relevant Lines: | 23513 |

---
##### 💛  - [Coveralls](https://coveralls.io)

apoelstra: The CI failure is real -- you're introducing a dependence on `alloc` in the `fmt` method that didn't used to be there.

In principle we shouldn't need one.

Interestingly my local CI passes -- so we've found a blind spot in it! Let me investigate..
apoelstra: Oh, lol, the blind spot is the entire `units` crate. Oops. This may also explain another "how did this get past CI?" situation last week..
sanket1729: I forgot what our consensus was about respecting `FromStr` and `Display` invariant? Do we make an effort to maintain it?
448-OG: > The CI failure is real -- you're introducing a dependence on `alloc` in the `fmt` method that didn't used to be there.
> 
> In principle we shouldn't need one.
> 
> Interestingly my local CI passes -- so we've found a blind spot in it! Let me investigate..

Yeah! Realized it was an issue with allocation but I was not able to make changes yesterday. Should be fixed now.

My tests also passed locally. Not sure why
448-OG: What is the current policy on running rustfmt, my editor does auto format but when I do a diff I see so many changes related to formatting
448-OG: So I also need to fix it for tests, somehow they succeed locally
apoelstra: > What is the current policy on running rustfmt, my editor does auto format but when I do a diff I see so many changes related to formatting

Please do not commit unrelated changes. Usually there should not be very many because we have a weekly bot which does all the formatting changes, but lately we have been skipping the bot because one mantainer claims undue difficulty in rebasing.

So you will need to override your editor or be careful about what changes you commit, for now.

apoelstra: > I forgot what our consensus was about respecting `FromStr` and `Display` invariant? Do we make an effort to maintain it?

Yes, we "make an effort". But there are some exceptional cases, most notably `Script`, and I don't think we've worked out an explicit policy.
sanket1729: @448-OG, you can use `just format` that would internally use the nightly formatter. In your editor is vscode, you can configure project specific settings for rust-bitcoin and set the formatter as `just format`

Patch:
diff --git a/SECURITY.md b/SECURITY.md
index c299816e..2e161330 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -10,7 +10,7 @@ As a general rule, an issue is a security vulnerability if it could lead to:
 * Loss of funds
 * Loss of privacy
 * Censorship (including e.g. by attaching an incorrectly low fee to a transaction)
-* Any "ordinary" security problem, such as remote code execution or invalid memory access
+* Any "ordinary" security problem, such as remote code execution or invalid memory acccess
 
 In general, use your best judgement in determining whether an issue is a security issue. If not,
 go ahead and post it to the public issue tracker.
diff --git a/bitcoin/examples/ecdsa-psbt.rs b/bitcoin/examples/ecdsa-psbt.rs
index b5984c8e..fa5b068e 100644
--- a/bitcoin/examples/ecdsa-psbt.rs
+++ b/bitcoin/examples/ecdsa-psbt.rs
@@ -133,7 +133,7 @@ fn new<C: Signing>(secp: &Secp256k1<C>, xpriv: &str) -> Result<ExportData> {
     fn master_fingerprint(&self) -> Fingerprint { self.master_xpub.fingerprint() }
 
     /// Signs `psbt` with this signer.
-    fn sign_psbt<C: Signing + Verification>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> {
+    fn sign_psbt<C: Signing>(&self, secp: &Secp256k1<C>, mut psbt: Psbt) -> Result<Psbt> {
         match psbt.sign(&self.master_xpriv, secp) {
             Ok(keys) => assert_eq!(keys.len(), 1),
             Err((_, e)) => {
diff --git a/bitcoin/src/blockdata/constants.rs b/bitcoin/src/blockdata/constants.rs
index 1f8de58b..33e5b543 100644
--- a/bitcoin/src/blockdata/constants.rs
+++ b/bitcoin/src/blockdata/constants.rs
@@ -30,7 +30,7 @@
 pub const DIFFCHANGE_TIMESPAN: u32 = 14 * 24 * 3600;
 
 /// The factor that non-witness serialization data is multiplied by during weight calculation.
-pub const WITNESS_SCALE_FACTOR: usize = units::weight::WITNESS_SCALE_FACTOR;
+pub const WITNESS_SCALE_FACTOR: usize = 4;
 /// The maximum allowed number of signature check operations in a block.
 pub const MAX_BLOCK_SIGOPS_COST: i64 = 80_000;
 /// Mainnet (bitcoin) pubkey address prefix.
diff --git a/units/src/fee_rate.rs b/bitcoin/src/blockdata/fee_rate.rs
similarity index 82%
rename from units/src/fee_rate.rs
rename to bitcoin/src/blockdata/fee_rate.rs
index 9dcb88dd..6aaf83f9 100644
--- a/units/src/fee_rate.rs
+++ b/bitcoin/src/blockdata/fee_rate.rs
@@ -5,11 +5,8 @@
 use core::fmt;
 use core::ops::{Div, Mul};
 
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
 use crate::amount::Amount;
-use crate::weight::Weight;
+use crate::blockdata::weight::Weight;
 
 /// Represents fee rate.
 ///
@@ -17,6 +14,7 @@
 /// up the types as well as basic formatting features.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
 #[cfg_attr(feature = "serde", serde(transparent))]
 pub struct FeeRate(u64);
 
@@ -95,6 +93,18 @@ pub fn checked_mul_by_weight(self, rhs: Weight) -> Option<Amount> {
     /// if overflow occurred.
     ///
     /// This is equivalent to `Self::checked_mul_by_weight()`.
+    ///
+    /// # Examples
+    ///
+    /// ```
+    /// # use bitcoin::{absolute, transaction, FeeRate, Transaction};
+    /// # // Dummy transaction.
+    /// # let tx = Transaction { version: transaction::Version::ONE, lock_time: absolute::LockTime::ZERO, input: vec![], output: vec![] };
+    ///
+    /// let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/vbyte is valid");
+    /// let fee = rate.fee_wu(tx.weight()).unwrap();
+    /// assert_eq!(fee.to_sat(), tx.vsize() as u64);
+    /// ```
     pub fn fee_wu(self, weight: Weight) -> Option<Amount> { self.checked_mul_by_weight(weight) }
 
     /// Calculates fee by multiplying this fee rate by weight, in virtual bytes, returning `None`
@@ -143,10 +153,12 @@ impl Div<Weight> for Amount {
     fn div(self, rhs: Weight) -> Self::Output { FeeRate(self.to_sat() * 1000 / rhs.to_wu()) }
 }
 
-crate::impl_parse_str_from_int_infallible!(FeeRate, u64, from_sat_per_kwu);
+crate::parse::impl_parse_str_from_int_infallible!(FeeRate, u64, from_sat_per_kwu);
 
 #[cfg(test)]
 mod tests {
+    use std::u64;
+
     use super::*;
 
     #[test]
@@ -224,4 +236,21 @@ fn checked_div_test() {
         let fee_rate = FeeRate(10).checked_div(0);
         assert!(fee_rate.is_none());
     }
+
+    #[test]
+    fn fee_convenience_functions_agree() {
+        use hex::test_hex_unwrap as hex;
+
+        use crate::blockdata::transaction::Transaction;
+        use crate::consensus::Decodable;
+
+        const SOME_TX: &str = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000";
+
+        let raw_tx = hex!(SOME_TX);
+        let tx: Transaction = Decodable::consensus_decode(&mut raw_tx.as_slice()).unwrap();
+
+        let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/byte is valid");
+
+        assert_eq!(rate.fee_vb(tx.vsize() as u64), rate.fee_wu(tx.weight()));
+    }
 }
diff --git a/bitcoin/src/blockdata/locktime/absolute.rs b/bitcoin/src/blockdata/locktime/absolute.rs
index a8e9a993..6cb52c85 100644
--- a/bitcoin/src/blockdata/locktime/absolute.rs
+++ b/bitcoin/src/blockdata/locktime/absolute.rs
@@ -6,24 +6,33 @@
 //! whether `LockTime < LOCKTIME_THRESHOLD`.
 //!
 
-use core::cmp::Ordering;
+use core::cmp::{Ordering, PartialOrd};
 use core::{fmt, mem};
 
+use internals::write_err;
 use io::{BufRead, Write};
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
-use units::parse;
 
-use crate::consensus::encode::{self, Decodable, Encodable};
-use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
 #[cfg(doc)]
 use crate::absolute;
+use crate::consensus::encode::{self, Decodable, Encodable};
+use crate::error::{ParseIntError, PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
+use crate::parse::{self, impl_parse_str, impl_parse_str_from_int_infallible};
+use crate::prelude::*;
 
-#[rustfmt::skip]                // Keep public re-exports separate.
-#[doc(inline)]
-pub use units::locktime::absolute::{
-    Height, Time, LOCK_TIME_THRESHOLD, ConversionError, ParseHeightError, ParseTimeError,
-};
+/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
+///
+/// `LockTime` values _below_ the threshold are interpreted as block heights, values _above_ (or
+/// equal to) the threshold are interpreted as block times (UNIX timestamp, seconds since epoch).
+///
+/// Bitcoin is able to safely use this value because a block height greater than 500,000,000 would
+/// never occur because it would represent a height in approximately 9500 years. Conversely, block
+/// times under 500,000,000 will never happen because they would represent times before 1986 which
+/// are, for obvious reasons, not useful within the Bitcoin network.
+///
+/// [Bitcoin Core]: https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
+pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000;
 
 /// An absolute lock time value, representing either a block height or a UNIX timestamp (seconds
 /// since epoch).
@@ -129,7 +138,7 @@ pub fn from_unprefixed_hex(s: &str) -> Result<Self, UnprefixedHexError> {
     /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
     #[inline]
     pub fn from_consensus(n: u32) -> Self {
-        if units::locktime::absolute::is_block_height(n) {
+        if is_block_height(n) {
             Self::Blocks(Height::from_consensus(n).expect("n is valid"))
         } else {
             Self::Seconds(Time::from_consensus(n).expect("n is valid"))
@@ -284,7 +293,7 @@ pub fn to_consensus_u32(self) -> u32 {
     }
 }
 
-units::impl_parse_str_from_int_infallible!(LockTime, u32, from_consensus);
+impl_parse_str_from_int_infallible!(LockTime, u32, from_consensus);
 
 impl From<Height> for LockTime {
     #[inline]
@@ -406,6 +415,406 @@ fn arbitrary_cmp(&self, other: &Self) -> Ordering {
     }
 }
 
+/// An absolute block height, guaranteed to always contain a valid height value.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Height(u32);
+
+impl Height {
+    /// Absolute block height 0, the genesis block.
+    pub const ZERO: Self = Height(0);
+
+    /// The minimum absolute block height (0), the genesis block.
+    pub const MIN: Self = Self::ZERO;
+
+    /// The maximum absolute block height.
+    pub const MAX: Self = Height(LOCK_TIME_THRESHOLD - 1);
+
+    /// Creates a `Height` from a hex string.
+    ///
+    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
+    pub fn from_hex(s: &str) -> Result<Self, ParseHeightError> { parse_hex(s, Self::from_consensus) }
+
+    /// Constructs a new block height.
+    ///
+    /// # Errors
+    ///
+    /// If `n` does not represent a block height value (see documentation on [`LockTime`]).
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::locktime::absolute::Height;
+    ///
+    /// let h: u32 = 741521;
+    /// let height = Height::from_consensus(h).expect("invalid height value");
+    /// assert_eq!(height.to_consensus_u32(), h);
+    /// ```
+    #[inline]
+    pub fn from_consensus(n: u32) -> Result<Height, ConversionError> {
+        if is_block_height(n) {
+            Ok(Self(n))
+        } else {
+            Err(ConversionError::invalid_height(n))
+        }
+    }
+
+    /// Converts this `Height` to its inner `u32` value.
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::absolute::LockTime;
+    ///
+    /// let n_lock_time: u32 = 741521;
+    /// let lock_time = LockTime::from_consensus(n_lock_time);
+    /// assert!(lock_time.is_block_height());
+    /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
+    #[inline]
+    pub fn to_consensus_u32(self) -> u32 { self.0 }
+}
+
+impl fmt::Display for Height {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
+
+/// Error returned when parsing block height fails.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ParseHeightError(ParseError);
+
+impl fmt::Display for ParseHeightError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseHeightError {
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
+}
+
+impl From<ParseError> for ParseHeightError {
+    fn from(value: ParseError) -> Self { Self(value) }
+}
+
+/// A UNIX timestamp, seconds since epoch, guaranteed to always contain a valid time value.
+///
+/// Note that there is no manipulation of the inner value during construction or when using
+/// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x -
+/// threshold) seconds since epoch'.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Time(u32);
+
+impl Time {
+    /// The minimum absolute block time (Tue Nov 05 1985 00:53:20 GMT+0000).
+    pub const MIN: Self = Time(LOCK_TIME_THRESHOLD);
+
+    /// The maximum absolute block time (Sun Feb 07 2106 06:28:15 GMT+0000).
+    pub const MAX: Self = Time(u32::max_value());
+
+    /// Creates a `Time` from a hex string.
+    ///
+    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
+    pub fn from_hex(s: &str) -> Result<Self, ParseTimeError> { parse_hex(s, Self::from_consensus) }
+
+    /// Constructs a new block time.
+    ///
+    /// # Errors
+    ///
+    /// If `n` does not encode a UNIX time stamp (see documentation on [`LockTime`]).
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::locktime::absolute::Time;
+    ///
+    /// let t: u32 = 1653195600; // May 22nd, 5am UTC.
+    /// let time = Time::from_consensus(t).expect("invalid time value");
+    /// assert_eq!(time.to_consensus_u32(), t);
+    /// ```
+    #[inline]
+    pub fn from_consensus(n: u32) -> Result<Time, ConversionError> {
+        if is_block_time(n) {
+            Ok(Self(n))
+        } else {
+            Err(ConversionError::invalid_time(n))
+        }
+    }
+
+    /// Converts this `Time` to its inner `u32` value.
+    ///
+    /// # Examples
+    /// ```rust
+    /// use bitcoin::absolute::LockTime;
+    ///
+    /// let n_lock_time: u32 = 1653195600; // May 22nd, 5am UTC.
+    /// let lock_time = LockTime::from_consensus(n_lock_time);
+    /// assert_eq!(lock_time.to_consensus_u32(), n_lock_time);
+    /// ```
+    #[inline]
+    pub fn to_consensus_u32(self) -> u32 { self.0 }
+}
+
+impl fmt::Display for Time {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
+
+/// Error returned when parsing block time fails.
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct ParseTimeError(ParseError);
+
+impl fmt::Display for ParseTimeError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        self.0.display(f, "block height", LOCK_TIME_THRESHOLD, u32::MAX)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseTimeError {
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
+}
+
+impl From<ParseError> for ParseTimeError {
+    fn from(value: ParseError) -> Self { Self(value) }
+}
+
+fn parser<T, E, S, F>(f: F) -> impl FnOnce(S) -> Result<T, E>
+where
+    E: From<ParseError>,
+    S: AsRef<str> + Into<String>,
+    F: FnOnce(u32) -> Result<T, ConversionError>,
+{
+    move |s| {
+        let n = s.as_ref().parse::<i64>().map_err(ParseError::invalid_int(s))?;
+        let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
+        f(n).map_err(ParseError::from).map_err(Into::into)
+    }
+}
+
+fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
+where
+    E: From<ParseError>,
+    S: AsRef<str> + Into<String>,
+    F: FnOnce(u32) -> Result<T, ConversionError>,
+{
+    let n = i64::from_str_radix(parse::strip_hex_prefix(s.as_ref()), 16).map_err(ParseError::invalid_int(s))?;
+    let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
+    f(n).map_err(ParseError::from).map_err(Into::into)
+}
+
+/// Returns true if `n` is a block height i.e., less than 500,000,000.
+fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
+
+/// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
+fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
+
+/// Catchall type for errors that relate to time locks.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum Error {
+    /// An error occurred while converting a `u32` to a lock time variant.
+    Conversion(ConversionError),
+    /// An error occurred while operating on lock times.
+    Operation(OperationError),
+    /// An error occurred while parsing a string into an `u32`.
+    Parse(ParseIntError),
+}
+
+internals::impl_from_infallible!(Error);
+
+impl fmt::Display for Error {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use Error::*;
+
+        match *self {
+            Conversion(ref e) => write_err!(f, "error converting lock time value"; e),
+            Operation(ref e) => write_err!(f, "error during lock time operation"; e),
+            Parse(ref e) => write_err!(f, "failed to parse lock time from string"; e),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use Error::*;
+
+        match *self {
+            Conversion(ref e) => Some(e),
+            Operation(ref e) => Some(e),
+            Parse(ref e) => Some(e),
+        }
+    }
+}
+
+impl From<ConversionError> for Error {
+    #[inline]
+    fn from(e: ConversionError) -> Self { Self::Conversion(e) }
+}
+
+impl From<OperationError> for Error {
+    #[inline]
+    fn from(e: OperationError) -> Self { Self::Operation(e) }
+}
+
+impl From<ParseIntError> for Error {
+    #[inline]
+    fn from(e: ParseIntError) -> Self { Self::Parse(e) }
+}
+
+/// An error that occurs when converting a `u32` to a lock time variant.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct ConversionError {
+    /// The expected timelock unit, height (blocks) or time (seconds).
+    unit: LockTimeUnit,
+    /// The invalid input value.
+    input: u32,
+}
+
+impl ConversionError {
+    /// Constructs a `ConversionError` from an invalid `n` when expecting a height value.
+    fn invalid_height(n: u32) -> Self { Self { unit: LockTimeUnit::Blocks, input: n } }
+
+    /// Constructs a `ConversionError` from an invalid `n` when expecting a time value.
+    fn invalid_time(n: u32) -> Self { Self { unit: LockTimeUnit::Seconds, input: n } }
+}
+
+impl fmt::Display for ConversionError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ConversionError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
+}
+
+/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
+#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
+enum LockTimeUnit {
+    /// Lock by blockheight.
+    Blocks,
+    /// Lock by blocktime.
+    Seconds,
+}
+
+impl fmt::Display for LockTimeUnit {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use LockTimeUnit::*;
+
+        match *self {
+            Blocks => write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
+            Seconds => write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
+        }
+    }
+}
+
+/// Errors than occur when operating on lock times.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum OperationError {
+    /// Cannot compare different lock time units (height vs time).
+    InvalidComparison,
+}
+
+internals::impl_from_infallible!(OperationError);
+
+impl fmt::Display for OperationError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        use OperationError::*;
+
+        match *self {
+            InvalidComparison =>
+                f.write_str("cannot compare different lock units (height vs time)"),
+        }
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for OperationError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use OperationError::*;
+
+        match *self {
+            InvalidComparison => None,
+        }
+    }
+}
+
+/// Internal - common representation for height and time.
+#[derive(Debug, Clone, Eq, PartialEq)]
+enum ParseError {
+    InvalidInteger { source: core::num::ParseIntError, input: String },
+    // unit implied by outer type
+    // we use i64 to have nicer messages for negative values
+    Conversion(i64),
+}
+
+internals::impl_from_infallible!(ParseError);
+
+impl ParseError {
+    fn invalid_int<S: Into<String>>(s: S) -> impl FnOnce(core::num::ParseIntError) -> Self {
+        move |source| Self::InvalidInteger { source, input: s.into() }
+    }
+
+    fn display(&self, f: &mut fmt::Formatter<'_>, subject: &str, lower_bound: u32, upper_bound: u32) -> fmt::Result {
+        use core::num::IntErrorKind;
+
+        use ParseError::*;
+
+        match self {
+            InvalidInteger { source, input } if *source.kind() == IntErrorKind::PosOverflow => {
+                write!(f, "{} {} is above limit {}", subject, input, upper_bound)
+            }
+            InvalidInteger { source, input } if *source.kind() == IntErrorKind::NegOverflow => {
+                write!(f, "{} {} is below limit {}", subject, input, lower_bound)
+            }
+            InvalidInteger { source, input } => {
+                write_err!(f, "failed to parse {} as {}", input, subject; source)
+            }
+            Conversion(value) if *value < i64::from(lower_bound) => {
+                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
+            }
+            Conversion(value) => {
+                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
+            }
+        }
+    }
+
+    // To be consistent with `write_err` we need to **not** return source in case of overflow
+    #[cfg(feature = "std")]
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        use core::num::IntErrorKind;
+
+        use ParseError::*;
+
+        match self {
+            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::PosOverflow => None,
+            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::NegOverflow => None,
+            InvalidInteger { source, .. } => Some(source),
+            Conversion(_) => None,
+        }
+    }
+}
+
+impl From<ParseIntError> for ParseError {
+    fn from(value: ParseIntError) -> Self {
+        Self::InvalidInteger { source: value.source, input: value.input }
+    }
+}
+
+impl From<ConversionError> for ParseError {
+    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -451,6 +860,46 @@ fn lock_time_from_invalid_hex_should_err() {
         assert!(result.is_err());
     }
 
+    #[test]
+    fn time_from_str_hex_happy_path() {
+        let actual = Time::from_hex("0x6289C350").unwrap();
+        let expected = Time::from_consensus(0x6289C350).unwrap();
+        assert_eq!(actual, expected);
+    }
+
+    #[test]
+    fn time_from_str_hex_no_prefix_happy_path() {
+        let time = Time::from_hex("6289C350").unwrap();
+        assert_eq!(time, Time(0x6289C350));
+    }
+
+    #[test]
+    fn time_from_str_hex_invalid_hex_should_err() {
+        let hex = "0xzb93";
+        let result = Time::from_hex(hex);
+        assert!(result.is_err());
+    }
+
+    #[test]
+    fn height_from_str_hex_happy_path() {
+        let actual = Height::from_hex("0xBA70D").unwrap();
+        let expected = Height(0xBA70D);
+        assert_eq!(actual, expected);
+    }
+
+    #[test]
+    fn height_from_str_hex_no_prefix_happy_path() {
+        let height = Height::from_hex("BA70D").unwrap();
+        assert_eq!(height, Height(0xBA70D));
+    }
+
+    #[test]
+    fn height_from_str_hex_invalid_hex_should_err() {
+        let hex = "0xzb93";
+        let result = Height::from_hex(hex);
+        assert!(result.is_err());
+    }
+
     #[test]
     fn parses_correctly_to_height_or_time() {
         let lock = LockTime::from_consensus(750_000);
diff --git a/bitcoin/src/blockdata/locktime/relative.rs b/bitcoin/src/blockdata/locktime/relative.rs
index 92e55f17..ad3b140b 100644
--- a/bitcoin/src/blockdata/locktime/relative.rs
+++ b/bitcoin/src/blockdata/locktime/relative.rs
@@ -11,13 +11,10 @@
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
 
+use crate::parse::impl_parse_str_from_int_infallible;
 #[cfg(doc)]
 use crate::relative;
 
-#[rustfmt::skip]                // Keep public re-exports separate.
-#[doc(inline)]
-pub use units::locktime::relative::{Height, Time, TimeOverflowError};
-
 /// A relative lock time value, representing either a block height or time (512 second intervals).
 ///
 /// The `relative::LockTime` type does not have any constructors, this is by design, please use
@@ -191,6 +188,105 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
+/// A relative lock time lock-by-blockheight value.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Height(u16);
+
+impl Height {
+    /// Relative block height 0, can be included in any block.
+    pub const ZERO: Self = Height(0);
+
+    /// The minimum relative block height (0), can be included in any block.
+    pub const MIN: Self = Self::ZERO;
+
+    /// The maximum relative block height.
+    pub const MAX: Self = Height(u16::max_value());
+
+    /// Returns the inner `u16` value.
+    #[inline]
+    pub fn value(self) -> u16 { self.0 }
+}
+
+impl From<u16> for Height {
+    #[inline]
+    fn from(value: u16) -> Self { Height(value) }
+}
+
+impl_parse_str_from_int_infallible!(Height, u16, from);
+
+impl fmt::Display for Height {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+/// A relative lock time lock-by-blocktime value.
+///
+/// For BIP 68 relative lock-by-blocktime locks, time is measure in 512 second intervals.
+#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
+pub struct Time(u16);
+
+impl Time {
+    /// Relative block time 0, can be included in any block.
+    pub const ZERO: Self = Time(0);
+
+    /// The minimum relative block time (0), can be included in any block.
+    pub const MIN: Self = Time::ZERO;
+
+    /// The maximum relative block time (33,554,432 seconds or approx 388 days).
+    pub const MAX: Self = Time(u16::max_value());
+
+    /// Create a [`Time`] using time intervals where each interval is equivalent to 512 seconds.
+    ///
+    /// Encoding finer granularity of time for relative lock-times is not supported in Bitcoin.
+    #[inline]
+    pub fn from_512_second_intervals(intervals: u16) -> Self { Time(intervals) }
+
+    /// Create a [`Time`] from seconds, converting the seconds into 512 second interval with ceiling
+    /// division.
+    ///
+    /// # Errors
+    ///
+    /// Will return an error if the input cannot be encoded in 16 bits.
+    #[inline]
+    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
+        if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
+            Ok(Time::from_512_second_intervals(interval))
+        } else {
+            Err(TimeOverflowError { seconds })
+        }
+    }
+
+    /// Returns the inner `u16` value.
+    #[inline]
+    pub fn value(self) -> u16 { self.0 }
+}
+
+impl_parse_str_from_int_infallible!(Time, u16, from_512_second_intervals);
+
+impl fmt::Display for Time {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
+}
+
+/// Input time in seconds was too large to be encoded to a 16 bit 512 second interval.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct TimeOverflowError {
+    /// Time value in seconds that overflowed.
+    // Private because we maintain an invariant that the `seconds` value does actually overflow.
+    pub(crate) seconds: u32
+}
+
+impl fmt::Display for TimeOverflowError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        write!(f, "{} seconds is too large to be encoded to a 16 bit 512 second interval", self.seconds)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for TimeOverflowError {}
+
 /// Tried to satisfy a lock-by-blocktime lock using a height value.
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
diff --git a/bitcoin/src/blockdata/mod.rs b/bitcoin/src/blockdata/mod.rs
index 1d7ee2bb..943de54a 100644
--- a/bitcoin/src/blockdata/mod.rs
+++ b/bitcoin/src/blockdata/mod.rs
@@ -8,10 +8,12 @@
 
 pub mod block;
 pub mod constants;
+pub mod fee_rate;
 pub mod locktime;
 pub mod opcodes;
 pub mod script;
 pub mod transaction;
+pub mod weight;
 pub mod witness;
 
 #[rustfmt::skip]                // Keep public re-exports separate.
@@ -20,37 +22,3 @@
     fee_rate::FeeRate,
     weight::Weight
 };
-
-/// Implements `FeeRate` and assoctiated features.
-pub mod fee_rate {
-    /// Re-export everything from the [`units::fee_rate`] module.
-    pub use units::fee_rate::*;
-
-    #[cfg(test)]
-    mod tests {
-        use super::*;
-
-        #[test]
-        fn fee_convenience_functions_agree() {
-            use hex::test_hex_unwrap as hex;
-
-            use crate::blockdata::transaction::Transaction;
-            use crate::consensus::Decodable;
-
-            const SOME_TX: &str = "0100000001a15d57094aa7a21a28cb20b59aab8fc7d1149a3bdbcddba9c622e4f5f6a99ece010000006c493046022100f93bb0e7d8db7bd46e40132d1f8242026e045f03a0efe71bbb8e3f475e970d790221009337cd7f1f929f00cc6ff01f03729b069a7c21b59b1736ddfee5db5946c5da8c0121033b9b137ee87d5a812d6f506efdd37f0affa7ffc310711c06c7f3e097c9447c52ffffffff0100e1f505000000001976a9140389035a9225b3839e2bbf32d826a1e222031fd888ac00000000";
-
-            let raw_tx = hex!(SOME_TX);
-            let tx: Transaction = Decodable::consensus_decode(&mut raw_tx.as_slice()).unwrap();
-
-            let rate = FeeRate::from_sat_per_vb(1).expect("1 sat/byte is valid");
-
-            assert_eq!(rate.fee_vb(tx.vsize() as u64), rate.fee_wu(tx.weight()));
-        }
-    }
-}
-
-/// Implements `Weight` and associated features.
-pub mod weight {
-    /// Re-export everything from the [`units::weight`] module.
-    pub use units::weight::*;
-}
diff --git a/bitcoin/src/blockdata/script/witness_version.rs b/bitcoin/src/blockdata/script/witness_version.rs
index a51a01d8..29fa83c4 100644
--- a/bitcoin/src/blockdata/script/witness_version.rs
+++ b/bitcoin/src/blockdata/script/witness_version.rs
@@ -12,11 +12,11 @@
 
 use bech32::Fe32;
 use internals::write_err;
-use units::{parse, ParseIntError};
 
 use crate::blockdata::opcodes::all::*;
 use crate::blockdata::opcodes::Opcode;
 use crate::blockdata::script::Instruction;
+use crate::error::ParseIntError;
 
 /// Version of the segregated witness program.
 ///
@@ -87,7 +87,7 @@ impl FromStr for WitnessVersion {
     type Err = FromStrError;
 
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        let version: u8 = parse::int(s)?;
+        let version: u8 = crate::parse::int(s)?;
         Ok(WitnessVersion::try_from(version)?)
     }
 }
diff --git a/bitcoin/src/blockdata/transaction.rs b/bitcoin/src/blockdata/transaction.rs
index 58443b44..50c83054 100644
--- a/bitcoin/src/blockdata/transaction.rs
+++ b/bitcoin/src/blockdata/transaction.rs
@@ -16,7 +16,6 @@
 use hashes::{sha256d, Hash};
 use internals::write_err;
 use io::{BufRead, Write};
-use units::parse;
 
 use super::Weight;
 use crate::blockdata::locktime::absolute::{self, Height, Time};
@@ -27,6 +26,7 @@
 use crate::consensus::{encode, Decodable, Encodable};
 use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
 use crate::internal_macros::{impl_consensus_encoding, impl_hashencode};
+use crate::parse::{self, impl_parse_str_from_int_infallible};
 #[cfg(doc)]
 use crate::sighash::{EcdsaSighashType, TapSighashType};
 use crate::prelude::*;
@@ -169,7 +169,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
             return Err(ParseOutPointError::VoutNotCanonical);
         }
     }
-    parse::int(s).map_err(ParseOutPointError::Vout)
+    crate::parse::int(s).map_err(ParseOutPointError::Vout)
 }
 
 impl core::str::FromStr for OutPoint {
@@ -449,7 +449,7 @@ pub fn from_seconds_floor(seconds: u32) -> Result<Self, TimeOverflowError> {
         if let Ok(interval) = u16::try_from(seconds / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError::new(seconds))
+            Err(TimeOverflowError { seconds })
         }
     }
 
@@ -462,7 +462,7 @@ pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
         if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
             Ok(Sequence::from_512_second_intervals(interval))
         } else {
-            Err(TimeOverflowError::new(seconds))
+            Err(TimeOverflowError { seconds })
         }
     }
 
@@ -526,7 +526,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     }
 }
 
-units::impl_parse_str_from_int_infallible!(Sequence, u32, from_consensus);
+impl_parse_str_from_int_infallible!(Sequence, u32, from_consensus);
 
 /// Bitcoin transaction output.
 ///
@@ -1702,7 +1702,7 @@ fn outpoint() {
             OutPoint::from_str(
                 "5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:lol"
             ),
-            Err(ParseOutPointError::Vout(parse::int::<u32, _>("lol").unwrap_err()))
+            Err(ParseOutPointError::Vout(crate::parse::int::<u32, _>("lol").unwrap_err()))
         );
 
         assert_eq!(
diff --git a/units/src/weight.rs b/bitcoin/src/blockdata/weight.rs
similarity index 96%
rename from units/src/weight.rs
rename to bitcoin/src/blockdata/weight.rs
index eef336ef..ffd4eb65 100644
--- a/units/src/weight.rs
+++ b/bitcoin/src/blockdata/weight.rs
@@ -5,18 +5,13 @@
 use core::fmt;
 use core::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Sub, SubAssign};
 
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-/// The factor that non-witness serialization data is multiplied by during weight calculation.
-pub const WITNESS_SCALE_FACTOR: usize = 4;
-
 /// Represents block weight - the weight of a transaction or block.
 ///
 /// This is an integer newtype representing weigth in `wu`. It provides protection against mixing
 /// up the types as well as basic formatting features.
 #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
 #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
 #[cfg_attr(feature = "serde", serde(transparent))]
 pub struct Weight(u64);
 
@@ -35,7 +30,7 @@ impl Weight {
     pub const MAX: Weight = Weight(u64::MAX);
 
     /// The factor that non-witness serialization data is multiplied by during weight calculation.
-    pub const WITNESS_SCALE_FACTOR: u64 = WITNESS_SCALE_FACTOR as u64;
+    pub const WITNESS_SCALE_FACTOR: u64 = crate::blockdata::constants::WITNESS_SCALE_FACTOR as u64;
 
     /// The maximum allowed weight for a block, see BIP 141 (network rule).
     pub const MAX_BLOCK: Weight = Weight(4_000_000);
@@ -337,4 +332,4 @@ fn sum<I>(iter: I) -> Self
     }
 }
 
-crate::impl_parse_str_from_int_infallible!(Weight, u64, from_wu);
+crate::parse::impl_parse_str_from_int_infallible!(Weight, u64, from_wu);
diff --git a/bitcoin/src/crypto/key.rs b/bitcoin/src/crypto/key.rs
index a1f03480..e3babd05 100644
--- a/bitcoin/src/crypto/key.rs
+++ b/bitcoin/src/crypto/key.rs
@@ -241,14 +241,14 @@ fn from_str(s: &str) -> Result<PublicKey, ParsePublicKeyError> {
                     InvalidChar(e) => ParsePublicKeyError::InvalidChar(e.invalid_char()),
                     InvalidLength(_) => unreachable!("length checked already")
                 })?;
-                Ok(PublicKey::from_slice(&bytes)?)
+                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
             },
             130 => {
                 let bytes = <[u8; 65]>::from_hex(s).map_err(|e| match e {
                     InvalidChar(e) => ParsePublicKeyError::InvalidChar(e.invalid_char()),
                     InvalidLength(_) => unreachable!("length checked already")
                 })?;
-                Ok(PublicKey::from_slice(&bytes)?)
+                Ok(PublicKey::from_slice(&bytes).expect("length checked already"))
             }
             len => Err(ParsePublicKeyError::InvalidHexLength(len)),
         }
@@ -1394,34 +1394,6 @@ fn public_key_from_str_wrong_length() {
         let res = PublicKey::from_str(s);
         assert!(res.is_err());
         assert_eq!(res.unwrap_err(), ParsePublicKeyError::InvalidHexLength(8));
-    }
-
-    #[test]
-    fn public_key_from_str_invalid_str() {
-        // Ensuring test cases fail when PublicKey::from_str is used on invalid keys
-        let s = "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b142";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::Secp256k1(secp256k1::Error::InvalidPublicKey)));
-
-        let s = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd169";
-        assert_eq!(s.len(), 66);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::Secp256k1(secp256k1::Error::InvalidPublicKey)));
-
-        let s = "062e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b133";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::Encoding(FromSliceError::InvalidKeyPrefix(6)));
-
-        let s = "042e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1af191923a2964c177f5b5923ae500fca49e99492d534aa3759d6b25a8bc971b13g";
-        assert_eq!(s.len(), 130);
-        let res = PublicKey::from_str(s);
-        assert!(res.is_err());
-        assert_eq!(res.unwrap_err(), ParsePublicKeyError::InvalidChar(103));
 
         let s = "032e58afe51f9ed8ad3cc7897f634d881fdbe49a81564629ded8156bebd2ffd1ag";
         assert_eq!(s.len(), 66);
diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs
index 7ae3eb76..70fc1048 100644
--- a/bitcoin/src/lib.rs
+++ b/bitcoin/src/lib.rs
@@ -89,6 +89,7 @@
 #[macro_use]
 mod test_macros;
 mod internal_macros;
+mod parse;
 #[cfg(feature = "serde")]
 mod serde_utils;
 
@@ -196,9 +197,3 @@ fn consensus_encode<W: Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Er
         }
     }
 }
-
-/// Unit parsing utilities.
-pub mod parse {
-    /// Re-export everything from the [`units::parse`] module.
-    pub use units::parse::ParseIntError;
-}
diff --git a/bitcoin/src/parse.rs b/bitcoin/src/parse.rs
index e69de29b..7e5b86c6 100644
--- a/bitcoin/src/parse.rs
+++ b/bitcoin/src/parse.rs
@@ -0,0 +1,179 @@
+// SPDX-License-Identifier: CC0-1.0
+
+use core::fmt;
+use core::str::FromStr;
+
+use internals::write_err;
+
+use crate::prelude::*;
+
+/// Error with rich context returned when a string can't be parsed as an integer.
+///
+/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
+/// parse as well as type information. As a result it provides very informative error messages that
+/// make it easier to understand the problem and correct mistakes.
+///
+/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
+/// in a performance-critical application you may want to box it or throw away the context by
+/// converting to `core` type.
+#[derive(Debug, Clone, PartialEq, Eq)]
+#[non_exhaustive]
+pub struct ParseIntError {
+    pub(crate) input: String,
+    // for displaying - see Display impl with nice error message below
+    bits: u8,
+    // We could represent this as a single bit but it wouldn't actually derease the cost of moving
+    // the struct because String contains pointers so there will be padding of bits at least
+    // pointer_size - 1 bytes: min 1B in practice.
+    is_signed: bool,
+    pub(crate) source: core::num::ParseIntError,
+}
+
+impl ParseIntError {
+    /// Returns the input that was attempted to be parsed.
+    pub fn input(&self) -> &str { &self.input }
+}
+
+impl fmt::Display for ParseIntError {
+    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+        let signed = if self.is_signed { "signed" } else { "unsigned" };
+        let n = if self.bits == 8 { "n" } else { "" };
+        write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
+    }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for ParseIntError {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.source) }
+}
+
+impl From<ParseIntError> for core::num::ParseIntError {
+    fn from(value: ParseIntError) -> Self { value.source }
+}
+
+impl AsRef<core::num::ParseIntError> for ParseIntError {
+    fn as_ref(&self) -> &core::num::ParseIntError { &self.source }
+}
+
+/// Not strictly necessary but serves as a lint - avoids weird behavior if someone accidentally
+/// passes non-integer to the `parse()` function.
+pub(crate) trait Integer:
+    FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized
+{
+}
+
+macro_rules! impl_integer {
+    ($($type:ty),* $(,)?) => {
+        $(
+        impl Integer for $type {}
+        )*
+    }
+}
+
+impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
+
+/// Parses the input string as an integer returning an error carrying rich context.
+///
+/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
+/// owned since it avoids allocation in error case.
+pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
+    s.as_ref().parse().map_err(|error| {
+        ParseIntError {
+            input: s.into(),
+            bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
+            // We detect if the type is signed by checking if -1 can be represented by it
+            // this way we don't have to implement special traits and optimizer will get rid of the
+            // computation.
+            is_signed: T::try_from(-1i8).is_ok(),
+            source: error,
+        }
+    })
+}
+
+/// Strips the hex prefix off `s` if one is present.
+pub(crate) fn strip_hex_prefix(s: &str) -> &str {
+    if let Some(stripped) = s.strip_prefix("0x") {
+        stripped
+    } else if let Some(stripped) = s.strip_prefix("0X") {
+        stripped
+    } else {
+        s
+    }
+}
+
+pub(crate) fn hex_u32<S: AsRef<str> + Into<String>>(s: S) -> Result<u32, ParseIntError> {
+    u32::from_str_radix(strip_hex_prefix(s.as_ref()), 16).map_err(|error| ParseIntError {
+        input: s.into(),
+        bits: u8::try_from(core::mem::size_of::<u32>() * 8).expect("max is 32 bits for u32"),
+        is_signed: u32::try_from(-1i8).is_ok(),
+        source: error,
+    })
+}
+
+/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
+/// conversion function `fn`.
+macro_rules! impl_tryfrom_str_from_int_infallible {
+    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
+        $(
+        impl core::convert::TryFrom<$from> for $to {
+            type Error = $crate::error::ParseIntError;
+
+            fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
+                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
+            }
+        }
+        )*
+    }
+}
+pub(crate) use impl_tryfrom_str_from_int_infallible;
+
+/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping
+/// the output using infallible conversion function `fn`.
+///
+/// The `Error` type is `ParseIntError`
+macro_rules! impl_parse_str_from_int_infallible {
+    ($to:ident, $inner:ident, $fn:ident) => {
+        $crate::parse::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; $crate::prelude::String, $to, $inner, $fn; $crate::prelude::Box<str>, $to, $inner, $fn);
+
+        impl core::str::FromStr for $to {
+            type Err = $crate::error::ParseIntError;
+
+            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
+                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
+            }
+        }
+
+    }
+}
+pub(crate) use impl_parse_str_from_int_infallible;
+
+macro_rules! impl_tryfrom_str {
+    ($($from:ty, $to:ty, $err:ty, $inner_fn:expr);*) => {
+        $(
+            impl core::convert::TryFrom<$from> for $to {
+                type Error = $err;
+
+                fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
+                    $inner_fn(s)
+                }
+            }
+        )*
+    }
+}
+pub(crate) use impl_tryfrom_str;
+
+/// Implements standard parsing traits for `$type` by calling into `$inner_fn`.
+macro_rules! impl_parse_str {
+    ($to:ty, $err:ty, $inner_fn:expr) => {
+        $crate::parse::impl_tryfrom_str!(&str, $to, $err, $inner_fn; String, $to, $err, $inner_fn; Box<str>, $to, $err, $inner_fn);
+
+        impl core::str::FromStr for $to {
+            type Err = $err;
+
+            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
+                $inner_fn(s)
+            }
+        }
+    }
+}
+pub(crate) use impl_parse_str;
diff --git a/bitcoin/src/pow.rs b/bitcoin/src/pow.rs
index b65b5976..e07009e8 100644
--- a/bitcoin/src/pow.rs
+++ b/bitcoin/src/pow.rs
@@ -12,14 +12,13 @@
 use io::{BufRead, Write};
 #[cfg(all(test, mutate))]
 use mutagen::mutate;
-use units::parse;
 
 use crate::blockdata::block::BlockHash;
 use crate::consensus::encode::{self, Decodable, Encodable};
 #[cfg(doc)]
 use crate::consensus::Params;
 use crate::error::{PrefixedHexError, UnprefixedHexError, ContainsPrefixError, MissingPrefixError};
-use crate::Network;
+use crate::{parse, Network};
 
 /// Implement traits and methods shared by `Target` and `Work`.
 macro_rules! do_impl {
diff --git a/bitcoin/src/psbt/mod.rs b/bitcoin/src/psbt/mod.rs
index cc0e6cf9..4eac052d 100644
--- a/bitcoin/src/psbt/mod.rs
+++ b/bitcoin/src/psbt/mod.rs
@@ -20,17 +20,15 @@
 
 use hashes::Hash;
 use internals::write_err;
-use secp256k1::{Keypair, Message, Secp256k1, Signing, Verification};
+use secp256k1::{Message, Secp256k1, Signing};
 
 use crate::bip32::{self, KeySource, Xpriv, Xpub};
 use crate::blockdata::transaction::{self, Transaction, TxOut};
-use crate::crypto::{ecdsa, taproot};
+use crate::crypto::ecdsa;
 use crate::crypto::key::{PrivateKey, PublicKey};
 use crate::prelude::*;
-use crate::sighash::{self, EcdsaSighashType, Prevouts, SighashCache};
-use crate::{Amount, FeeRate, TapSighashType};
-use crate::key::TapTweak;
-use crate::TapLeafHash;
+use crate::sighash::{self, EcdsaSighashType, SighashCache};
+use crate::{Amount, FeeRate};
 
 #[rustfmt::skip]                // Keep public re-exports separate.
 #[doc(inline)]
@@ -305,7 +303,7 @@ pub fn sign<C, K>(
         secp: &Secp256k1<C>,
     ) -> Result<SigningKeys, (SigningKeys, SigningErrors)>
     where
-        C: Signing + Verification,
+        C: Signing,
         K: GetKey,
     {
         let tx = self.unsigned_tx.clone(); // clone because we need to mutably borrow when signing.
@@ -315,31 +313,16 @@ pub fn sign<C, K>(
         let mut errors = BTreeMap::new();
 
         for i in 0..self.inputs.len() {
-            match self.signing_algorithm(i) {
-                Ok(SigningAlgorithm::Ecdsa) => {
-                    match self.bip32_sign_ecdsa(k, i, &mut cache, secp) {
-                        Ok(v) => {
-                            used.insert(i, v);
-                        }
-                        Err(e) => {
-                            errors.insert(i, e);
-                        }
+            if let Ok(SigningAlgorithm::Ecdsa) = self.signing_algorithm(i) {
+                match self.bip32_sign_ecdsa(k, i, &mut cache, secp) {
+                    Ok(v) => {
+                        used.insert(i, v);
                     }
-                }
-                Ok(SigningAlgorithm::Schnorr) => {
-                    match self.bip32_sign_schnorr(k, i, &mut cache, secp) {
-                        Ok(v) => {
-                            used.insert(i, v);
-                        }
-                        Err(e) => {
-                            errors.insert(i, e);
-                        }
+                    Err(e) => {
+                        errors.insert(i, e);
                     }
                 }
-                Err(e) => {
-                    errors.insert(i, e);
-                }
-            }
+            };
         }
         if errors.is_empty() {
             Ok(used)
@@ -402,102 +385,6 @@ fn bip32_sign_ecdsa<C, K, T>(
         Ok(used)
     }
 
-    /// Attempts to create all signatures required by this PSBT's `tap_key_origins` field, adding
-    /// them to `tap_key_sig` or `tap_script_sigs`.
-    ///
-    /// # Returns
-    ///
-    /// - Ok: A list of the public keys used in signing.
-    /// - Err: Error encountered trying to calculate the sighash AND we had the signing key. Also panics
-    /// if input_index is out of bounds.
-    fn bip32_sign_schnorr<C, K, T>(
-        &mut self,
-        k: &K,
-        input_index: usize,
-        cache: &mut SighashCache<T>,
-        secp: &Secp256k1<C>,
-    ) -> Result<Vec<PublicKey>, SignError>
-        where
-            C: Signing + Verification,
-            T: Borrow<Transaction>,
-            K: GetKey,
-    {
-
-        let mut input = self.inputs[input_index].clone();
-
-        let mut used = vec![]; // List of pubkeys used to sign the input.
-
-        for (&xonly, (leaf_hashes, key_source)) in input.tap_key_origins.iter() {
-            let sk = if let Ok(Some(secret_key)) = k.get_key(KeyRequest::Bip32(key_source.clone()), secp) {
-                secret_key
-            } else {
-                continue;
-            };
-
-            // Considering the responsibility of the PSBT's finalizer to extract valid signatures,
-            // the goal of this algorithm is to provide signatures to the best of our ability:
-            // 1) If the conditions for key path spend are met, proceed to provide the signature for key path spend
-            // 2) If the conditions for script path spend are met, proceed to provide the signature for script path spend
-
-            // key path spend
-            if let Some(internal_key) = input.tap_internal_key {
-
-                // BIP 371: The internal key does not have leaf hashes, so can be indicated with a hashes len of 0.
-
-                // Based on input.tap_internal_key.is_some() alone, it is not sufficient to determine whether it is a key path spend.
-                // According to BIP 371, we also need to consider the condition leaf_hashes.is_empty() for a more accurate determination.
-                if internal_key == xonly && leaf_hashes.is_empty() && input.tap_key_sig.is_none() {
-                    let (msg, sighash_type) = self.sighash_taproot(input_index, cache, None)?;
-                    let key_pair = Keypair::from_secret_key(secp, &sk.inner)
-                        .tap_tweak(secp, input.tap_merkle_root)
-                        .to_inner();
-
-                    #[cfg(feature = "rand-std")]
-                    let signature = secp.sign_schnorr(&msg, &key_pair);
-                    #[cfg(not(feature = "rand-std"))]
-                    let signature = secp.sign_schnorr_no_aux_rand(&msg, &key_pair);
-
-                    let signature = taproot::Signature { signature, sighash_type };
-                    input.tap_key_sig = Some(signature);
-
-                    used.push(sk.public_key(secp));
-                }
-            }
-
-            // script path spend
-            if let Some((leaf_hashes, _)) = input.tap_key_origins.get(&xonly) {
-                let leaf_hashes = leaf_hashes
-                    .iter()
-                    .filter(|lh| !input.tap_script_sigs.contains_key(&(xonly, **lh)))
-                    .cloned()
-                    .collect::<Vec<_>>();
-
-                if !leaf_hashes.is_empty() {
-
-                    let key_pair = Keypair::from_secret_key(secp, &sk.inner);
-
-                    for lh in leaf_hashes {
-                        let (msg, sighash_type) = self.sighash_taproot(input_index, cache, Some(lh))?;
-
-                        #[cfg(feature = "rand-std")]
-                        let signature = secp.sign_schnorr(&msg, &key_pair);
-                        #[cfg(not(feature = "rand-std"))]
-                        let signature = secp.sign_schnorr_no_aux_rand(&msg, &key_pair);
-
-                        let signature = taproot::Signature { signature, sighash_type };
-                        input.tap_script_sigs.insert((xonly, lh), signature);
-                    }
-
-                    used.push(sk.public_key(secp));
-                }
-            }
-        }
-
-        self.inputs[input_index] = input;
-
-        Ok(used)
-    }
-
     /// Returns the sighash message to sign an ECDSA input along with the sighash type.
     ///
     /// Uses the [`EcdsaSighashType`] from this input if one is specified. If no sighash type is
@@ -559,64 +446,6 @@ pub fn sighash_ecdsa<T: Borrow<Transaction>>(
         }
     }
 
-    /// Returns the sighash message to sign an SCHNORR input along with the sighash type.
-    ///
-    /// Uses the [`TapSighashType`] from this input if one is specified. If no sighash type is
-    /// specified uses [`TapSighashType::Default`].
-    fn sighash_taproot<T: Borrow<Transaction>>(
-        &self,
-        input_index: usize,
-        cache: &mut SighashCache<T>,
-        leaf_hash: Option<TapLeafHash>
-    ) -> Result<(Message, TapSighashType), SignError> {
-        use OutputType::*;
-
-        if self.signing_algorithm(input_index)? != SigningAlgorithm::Schnorr {
-            return Err(SignError::WrongSigningAlgorithm);
-        }
-
-        let input = self.checked_input(input_index)?;
-
-        match self.output_type(input_index)? {
-            Tr => {
-                let hash_ty = input
-                    .sighash_type
-                    .unwrap_or_else(|| TapSighashType::Default.into())
-                    .taproot_hash_ty()
-                    .map_err(|_| SignError::InvalidSighashType)?;
-
-                let spend_utxos = (0..self.inputs.len())
-                    .map(|i|  self.spend_utxo(i).ok())
-                    .collect::<Vec<_>>();
-                let all_spend_utxos;
-
-                let is_anyone_can_pay = PsbtSighashType::from(hash_ty).to_u32() & 0x80 != 0;
-
-                let prev_outs = if is_anyone_can_pay {
-                    Prevouts::One(
-                        input_index,
-                        spend_utxos[input_index].ok_or(SignError::MissingSpendUtxo)?,
-                    )
-                } else if spend_utxos.iter().all(Option::is_some) {
-                    all_spend_utxos = spend_utxos.iter().filter_map(|x| *x).collect::<Vec<_>>();
-                    Prevouts::All(&all_spend_utxos)
-                } else {
-                    return Err(SignError::MissingSpendUtxo);
-                };
-
-                let sighash = if let Some(leaf_hash) = leaf_hash {
-                    cache.taproot_script_spend_signature_hash(input_index, &prev_outs, leaf_hash, hash_ty)?
-                } else {
-                    cache.taproot_key_spend_signature_hash(input_index, &prev_outs, hash_ty)?
-                };
-                Ok((Message::from(sighash), hash_ty))
-            }
-            _ => {
-                Err(SignError::Unsupported)
-            }
-        }
-    }
-
     /// Returns the spending utxo for this PSBT's input at `input_index`.
     pub fn spend_utxo(&self, input_index: usize) -> Result<&TxOut, SignError> {
         let input = self.checked_input(input_index)?;
@@ -947,8 +776,6 @@ pub enum SignError {
     SegwitV0Sighash(transaction::InputsIndexError),
     /// Sighash computation error (p2wpkh input).
     P2wpkhSighash(sighash::P2wpkhError),
-    /// Sighash computation error (taproot input).
-    TaprootError(sighash::TaprootError),
     /// Unable to determine the output type.
     UnknownOutputType,
     /// Unable to find key.
@@ -977,7 +804,6 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
             NotWpkh => write!(f, "the scriptPubkey is not a P2WPKH script"),
             SegwitV0Sighash(ref e) => write_err!(f, "segwit v0 sighash"; e),
             P2wpkhSighash(ref e) => write_err!(f, "p2wpkh sighash"; e),
-            TaprootError(ref e) => write_err!(f, "taproot sighash"; e),
             UnknownOutputType => write!(f, "unable to determine the output type"),
             KeyNotFound => write!(f, "unable to find key"),
             WrongSigningAlgorithm =>
@@ -995,7 +821,6 @@ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
         match *self {
             SegwitV0Sighash(ref e) => Some(e),
             P2wpkhSighash(ref e) => Some(e),
-            TaprootError(ref e) => Some(e),
             IndexOutOfBounds(ref e) => Some(e),
             InvalidSighashType
             | MissingInputUtxo
@@ -1021,10 +846,6 @@ impl From<IndexOutOfBoundsError> for SignError {
     fn from(e: IndexOutOfBoundsError) -> Self { SignError::IndexOutOfBounds(e) }
 }
 
-impl From<sighash::TaprootError> for SignError {
-    fn from(e: sighash::TaprootError) -> Self { SignError::TaprootError(e) }
-}
-
 /// This error is returned when extracting a [`Transaction`] from a [`Psbt`].
 #[derive(Debug, Clone, PartialEq, Eq)]
 #[non_exhaustive]
@@ -2280,9 +2101,9 @@ fn sign_psbt() {
         };
         psbt.inputs[1].witness_utxo = Some(txout_unknown_future);
 
-        let (signing_keys, _) = psbt.sign(&key_map, &secp).unwrap_err();
+        let sigs = psbt.sign(&key_map, &secp).unwrap();
 
-        assert_eq!(signing_keys.len(), 1);
-        assert_eq!(signing_keys[&0], vec![pk]);
+        assert!(sigs.len() == 1);
+        assert!(sigs[&0] == vec![pk]);
     }
 }
diff --git a/bitcoin/tests/bip_174.rs b/bitcoin/tests/psbt.rs
similarity index 100%
rename from bitcoin/tests/bip_174.rs
rename to bitcoin/tests/psbt.rs
diff --git a/contrib/run_task.sh b/contrib/run_task.sh
index 42c83135..f7aa21ac 100755
--- a/contrib/run_task.sh
+++ b/contrib/run_task.sh
@@ -212,12 +212,10 @@ do_asan() {
       RUSTFLAGS='-Zsanitizer=address -Clinker=clang -Cforce-frame-pointers=yes'                    \
       ASAN_OPTIONS='detect_leaks=1 detect_invalid_pointer_pairs=1 detect_stack_use_after_return=1' \
       cargo test --lib --no-default-features --features="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
-    # There is currently a bug in the MemorySanitizer (MSAN) - disable the job for now.
-    #
-    # cargo clean
-    # CC='clang -fsanitize=memory -fno-omit-frame-pointer'                                         \
-    #   RUSTFLAGS='-Zsanitizer=memory -Zsanitizer-memory-track-origins -Cforce-frame-pointers=yes'   \
-    #   cargo test --lib --no-default-features --features="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
+    cargo clean
+    CC='clang -fsanitize=memory -fno-omit-frame-pointer'                                         \
+      RUSTFLAGS='-Zsanitizer=memory -Zsanitizer-memory-track-origins -Cforce-frame-pointers=yes'   \
+      cargo test --lib --no-default-features --features="$ASAN_FEATURES" -Zbuild-std --target x86_64-unknown-linux-gnu
 }
 
 # Bench only works with a non-stable toolchain (nightly, beta).
diff --git a/units/src/amount.rs b/units/src/amount.rs
index 963a248f..e96238ba 100644
--- a/units/src/amount.rs
+++ b/units/src/amount.rs
@@ -1064,8 +1064,15 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
 // Just using Bitcoin denominated string.
 impl fmt::Display for Amount {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.fmt_value_in(f, Denomination::Bitcoin)?;
-        write!(f, " {}", Denomination::Bitcoin)
+        let satoshis = self.to_sat();
+        let denomination = Denomination::Bitcoin;
+        let mut format_options = FormatOptions::from_formatter(f);
+
+        if f.precision().is_none() && satoshis.rem_euclid(Amount::ONE_BTC.to_sat()) != 0 {
+            format_options.precision = Some(8);
+        }
+
+        fmt_satoshi_in(satoshis, false, f, denomination, true, format_options)
     }
 }
 
@@ -2907,4 +2914,35 @@ fn disallow_unknown_denomination() {
             }
         }
     }
+
+    #[test]
+    #[cfg(feature = "alloc")]
+    fn trailing_zeros_for_amount() {
+        assert_eq!(format!("{}", Amount::ONE_SAT), "0.00000001 BTC");
+        assert_eq!(format!("{}", Amount::ONE_BTC), "1 BTC");
+        assert_eq!(format!("{}", Amount::from_sat(1)), "0.00000001 BTC");
+        assert_eq!(format!("{}", Amount::from_sat(10)), "0.00000010 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(10)), "0.0000001 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(100)), "0.000001 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(1000)), "0.00001 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(10_000)), "0.0001 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(100_000)), "0.001 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(1_000_000)), "0.01 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(10_000_000)), "0.10 BTC");
+        assert_eq!(format!("{:.2}", Amount::from_sat(100_000_000)), "1.00 BTC");
+        assert_eq!(format!("{}", Amount::from_sat(100_000_000)), "1 BTC");
+        assert_eq!(format!("{}", Amount::from_sat(40_000_000_000)), "400 BTC");
+        assert_eq!(
+            format!("{:.10}", Amount::from_sat(100_000_000)),
+            "1.0000000000 BTC"
+        );
+        assert_eq!(
+            format!("{}", Amount::from_sat(400_000_000_000_010)),
+            "4000000.00000010 BTC"
+        );
+        assert_eq!(
+            format!("{}", Amount::from_sat(400_000_000_000_000)),
+            "4000000 BTC"
+        );
+    }
 }
diff --git a/units/src/lib.rs b/units/src/lib.rs
index 3b42f1c2..a79f496c 100644
--- a/units/src/lib.rs
+++ b/units/src/lib.rs
@@ -34,28 +34,6 @@
 pub extern crate serde;
 
 pub mod amount;
-#[cfg(feature = "alloc")]
-pub mod locktime;
-#[cfg(feature = "alloc")]
-pub mod fee_rate;
-#[cfg(feature = "alloc")]
-pub mod parse;
-#[cfg(feature = "alloc")]
-pub mod weight;
 
 #[doc(inline)]
-pub use self::{
-    amount::{Amount, ParseAmountError, SignedAmount},
-};
-#[cfg(feature = "alloc")]
-pub use self::parse::ParseIntError;
-
-#[rustfmt::skip]
-#[allow(unused_imports)]
-mod prelude {
-    #[cfg(all(feature = "alloc", not(feature = "std")))]
-    pub use alloc::{string::{String, ToString}, vec::Vec, boxed::Box, borrow::{Borrow, BorrowMut, Cow, ToOwned}, slice, rc};
-
-    #[cfg(feature = "std")]
-    pub use std::{string::{String, ToString}, vec::Vec, boxed::Box, borrow::{Borrow, BorrowMut, Cow, ToOwned}, rc};
-}
+pub use self::amount::{Amount, ParseAmountError, SignedAmount};
diff --git a/units/src/locktime/absolute.rs b/units/src/locktime/absolute.rs
deleted file mode 100644
index 0930f3cd..00000000
--- a/units/src/locktime/absolute.rs
+++ /dev/null
@@ -1,367 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides type `Height` and `Time` types used by the `rust-bitcoin` `absolute::LockTime` type.
-
-use core::fmt;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-use internals::write_err;
-
-#[cfg(feature = "alloc")]
-use crate::prelude::*;
-use crate::parse::{self, ParseIntError};
-
-/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
-///
-/// `LockTime` values _below_ the threshold are interpreted as block heights, values _above_ (or
-/// equal to) the threshold are interpreted as block times (UNIX timestamp, seconds since epoch).
-///
-/// Bitcoin is able to safely use this value because a block height greater than 500,000,000 would
-/// never occur because it would represent a height in approximately 9500 years. Conversely, block
-/// times under 500,000,000 will never happen because they would represent times before 1986 which
-/// are, for obvious reasons, not useful within the Bitcoin network.
-///
-/// [Bitcoin Core]: https://github.com/bitcoin/bitcoin/blob/9ccaee1d5e2e4b79b0a7c29aadb41b97e4741332/src/script/script.h#L39
-pub const LOCK_TIME_THRESHOLD: u32 = 500_000_000;
-
-/// An absolute block height, guaranteed to always contain a valid height value.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Height(u32);
-
-impl Height {
-    /// Absolute block height 0, the genesis block.
-    pub const ZERO: Self = Height(0);
-
-    /// The minimum absolute block height (0), the genesis block.
-    pub const MIN: Self = Self::ZERO;
-
-    /// The maximum absolute block height.
-    pub const MAX: Self = Height(LOCK_TIME_THRESHOLD - 1);
-
-    /// Creates a `Height` from a hex string.
-    ///
-    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
-    pub fn from_hex(s: &str) -> Result<Self, ParseHeightError> { parse_hex(s, Self::from_consensus) }
-
-    /// Constructs a new block height.
-    ///
-    /// # Errors
-    ///
-    /// If `n` does not represent a valid block height value.
-    ///
-    /// # Examples
-    /// ```rust
-    /// use bitcoin_units::locktime::absolute::Height;
-    ///
-    /// let h: u32 = 741521;
-    /// let height = Height::from_consensus(h).expect("invalid height value");
-    /// assert_eq!(height.to_consensus_u32(), h);
-    /// ```
-    #[inline]
-    pub fn from_consensus(n: u32) -> Result<Height, ConversionError> {
-        if is_block_height(n) {
-            Ok(Self(n))
-        } else {
-            Err(ConversionError::invalid_height(n))
-        }
-    }
-
-    /// Converts this `Height` to its inner `u32` value.
-    #[inline]
-    pub fn to_consensus_u32(self) -> u32 { self.0 }
-}
-
-impl fmt::Display for Height {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-crate::impl_parse_str!(Height, ParseHeightError, parser(Height::from_consensus));
-
-/// Error returned when parsing block height fails.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct ParseHeightError(ParseError);
-
-impl fmt::Display for ParseHeightError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.0.display(f, "block height", 0, LOCK_TIME_THRESHOLD - 1)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseHeightError {
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
-}
-
-impl From<ParseError> for ParseHeightError {
-    fn from(value: ParseError) -> Self { Self(value) }
-}
-
-/// A UNIX timestamp, seconds since epoch, guaranteed to always contain a valid time value.
-///
-/// Note that there is no manipulation of the inner value during construction or when using
-/// `to_consensus_u32()`. Said another way, `Time(x)` means 'x seconds since epoch' _not_ '(x -
-/// threshold) seconds since epoch'.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Time(u32);
-
-impl Time {
-    /// The minimum absolute block time (Tue Nov 05 1985 00:53:20 GMT+0000).
-    pub const MIN: Self = Time(LOCK_TIME_THRESHOLD);
-
-    /// The maximum absolute block time (Sun Feb 07 2106 06:28:15 GMT+0000).
-    pub const MAX: Self = Time(u32::max_value());
-
-    /// Creates a `Time` from a hex string.
-    ///
-    /// The input string is may or may not contain a typical hex prefix e.g., `0x`.
-    pub fn from_hex(s: &str) -> Result<Self, ParseTimeError> { parse_hex(s, Self::from_consensus) }
-
-    /// Constructs a new block time.
-    ///
-    /// # Errors
-    ///
-    /// If `n` does not encode a valid UNIX time stamp.
-    ///
-    /// # Examples
-    /// ```rust
-    /// use bitcoin_units::locktime::absolute::Time;
-    ///
-    /// let t: u32 = 1653195600; // May 22nd, 5am UTC.
-    /// let time = Time::from_consensus(t).expect("invalid time value");
-    /// assert_eq!(time.to_consensus_u32(), t);
-    /// ```
-    #[inline]
-    pub fn from_consensus(n: u32) -> Result<Time, ConversionError> {
-        if is_block_time(n) {
-            Ok(Self(n))
-        } else {
-            Err(ConversionError::invalid_time(n))
-        }
-    }
-
-    /// Converts this `Time` to its inner `u32` value.
-    #[inline]
-    pub fn to_consensus_u32(self) -> u32 { self.0 }
-}
-
-impl fmt::Display for Time {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-crate::impl_parse_str!(Time, ParseTimeError, parser(Time::from_consensus));
-
-/// Error returned when parsing block time fails.
-#[derive(Debug, Clone, Eq, PartialEq)]
-pub struct ParseTimeError(ParseError);
-
-impl fmt::Display for ParseTimeError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        self.0.display(f, "block height", LOCK_TIME_THRESHOLD, u32::MAX)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseTimeError {
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { self.0.source() }
-}
-
-impl From<ParseError> for ParseTimeError {
-    fn from(value: ParseError) -> Self { Self(value) }
-}
-
-fn parser<T, E, S, F>(f: F) -> impl FnOnce(S) -> Result<T, E>
-where
-    E: From<ParseError>,
-    S: AsRef<str> + Into<String>,
-    F: FnOnce(u32) -> Result<T, ConversionError>,
-{
-    move |s| {
-        let n = s.as_ref().parse::<i64>().map_err(ParseError::invalid_int(s))?;
-        let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
-        f(n).map_err(ParseError::from).map_err(Into::into)
-    }
-}
-
-fn parse_hex<T, E, S, F>(s: S, f: F) -> Result<T, E>
-where
-    E: From<ParseError>,
-    S: AsRef<str> + Into<String>,
-    F: FnOnce(u32) -> Result<T, ConversionError>,
-{
-    let n = i64::from_str_radix(parse::strip_hex_prefix(s.as_ref()), 16).map_err(ParseError::invalid_int(s))?;
-    let n = u32::try_from(n).map_err(|_| ParseError::Conversion(n))?;
-    f(n).map_err(ParseError::from).map_err(Into::into)
-}
-
-/// Returns true if `n` is a block height i.e., less than 500,000,000.
-pub fn is_block_height(n: u32) -> bool { n < LOCK_TIME_THRESHOLD }
-
-/// Returns true if `n` is a UNIX timestamp i.e., greater than or equal to 500,000,000.
-pub fn is_block_time(n: u32) -> bool { n >= LOCK_TIME_THRESHOLD }
-
-/// An error that occurs when converting a `u32` to a lock time variant.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct ConversionError {
-    /// The expected timelock unit, height (blocks) or time (seconds).
-    unit: LockTimeUnit,
-    /// The invalid input value.
-    input: u32,
-}
-
-impl ConversionError {
-    /// Constructs a `ConversionError` from an invalid `n` when expecting a height value.
-    fn invalid_height(n: u32) -> Self { Self { unit: LockTimeUnit::Blocks, input: n } }
-
-    /// Constructs a `ConversionError` from an invalid `n` when expecting a time value.
-    fn invalid_time(n: u32) -> Self { Self { unit: LockTimeUnit::Seconds, input: n } }
-}
-
-impl fmt::Display for ConversionError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "invalid lock time value {}, {}", self.input, self.unit)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ConversionError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
-}
-
-/// Describes the two types of locking, lock-by-blockheight and lock-by-blocktime.
-#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)]
-enum LockTimeUnit {
-    /// Lock by blockheight.
-    Blocks,
-    /// Lock by blocktime.
-    Seconds,
-}
-
-impl fmt::Display for LockTimeUnit {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        use LockTimeUnit::*;
-
-        match *self {
-            Blocks => write!(f, "expected lock-by-blockheight (must be < {})", LOCK_TIME_THRESHOLD),
-            Seconds => write!(f, "expected lock-by-blocktime (must be >= {})", LOCK_TIME_THRESHOLD),
-        }
-    }
-}
-
-/// Internal - common representation for height and time.
-#[derive(Debug, Clone, Eq, PartialEq)]
-enum ParseError {
-    InvalidInteger { source: core::num::ParseIntError, input: String },
-    // unit implied by outer type
-    // we use i64 to have nicer messages for negative values
-    Conversion(i64),
-}
-
-internals::impl_from_infallible!(ParseError);
-
-impl ParseError {
-    fn invalid_int<S: Into<String>>(s: S) -> impl FnOnce(core::num::ParseIntError) -> Self {
-        move |source| Self::InvalidInteger { source, input: s.into() }
-    }
-
-    fn display(&self, f: &mut fmt::Formatter<'_>, subject: &str, lower_bound: u32, upper_bound: u32) -> fmt::Result {
-        use core::num::IntErrorKind;
-
-        use ParseError::*;
-
-        match self {
-            InvalidInteger { source, input } if *source.kind() == IntErrorKind::PosOverflow => {
-                write!(f, "{} {} is above limit {}", subject, input, upper_bound)
-            }
-            InvalidInteger { source, input } if *source.kind() == IntErrorKind::NegOverflow => {
-                write!(f, "{} {} is below limit {}", subject, input, lower_bound)
-            }
-            InvalidInteger { source, input } => {
-                write_err!(f, "failed to parse {} as {}", input, subject; source)
-            }
-            Conversion(value) if *value < i64::from(lower_bound) => {
-                write!(f, "{} {} is below limit {}", subject, value, lower_bound)
-            }
-            Conversion(value) => {
-                write!(f, "{} {} is above limit {}", subject, value, upper_bound)
-            }
-        }
-    }
-
-    // To be consistent with `write_err` we need to **not** return source in case of overflow
-    #[cfg(feature = "std")]
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
-        use core::num::IntErrorKind;
-
-        use ParseError::*;
-
-        match self {
-            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::PosOverflow => None,
-            InvalidInteger { source, .. } if *source.kind() == IntErrorKind::NegOverflow => None,
-            InvalidInteger { source, .. } => Some(source),
-            Conversion(_) => None,
-        }
-    }
-}
-
-impl From<ParseIntError> for ParseError {
-    fn from(value: ParseIntError) -> Self {
-        Self::InvalidInteger { source: value.source, input: value.input }
-    }
-}
-
-impl From<ConversionError> for ParseError {
-    fn from(value: ConversionError) -> Self { Self::Conversion(value.input.into()) }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn time_from_str_hex_happy_path() {
-        let actual = Time::from_hex("0x6289C350").unwrap();
-        let expected = Time::from_consensus(0x6289C350).unwrap();
-        assert_eq!(actual, expected);
-    }
-
-    #[test]
-    fn time_from_str_hex_no_prefix_happy_path() {
-        let time = Time::from_hex("6289C350").unwrap();
-        assert_eq!(time, Time(0x6289C350));
-    }
-
-    #[test]
-    fn time_from_str_hex_invalid_hex_should_err() {
-        let hex = "0xzb93";
-        let result = Time::from_hex(hex);
-        assert!(result.is_err());
-    }
-
-    #[test]
-    fn height_from_str_hex_happy_path() {
-        let actual = Height::from_hex("0xBA70D").unwrap();
-        let expected = Height(0xBA70D);
-        assert_eq!(actual, expected);
-    }
-
-    #[test]
-    fn height_from_str_hex_no_prefix_happy_path() {
-        let height = Height::from_hex("BA70D").unwrap();
-        assert_eq!(height, Height(0xBA70D));
-    }
-
-    #[test]
-    fn height_from_str_hex_invalid_hex_should_err() {
-        let hex = "0xzb93";
-        let result = Height::from_hex(hex);
-        assert!(result.is_err());
-    }
-
-}
diff --git a/units/src/locktime/mod.rs b/units/src/locktime/mod.rs
deleted file mode 100644
index 4cf62ad1..00000000
--- a/units/src/locktime/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides absolute and relative locktimes.
-
-pub mod absolute;
-pub mod relative;
diff --git a/units/src/locktime/relative.rs b/units/src/locktime/relative.rs
deleted file mode 100644
index a44a7486..00000000
--- a/units/src/locktime/relative.rs
+++ /dev/null
@@ -1,117 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Provides type `Height` and `Time` types used by the `rust-bitcoin` `relative::LockTime` type.
-
-use core::fmt;
-
-#[cfg(feature = "serde")]
-use serde::{Deserialize, Serialize};
-
-/// A relative lock time lock-by-blockheight value.
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Height(u16);
-
-impl Height {
-    /// Relative block height 0, can be included in any block.
-    pub const ZERO: Self = Height(0);
-
-    /// The minimum relative block height (0), can be included in any block.
-    pub const MIN: Self = Self::ZERO;
-
-    /// The maximum relative block height.
-    pub const MAX: Self = Height(u16::max_value());
-
-    /// Returns the inner `u16` value.
-    #[inline]
-    pub fn value(self) -> u16 { self.0 }
-}
-
-impl From<u16> for Height {
-    #[inline]
-    fn from(value: u16) -> Self { Height(value) }
-}
-
-crate::impl_parse_str_from_int_infallible!(Height, u16, from);
-
-impl fmt::Display for Height {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-/// A relative lock time lock-by-blocktime value.
-///
-/// For BIP 68 relative lock-by-blocktime locks, time is measure in 512 second intervals.
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
-pub struct Time(u16);
-
-impl Time {
-    /// Relative block time 0, can be included in any block.
-    pub const ZERO: Self = Time(0);
-
-    /// The minimum relative block time (0), can be included in any block.
-    pub const MIN: Self = Time::ZERO;
-
-    /// The maximum relative block time (33,554,432 seconds or approx 388 days).
-    pub const MAX: Self = Time(u16::max_value());
-
-    /// Create a [`Time`] using time intervals where each interval is equivalent to 512 seconds.
-    ///
-    /// Encoding finer granularity of time for relative lock-times is not supported in Bitcoin.
-    #[inline]
-    pub fn from_512_second_intervals(intervals: u16) -> Self { Time(intervals) }
-
-    /// Create a [`Time`] from seconds, converting the seconds into 512 second interval with ceiling
-    /// division.
-    ///
-    /// # Errors
-    ///
-    /// Will return an error if the input cannot be encoded in 16 bits.
-    #[inline]
-    pub fn from_seconds_ceil(seconds: u32) -> Result<Self, TimeOverflowError> {
-        if let Ok(interval) = u16::try_from((seconds + 511) / 512) {
-            Ok(Time::from_512_second_intervals(interval))
-        } else {
-            Err(TimeOverflowError { seconds })
-        }
-    }
-
-    /// Returns the inner `u16` value.
-    #[inline]
-    pub fn value(self) -> u16 { self.0 }
-}
-
-crate::impl_parse_str_from_int_infallible!(Time, u16, from_512_second_intervals);
-
-impl fmt::Display for Time {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fmt::Display::fmt(&self.0, f) }
-}
-
-/// Input time in seconds was too large to be encoded to a 16 bit 512 second interval.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct TimeOverflowError {
-    /// Time value in seconds that overflowed.
-    // Private because we maintain an invariant that the `seconds` value does actually overflow.
-    pub(crate) seconds: u32
-}
-
-impl TimeOverflowError {
-    /// Creates a new `TimeOverflowError` using `seconds`.
-    ///
-    /// # Panics
-    ///
-    /// If `seconds` would not actually overflow a `u16`.
-    pub fn new(seconds: u32) -> Self {
-        assert!(u16::try_from((seconds + 511) / 512).is_err());
-        Self { seconds }
-    }
-}
-
-impl fmt::Display for TimeOverflowError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        write!(f, "{} seconds is too large to be encoded to a 16 bit 512 second interval", self.seconds)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for TimeOverflowError {}
diff --git a/units/src/parse.rs b/units/src/parse.rs
deleted file mode 100644
index ba0b4b2c..00000000
--- a/units/src/parse.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-// SPDX-License-Identifier: CC0-1.0
-
-//! Parsing utilities.
-
-use core::fmt;
-use core::str::FromStr;
-use alloc::string::String;
-
-use internals::write_err;
-
-/// Error with rich context returned when a string can't be parsed as an integer.
-///
-/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
-/// parse as well as type information. As a result it provides very informative error messages that
-/// make it easier to understand the problem and correct mistakes.
-///
-/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
-/// in a performance-critical application you may want to box it or throw away the context by
-/// converting to `core` type.
-#[derive(Debug, Clone, PartialEq, Eq)]
-#[non_exhaustive]
-pub struct ParseIntError {
-    pub(crate) input: String,
-    // for displaying - see Display impl with nice error message below
-    bits: u8,
-    // We could represent this as a single bit but it wouldn't actually derease the cost of moving
-    // the struct because String contains pointers so there will be padding of bits at least
-    // pointer_size - 1 bytes: min 1B in practice.
-    is_signed: bool,
-    pub(crate) source: core::num::ParseIntError,
-}
-
-impl ParseIntError {
-    /// Returns the input that was attempted to be parsed.
-    pub fn input(&self) -> &str { &self.input }
-}
-
-impl fmt::Display for ParseIntError {
-    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
-        let signed = if self.is_signed { "signed" } else { "unsigned" };
-        let n = if self.bits == 8 { "n" } else { "" };
-        write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
-    }
-}
-
-#[cfg(feature = "std")]
-impl std::error::Error for ParseIntError {
-    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { Some(&self.source) }
-}
-
-impl From<ParseIntError> for core::num::ParseIntError {
-    fn from(value: ParseIntError) -> Self { value.source }
-}
-
-impl AsRef<core::num::ParseIntError> for ParseIntError {
-    fn as_ref(&self) -> &core::num::ParseIntError { &self.source }
-}
-
-/// Not strictly necessary but serves as a lint - avoids weird behavior if someone accidentally
-/// passes non-integer to the `parse()` function.
-pub trait Integer:
-    FromStr<Err = core::num::ParseIntError> + TryFrom<i8> + Sized
-{
-}
-
-macro_rules! impl_integer {
-    ($($type:ty),* $(,)?) => {
-        $(
-        impl Integer for $type {}
-        )*
-    }
-}
-
-impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
-
-/// Parses the input string as an integer returning an error carrying rich context.
-///
-/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
-/// owned since it avoids allocation in error case.
-pub fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
-    s.as_ref().parse().map_err(|error| {
-        ParseIntError {
-            input: s.into(),
-            bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
-            // We detect if the type is signed by checking if -1 can be represented by it
-            // this way we don't have to implement special traits and optimizer will get rid of the
-            // computation.
-            is_signed: T::try_from(-1i8).is_ok(),
-            source: error,
-        }
-    })
-}
-
-/// Strips the hex prefix off `s` if one is present.
-pub(crate) fn strip_hex_prefix(s: &str) -> &str {
-    if let Some(stripped) = s.strip_prefix("0x") {
-        stripped
-    } else if let Some(stripped) = s.strip_prefix("0X") {
-        stripped
-    } else {
-        s
-    }
-}
-
-/// Parses a u32 from a hex string.
-///
-/// Input string may or may not contain a `0x` prefix.
-pub fn hex_u32<S: AsRef<str> + Into<String>>(s: S) -> Result<u32, ParseIntError> {
-    u32::from_str_radix(strip_hex_prefix(s.as_ref()), 16).map_err(|error| ParseIntError {
-        input: s.into(),
-        bits: u8::try_from(core::mem::size_of::<u32>() * 8).expect("max is 32 bits for u32"),
-        is_signed: u32::try_from(-1i8).is_ok(),
-        source: error,
-    })
-}
-
-/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using infallible
-/// conversion function `fn`.
-#[macro_export]
-macro_rules! impl_tryfrom_str_from_int_infallible {
-    ($($from:ty, $to:ident, $inner:ident, $fn:ident);*) => {
-        $(
-        impl core::convert::TryFrom<$from> for $to {
-            type Error = $crate::parse::ParseIntError;
-
-            fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
-                $crate::parse::int::<$inner, $from>(s).map($to::$fn)
-            }
-        }
-        )*
-    }
-}
-
-/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping
-/// the output using infallible conversion function `fn`.
-///
-/// The `Error` type is `ParseIntError`
-#[macro_export]
-macro_rules! impl_parse_str_from_int_infallible {
-    ($to:ident, $inner:ident, $fn:ident) => {
-        #[cfg(all(feature = "alloc", not(feature = "std")))]
-        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; alloc::string::String, $to, $inner, $fn; alloc::boxed::Box<str>, $to, $inner, $fn);
-        #[cfg(feature = "std")]
-        $crate::impl_tryfrom_str_from_int_infallible!(&str, $to, $inner, $fn; std::string::String, $to, $inner, $fn; std::boxed::Box<str>, $to, $inner, $fn);
-
-        impl core::str::FromStr for $to {
-            type Err = $crate::parse::ParseIntError;
-
-            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
-                $crate::parse::int::<$inner, &str>(s).map($to::$fn)
-            }
-        }
-
-    }
-}
-
-/// Implements `TryFrom<$from> for $to`.
-#[macro_export]
-macro_rules! impl_tryfrom_str {
-    ($($from:ty, $to:ty, $err:ty, $inner_fn:expr);*) => {
-        $(
-            impl core::convert::TryFrom<$from> for $to {
-                type Error = $err;
-
-                fn try_from(s: $from) -> core::result::Result<Self, Self::Error> {
-                    $inner_fn(s)
-                }
-            }
-        )*
-    }
-}
-
-/// Implements standard parsing traits for `$type` by calling into `$inner_fn`.
-#[macro_export]
-macro_rules! impl_parse_str {
-    ($to:ty, $err:ty, $inner_fn:expr) => {
-        $crate::impl_tryfrom_str!(&str, $to, $err, $inner_fn; String, $to, $err, $inner_fn; Box<str>, $to, $err, $inner_fn);
-
-        impl core::str::FromStr for $to {
-            type Err = $err;
-
-            fn from_str(s: &str) -> core::result::Result<Self, Self::Err> {
-                $inner_fn(s)
-            }
-        }
-    }
-}


Test patch:
diff --git a/bitcoin/tests/psbt-sign-taproot.rs b/bitcoin/tests/psbt-sign-taproot.rs
deleted file mode 100644
index e1aad558..00000000
--- a/bitcoin/tests/psbt-sign-taproot.rs
+++ /dev/null
@@ -1,316 +0,0 @@
-#![cfg(not(feature = "rand-std"))]
-
-use std::collections::BTreeMap;
-use std::str::FromStr;
-use secp256k1::{Keypair, Signing, Secp256k1, XOnlyPublicKey};
-use bitcoin::{absolute, Address, Network, OutPoint, PrivateKey, Psbt, script, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness};
-use bitcoin::bip32::{DerivationPath, Fingerprint};
-use bitcoin::consensus::encode::serialize_hex;
-use bitcoin::opcodes::all::OP_CHECKSIG;
-use bitcoin::psbt::{GetKey, Input, KeyRequest, PsbtSighashType, SignError};
-use bitcoin::taproot::{LeafVersion, TaprootBuilder, TaprootSpendInfo};
-use bitcoin::transaction::Version;
-use units::Amount;
-
-
-#[test]
-fn psbt_sign_taproot() {
-
-    struct Keystore {
-        sk: PrivateKey,
-        mfp: Fingerprint,
-    }
-
-    impl GetKey for Keystore {
-        type Error = SignError;
-        fn get_key<C: Signing>(&self, key_request: KeyRequest, _secp: &Secp256k1<C>) -> Result<Option<PrivateKey>, Self::Error> {
-            match key_request {
-                KeyRequest::Bip32((mfp, _)) => {
-                    if mfp == self.mfp {
-                        Ok(Some(self.sk))
-                    } else {
-                        Err(SignError::KeyNotFound)
-                    }
-                }
-                _ => Err(SignError::KeyNotFound)
-            }
-        }
-    }
-
-    let secp = &Secp256k1::new();
-
-    let sk_path = [
-        ("dff1c8c2c016a572914b4c5adb8791d62b4768ae9d0a61be8ab94cf5038d7d90", "86'/1'/0'/0/0"),
-        ("1ede31b0e7e47c2afc65ffd158b1b1b9d3b752bba8fd117dc8b9e944a390e8d9", "86'/1'/0'/0/1"),
-        ("1fb777f1a6fb9b76724551f8bc8ad91b77f33b8c456d65d746035391d724922a", "86'/1'/0'/0/2"),
-    ];
-    let mfp = "73c5da0a";
-
-    //
-    // Step 0: Create P2TR address.
-    //
-
-    // Create three basic scripts to test script path spend.
-    let script1 = create_basic_single_sig_script(secp, sk_path[0].0); // m/86'/1'/0'/0/0
-    let script2 = create_basic_single_sig_script(secp, sk_path[1].0); // m/86'/1'/0'/0/1
-    let script3 = create_basic_single_sig_script(secp, sk_path[2].0); // m/86'/1'/0'/0/2
-
-    // Just use one of the secret keys for the key path spend.
-    let kp = Keypair::from_seckey_str(secp, &sk_path[2].0).expect("failed to create keypair");
-
-    let internal_key = kp.x_only_public_key().0; // Ignore the parity.
-
-    let tree = create_taproot_tree(secp, script1.clone(), script2.clone(), script3.clone(), internal_key);
-
-    let address = create_p2tr_address(tree.clone());
-    assert_eq!("tb1pytee2mxz0f4fkrsqqws2lsgnkp8nrw2atjkjy2n9gahggsphr0gszaxxmv", address.to_string());
-
-    // m/86'/1'/0'/0/7
-    let to_address = "tb1pyfv094rr0vk28lf8v9yx3veaacdzg26ztqk4ga84zucqqhafnn5q9my9rz";
-    let to_address = Address::from_str(to_address).unwrap().assume_checked();
-
-    // key path spend
-    {
-        //
-        // Step 1: create psbt for key path spend.
-        //
-        let mut psbt_key_path_spend = create_psbt_for_taproot_key_path_spend(address.clone(), to_address.clone(), tree.clone());
-
-        //
-        // Step 2: sign psbt.
-        //
-        let keystore = Keystore {
-            mfp: Fingerprint::from_str(mfp).unwrap(),
-            sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
-        };
-        let _ = psbt_key_path_spend.sign(&keystore, secp);
-
-        let sig = "92864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c87";
-        assert_eq!(sig, psbt_key_path_spend.inputs[0].tap_key_sig.unwrap().signature.to_string());
-
-        //
-        // Step 3: finalize psbt.
-        //
-        let final_psbt = finalize_psbt_for_key_path_spend(psbt_key_path_spend);
-        let tx = final_psbt.extract_tx().unwrap();
-
-        let tx_id = "5306516f2032d9f34c9f2f6d2b1b8ad2486ef1ba196d8d8d780e59773e48ad6d";
-        assert_eq!(tx_id, tx.compute_txid().to_string());
-
-        let tx_bytes = "020000000001013aee4d6b51da574900e56d173041115bd1e1d01d4697a845784cf716a10c98060000000000ffffffff0100190000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce8014092864dc9e56b6260ecbd54ec16b94bb597a2e6be7cca0de89d75e17921e0e1528cba32dd04217175c237e1835b5db1c8b384401718514f9443dce933c6ba9c8700000000";
-        let tx_hex = serialize_hex(&tx);
-        assert_eq!(tx_bytes, tx_hex);
-    }
-
-    // script path spend
-    {
-        // use private key of path "m/86'/1'/0'/0/1" as signing key
-        let kp = Keypair::from_seckey_str(secp, &sk_path[1].0).expect("failed to create keypair");
-        let x_only_pubkey = kp.x_only_public_key().0;
-        let signing_key_path = sk_path[1].1;
-
-        let keystore = Keystore {
-            mfp: Fingerprint::from_str(mfp).unwrap(),
-            sk: PrivateKey::new(kp.secret_key(), Network::Testnet),
-        };
-
-        //
-        // Step 1: create psbt for script path spend.
-        //
-        let mut psbt_script_path_spend = create_psbt_for_taproot_script_path_spend(address.clone(), to_address.clone(), tree.clone(), x_only_pubkey, signing_key_path, script2.clone());
-
-        //
-        // Step 2: sign psbt.
-        //
-        let _ = psbt_script_path_spend.sign(&keystore, secp);
-
-        let sig = "9c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac85727";
-        assert_eq!(sig, psbt_script_path_spend.inputs[0].tap_script_sigs.get(&(x_only_pubkey, script2.clone().tapscript_leaf_hash())).unwrap().signature.to_string());
-
-        //
-        // Step 3: finalize psbt.
-        //
-        let final_psbt = finalize_psbt_for_script_path_spend(psbt_script_path_spend);
-        let tx = final_psbt.extract_tx().unwrap();
-
-        let tx_id = "a51f723beffc810248809355ba9c9e4b39c6e55c08429f0aeaa79b73f18bc2a6";
-        assert_eq!(tx_id, tx.compute_txid().to_string());
-
-        let tx_hex = serialize_hex(&tx);
-        let tx_bytes = "0200000000010176a3c94a6b21d742e8ca192130ad10fdfc4c83510cb6baba8572a5fc70677c9d0000000000ffffffff0170170000000000002251202258f2d4637b2ca3fd27614868b33dee1a242b42582d5474f51730005fa99ce803419c1466e1631a58c55fcb8642ce5f7896314f4b565d92c5c80b17aa9abf56d22e0b5e5dcbcfe836bbd7d409491f58aa9e1f68a491ef8f05eef62fb50ffac857270122203058679f6d60b87ef921d98a2a9a1f1e0779dae27bedbd1cdb2f147a07835ac9ac61c1b68df382cad577d8304d5a8e640c3cb42d77c10016ab754caa4d6e68b6cb296d9b9d92a717ebeba858f75182936f0da5a7aecc434b0eebb2dc8a6af5409422ccf87f124e735a592a8ff390a68f6f05469ba8422e246dc78b0b57cd1576ffa98c00000000";
-        assert_eq!(tx_bytes, tx_hex);
-    }
-}
-
-fn create_basic_single_sig_script(secp: &Secp256k1::<secp256k1::All>, sk: &str) -> ScriptBuf {
-    let kp = Keypair::from_seckey_str(secp, sk).expect("failed to create keypair");
-    let x_only_pubkey = kp.x_only_public_key().0;
-    script::Builder::new()
-        .push_slice(x_only_pubkey.serialize())
-        .push_opcode(OP_CHECKSIG)
-        .into_script()
-}
-
-fn create_taproot_tree(secp: &Secp256k1::<secp256k1::All>, script1: ScriptBuf, script2: ScriptBuf, script3: ScriptBuf, internal_key: XOnlyPublicKey) -> TaprootSpendInfo {
-    let builder = TaprootBuilder::new();
-    let builder = builder.add_leaf(2, script1).unwrap();
-    let builder = builder.add_leaf(2, script2).unwrap();
-    let builder = builder.add_leaf(1, script3).unwrap();
-    builder.finalize(secp, internal_key).unwrap()
-}
-
-fn create_p2tr_address(tree: TaprootSpendInfo) -> Address {
-    let output_key = tree.output_key();
-    Address::p2tr_tweaked(output_key, Network::Testnet)
-}
-
-fn create_psbt_for_taproot_key_path_spend(from_address: Address, to_address: Address, tree: TaprootSpendInfo) -> Psbt {
-
-    let send_value = 6400;
-    let out_puts = vec![
-        TxOut { value: Amount::from_sat(send_value), script_pubkey: to_address.script_pubkey() },
-    ];
-    let prev_tx_id = "06980ca116f74c7845a897461dd0e1d15b114130176de5004957da516b4dee3a";
-
-    let transaction = Transaction {
-        version: Version(2),
-        lock_time: absolute::LockTime::ZERO,
-        input: vec![TxIn {
-            previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
-            script_sig: ScriptBuf::new(),
-            sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
-            witness: Witness::default(),
-        }],
-        output: out_puts,
-    };
-
-    let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
-
-
-    let mfp = "73c5da0a";
-    let internal_key_path = "86'/1'/0'/0/2";
-
-    let mut origins = BTreeMap::new();
-    origins.insert(
-        tree.internal_key(),
-        (
-            vec![],
-            (
-                Fingerprint::from_str(mfp).unwrap(),
-                DerivationPath::from_str(internal_key_path).unwrap(),
-            ),
-        ),
-    );
-
-    let utxo_value = 6588;
-    let mut input = Input {
-        witness_utxo: {
-            let script_pubkey = from_address.script_pubkey();
-            Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
-        },
-        tap_key_origins: origins,
-        ..Default::default()
-    };
-    let ty = PsbtSighashType::from_str("SIGHASH_DEFAULT").unwrap();
-    input.sighash_type = Some(ty);
-    input.tap_internal_key = Some(tree.internal_key());
-    input.tap_merkle_root = tree.merkle_root();
-    psbt.inputs = vec![input];
-    psbt
-}
-
-fn finalize_psbt_for_key_path_spend(mut psbt: Psbt) -> Psbt {
-    psbt.inputs.iter_mut().for_each(|input| {
-        let mut script_witness: Witness = Witness::new();
-        script_witness.push(input.tap_key_sig.unwrap().to_vec());
-        input.final_script_witness = Some(script_witness);
-        input.partial_sigs = BTreeMap::new();
-        input.sighash_type = None;
-        input.redeem_script = None;
-        input.witness_script = None;
-        input.bip32_derivation = BTreeMap::new();
-    });
-    psbt
-}
-
-fn create_psbt_for_taproot_script_path_spend(from_address: Address, to_address: Address, tree: TaprootSpendInfo, x_only_pubkey_of_signing_key: XOnlyPublicKey, signing_key_path: &str, use_script: ScriptBuf) -> Psbt {
-    let utxo_value = 6280;
-    let send_value = 6000;
-    let mfp = "73c5da0a";
-
-    let out_puts = vec![
-        TxOut { value: Amount::from_sat(send_value), script_pubkey: to_address.script_pubkey() },
-    ];
-    let prev_tx_id = "9d7c6770fca57285babab60c51834cfcfd10ad302119cae842d7216b4ac9a376";
-    let transaction = Transaction {
-        version: Version(2),
-        lock_time: absolute::LockTime::ZERO,
-        input: vec![TxIn {
-            previous_output: OutPoint { txid: prev_tx_id.parse().unwrap(), vout: 0 },
-            script_sig: ScriptBuf::new(),
-            sequence: Sequence(0xFFFFFFFF), // Ignore nSequence.
-            witness: Witness::default(),
-        }],
-        output: out_puts,
-    };
-
-    let mut psbt = Psbt::from_unsigned_tx(transaction).unwrap();
-
-    let mut origins = BTreeMap::new();
-    origins.insert(
-        x_only_pubkey_of_signing_key,
-        (
-            vec![use_script.tapscript_leaf_hash()],
-            (
-                Fingerprint::from_str(mfp).unwrap(),
-                DerivationPath::from_str(signing_key_path).unwrap(),
-            ),
-        ),
-    );
-
-    let mut tap_scripts = BTreeMap::new();
-    tap_scripts.insert(
-        tree.control_block(&(use_script.clone(), LeafVersion::TapScript)).unwrap(),
-        (use_script.clone(), LeafVersion::TapScript),
-    );
-
-    let mut input = Input {
-        witness_utxo: {
-            let script_pubkey= from_address.script_pubkey();
-            Some(TxOut { value: Amount::from_sat(utxo_value), script_pubkey })
-        },
-        tap_key_origins: origins,
-        tap_scripts,
-        ..Default::default()
-    };
-    let ty = PsbtSighashType::from_str("SIGHASH_ALL").unwrap();
-    input.sighash_type = Some(ty);
-    input.tap_internal_key = Some(tree.internal_key());
-    input.tap_merkle_root = tree.merkle_root();
-    psbt.inputs = vec![input];
-    psbt
-}
-
-
-fn finalize_psbt_for_script_path_spend(mut psbt: Psbt) -> Psbt {
-    psbt.inputs.iter_mut().for_each(|input| {
-        let mut script_witness: Witness = Witness::new();
-        for (_, signature) in input.tap_script_sigs.iter() {
-            script_witness.push(signature.to_vec());
-        }
-        for (control_block, (script, _)) in input.tap_scripts.iter() {
-            script_witness.push(script.to_bytes());
-            script_witness.push(control_block.serialize());
-        }
-        input.final_script_witness = Some(script_witness);
-        input.partial_sigs = BTreeMap::new();
-        input.sighash_type = None;
-        input.redeem_script = None;
-        input.witness_script = None;
-        input.bip32_derivation = BTreeMap::new();
-        input.tap_script_sigs = BTreeMap::new();
-        input.tap_scripts = BTreeMap::new();
-        input.tap_key_sig = None;
-    });
-    psbt
-}