Merge rust-bitcoin/rust-bitcoin#1225: Add `bitcoin-units` crate

396e049a7a Use InputString instead of String (Tobin C. Harding)
acacf45edf Add ParseDenominationError (Tobin C. Harding)
69e56a64ed Add bitcoin-units crate (Tobin C. Harding)
4ecb1fe7da internals: Add docs to InputString (Tobin C. Harding)
fa8d3002cd internals: Fix docs typo (Tobin C. Harding)

Pull request description:

  Create a new  `bitcoin-units` crate as described [here](https://github.com/rust-bitcoin/rust-bitcoin/issues/550#issuecomment-1012103022).

  Only the `amount` module is currently included.

  I've resolved the `Encodale/Decodable` issue by keeping the `amount` module in `bitcoin`.

ACKs for top commit:
  Kixunil:
    ACK 396e049a7a
  apoelstra:
    ACK 396e049a7a

Tree-SHA512: caf5e9da0458435ab19d00d4506896257e898525a4472d435fdac1d1a37bb747befd56993b106673f938475e5777d952a13ba04a2d3cb710d7afe7f5faebb7b8
This commit is contained in:
Andrew Poelstra 2023-12-11 17:34:42 +00:00
commit 4777ec9a90
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
19 changed files with 401 additions and 73 deletions

View File

@ -17,7 +17,6 @@ jobs:
matrix: matrix:
fuzz_target: [ fuzz_target: [
bitcoin_deserialize_address, bitcoin_deserialize_address,
bitcoin_deserialize_amount,
bitcoin_deserialize_block, bitcoin_deserialize_block,
bitcoin_deserialize_prefilled_transaction, bitcoin_deserialize_prefilled_transaction,
bitcoin_deserialize_psbt, bitcoin_deserialize_psbt,
@ -34,6 +33,7 @@ jobs:
hashes_sha256, hashes_sha256,
hashes_sha512_256, hashes_sha512_256,
hashes_sha512, hashes_sha512,
units_deserialize_amount,
] ]
steps: steps:
- name: Install test dependencies - name: Install test dependencies

View File

@ -39,6 +39,7 @@ dependencies = [
"bincode", "bincode",
"bitcoin-internals", "bitcoin-internals",
"bitcoin-io", "bitcoin-io",
"bitcoin-units",
"bitcoin_hashes", "bitcoin_hashes",
"bitcoinconsensus", "bitcoinconsensus",
"hex-conservative", "hex-conservative",
@ -72,6 +73,16 @@ dependencies = [
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "bitcoin-units"
version = "0.1.0"
dependencies = [
"bitcoin-internals",
"serde",
"serde_json",
"serde_test",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
version = "0.13.0" version = "0.13.0"

View File

@ -38,6 +38,7 @@ dependencies = [
"bincode", "bincode",
"bitcoin-internals", "bitcoin-internals",
"bitcoin-io", "bitcoin-io",
"bitcoin-units",
"bitcoin_hashes", "bitcoin_hashes",
"bitcoinconsensus", "bitcoinconsensus",
"hex-conservative", "hex-conservative",
@ -71,6 +72,16 @@ dependencies = [
name = "bitcoin-io" name = "bitcoin-io"
version = "0.1.0" version = "0.1.0"
[[package]]
name = "bitcoin-units"
version = "0.1.0"
dependencies = [
"bitcoin-internals",
"serde",
"serde_json",
"serde_test",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"
version = "0.13.0" version = "0.13.0"

View File

@ -1,5 +1,5 @@
[workspace] [workspace]
members = ["bitcoin", "hashes", "internals", "fuzz", "io"] members = ["bitcoin", "hashes", "internals", "fuzz", "io", "units"]
resolver = "2" resolver = "2"
[patch.crates-io.bitcoin] [patch.crates-io.bitcoin]
@ -13,3 +13,6 @@ path = "internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "io" path = "io"
[patch.crates-io.bitcoin-units]
path = "units"

View File

@ -15,10 +15,10 @@ exclude = ["tests", "contrib"]
[features] [features]
default = [ "std", "secp-recovery" ] default = [ "std", "secp-recovery" ]
std = ["bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "secp256k1/std"] std = ["bech32/std", "hashes/std", "hex/std", "internals/std", "io/std", "secp256k1/std", "units/std"]
rand-std = ["secp256k1/rand-std", "std"] rand-std = ["secp256k1/rand-std", "std"]
rand = ["secp256k1/rand"] rand = ["secp256k1/rand"]
serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde"] serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde", "units/serde"]
secp-lowmemory = ["secp256k1/lowmemory"] secp-lowmemory = ["secp256k1/lowmemory"]
secp-recovery = ["secp256k1/recovery"] secp-recovery = ["secp256k1/recovery"]
bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]
@ -35,6 +35,7 @@ hex_lit = "0.1.1"
internals = { package = "bitcoin-internals", version = "0.2.0" } internals = { package = "bitcoin-internals", version = "0.2.0" }
io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["alloc"] } io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["alloc"] }
secp256k1 = { version = "0.28.0", default-features = false, features = ["hashes", "alloc"] } secp256k1 = { version = "0.28.0", default-features = false, features = ["hashes", "alloc"] }
units = { package = "bitcoin-units", version = "0.1.0", default-features = false, features = ["alloc"] }
base64 = { version = "0.21.3", optional = true } base64 = { version = "0.21.3", optional = true }
# Only use this feature for no-std builds, otherwise use bitcoinconsensus-std. # Only use this feature for no-std builds, otherwise use bitcoinconsensus-std.

View File

@ -35,3 +35,6 @@ path = "../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../io" path = "../../io"
[patch.crates-io.bitcoin-units]
path = "../../units"

View File

@ -91,7 +91,6 @@ mod serde_utils;
#[macro_use] #[macro_use]
pub mod p2p; pub mod p2p;
pub mod address; pub mod address;
pub mod amount;
pub mod base58; pub mod base58;
pub mod bip152; pub mod bip152;
pub mod bip158; pub mod bip158;
@ -187,3 +186,35 @@ mod prelude {
pub use hex::DisplayHex; pub use hex::DisplayHex;
} }
pub mod amount {
//! Bitcoin amounts.
//!
//! This module mainly introduces the [Amount] and [SignedAmount] types.
//! We refer to the documentation on the types for more information.
use crate::consensus::{encode, Decodable, Encodable};
use crate::io;
#[rustfmt::skip] // Keep public re-exports separate.
#[doc(inline)]
pub use units::amount::{
Amount, CheckedSum, Denomination, Display, ParseAmountError, SignedAmount,
};
#[cfg(feature = "serde")]
pub use units::amount::serde;
impl Decodable for Amount {
#[inline]
fn consensus_decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
Ok(Amount::from_sat(Decodable::consensus_decode(r)?))
}
}
impl Encodable for Amount {
#[inline]
fn consensus_encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.to_sat().consensus_encode(w)
}
}
}

View File

@ -2,7 +2,7 @@
set -ex set -ex
CRATES="bitcoin hashes internals fuzz" CRATES="bitcoin hashes units internals fuzz"
DEPS="recent minimal" DEPS="recent minimal"
for dep in $DEPS for dep in $DEPS

View File

@ -21,10 +21,6 @@ serde_cbor = "0.9"
name = "bitcoin_deserialize_address" name = "bitcoin_deserialize_address"
path = "fuzz_targets/bitcoin/deserialize_address.rs" path = "fuzz_targets/bitcoin/deserialize_address.rs"
[[bin]]
name = "bitcoin_deserialize_amount"
path = "fuzz_targets/bitcoin/deserialize_amount.rs"
[[bin]] [[bin]]
name = "bitcoin_deserialize_block" name = "bitcoin_deserialize_block"
path = "fuzz_targets/bitcoin/deserialize_block.rs" path = "fuzz_targets/bitcoin/deserialize_block.rs"
@ -88,3 +84,7 @@ path = "fuzz_targets/hashes/sha512_256.rs"
[[bin]] [[bin]]
name = "hashes_sha512" name = "hashes_sha512"
path = "fuzz_targets/hashes/sha512.rs" path = "fuzz_targets/hashes/sha512.rs"
[[bin]]
name = "units_deserialize_amount"
path = "fuzz_targets/units/deserialize_amount.rs"

View File

@ -36,3 +36,6 @@ path = "../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../io" path = "../../io"
[patch.crates-io.bitcoin-units]
path = "../../units"

View File

@ -24,3 +24,6 @@ path = "../../../internals"
[patch.crates-io.bitcoin-io] [patch.crates-io.bitcoin-io]
path = "../../../io" path = "../../../io"
[patch.crates-io.bitcoin-units]
path = "../../../units"

View File

@ -17,7 +17,29 @@ pub struct InputString(Storage);
impl InputString { impl InputString {
/// Displays a message saying `failed to parse <self> as <what>`. /// Displays a message saying `failed to parse <self> as <what>`.
/// ///
/// This is normally used whith the `write_err!` macro. /// This is normally used with the `write_err!` macro.
///
/// # Examples
///
/// ```
/// use core::fmt;
/// use bitcoin_internals::error::InputString;
/// use bitcoin_internals::write_err;
///
/// /// An example parsing error including the parse error from core.
/// #[derive(Debug, Clone, PartialEq, Eq)]
/// pub struct ParseError {
/// input: InputString,
/// error: core::num::ParseIntError,
/// }
///
/// impl fmt::Display for ParseError {
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
/// // Outputs "failed to parse '<input string>' as foo"
/// write_err!(f, "{}", self.input.display_cannot_parse("foo"); self.error)
/// }
/// }
/// ```
pub fn display_cannot_parse<'a, T>(&'a self, what: &'a T) -> CannotParse<'a, T> pub fn display_cannot_parse<'a, T>(&'a self, what: &'a T) -> CannotParse<'a, T>
where where
T: fmt::Display + ?Sized, T: fmt::Display + ?Sized,
@ -28,6 +50,24 @@ impl InputString {
/// Formats a message saying `<self> is not a known <what>`. /// Formats a message saying `<self> is not a known <what>`.
/// ///
/// This is normally used in leaf parse errors (with no source) when parsing an enum. /// This is normally used in leaf parse errors (with no source) when parsing an enum.
///
/// # Examples
///
/// ```
/// use core::fmt;
/// use bitcoin_internals::error::InputString;
///
/// /// An example parsing error.
/// #[derive(Debug, Clone, PartialEq, Eq)]
/// pub struct ParseError(InputString);
///
/// impl fmt::Display for ParseError {
/// fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
/// // Outputs "'<input string>' is not a known foo"
/// self.0.unknown_variant("foo", f)
/// }
/// }
/// ```
pub fn unknown_variant<T>(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result pub fn unknown_variant<T>(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result
where where
T: fmt::Display + ?Sized, T: fmt::Display + ?Sized,

View File

@ -36,7 +36,7 @@ macro_rules! parse_error_type {
impl core::fmt::Display for $name { impl core::fmt::Display for $name {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
$crate::error::write_err!("{}", self.input.display_cannot_parse($subject); self.source) $crate::error::write_err!(f, "{}", self.input.display_cannot_parse($subject); self.source)
} }
} }

31
units/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "bitcoin-units"
version = "0.1.0"
authors = ["Andrew Poelstra <apoelstra@wpsoftware.net>"]
license = "CC0-1.0"
repository = "https://github.com/rust-bitcoin/rust-bitcoin/"
description = "Basic Bitcoin numeric units such as amount"
categories = ["cryptography::cryptocurrencies"]
keywords = ["bitcoin", "newtypes"]
readme = "README.md"
edition = "2021"
rust-version = "1.56.1"
exclude = ["tests", "contrib"]
[features]
default = ["std"]
std = ["alloc", "internals/std"]
alloc = ["internals/alloc"]
[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
[dependencies]
internals = { package = "bitcoin-internals", version = "0.2.0" }
serde = { version = "1.0.103", default-features = false, features = ["derive"], optional = true }
[dev-dependencies]
serde_test = "1.0"
serde_json = "1.0"

15
units/README.md Normal file
View File

@ -0,0 +1,15 @@
Bitcoin Units
=============
This crate provides basic Bitcoin numeric units such as `Amount`.
## Minimum Supported Rust Version (MSRV)
This library should always compile with any combination of features on **Rust 1.56.1**.
## Licensing
The code in this project is licensed under the [Creative Commons CC0 1.0 Universal license](LICENSE).
We use the [SPDX license list](https://spdx.org/licenses/) and [SPDX IDs](https://spdx.dev/ids/).

79
units/contrib/test.sh Executable file
View File

@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -ex
FEATURES="std alloc serde"
cargo --version
rustc --version
# Work out if we are using a nightly toolchain.
NIGHTLY=false
if cargo --version | grep nightly >/dev/null; then
NIGHTLY=true
fi
# Make all cargo invocations verbose
export CARGO_TERM_VERBOSE=true
# Defaults / sanity checks
cargo build
cargo test
if [ "$DO_LINT" = true ]
then
cargo clippy --locked --all-features --all-targets -- -D warnings
fi
if [ "$DO_FEATURE_MATRIX" = true ]; then
# No features
cargo build --locked --no-default-features
cargo test --locked --no-default-features
# Default features (this is std and alloc)
cargo build --locked
cargo test --locked
# All features
cargo build --locked --no-default-features --all-features
cargo test --locked --no-default-features --all-features
fi
REPO_DIR=$(git rev-parse --show-toplevel)
# Build the docs if told to (this only works with the nightly toolchain)
if [ "$DO_DOCSRS" = true ]; then
RUSTDOCFLAGS="--cfg docsrs -D warnings -D rustdoc::broken-intra-doc-links" cargo +nightly doc --all-features
fi
# Build the docs with a stable toolchain, in unison with the DO_DOCSRS command
# above this checks that we feature guarded docs imports correctly.
if [ "$DO_DOCS" = true ]; then
RUSTDOCFLAGS="-D warnings" cargo +stable doc --all-features
fi
# Run formatter if told to.
if [ "$DO_FMT" = true ]; then
if [ "$NIGHTLY" = false ]; then
echo "DO_FMT requires a nightly toolchain (consider using RUSTUP_TOOLCHAIN)"
exit 1
fi
rustup component add rustfmt
cargo fmt --check
fi
# Bench if told to, only works with non-stable toolchain (nightly, beta).
if [ "$DO_BENCH" = true ]
then
if [ "$NIGHTLY" = false ]
then
if [ -n "$RUSTUP_TOOLCHAIN" ]
then
echo "RUSTUP_TOOLCHAIN is set to a non-nightly toolchain but DO_BENCH requires a nightly toolchain"
else
echo "DO_BENCH requires a nightly toolchain"
fi
exit 1
fi
RUSTFLAGS='--cfg=bench' cargo bench
fi

View File

@ -4,22 +4,26 @@
//! //!
//! This module mainly introduces the [Amount] and [SignedAmount] types. //! This module mainly introduces the [Amount] and [SignedAmount] types.
//! We refer to the documentation on the types for more information. //! We refer to the documentation on the types for more information.
//!
use core::cmp::Ordering; use core::cmp::Ordering;
use core::fmt::{self, Write}; use core::fmt::{self, Write};
use core::str::FromStr; use core::str::FromStr;
use core::{default, ops}; use core::{default, ops};
use crate::consensus::encode::{self, Decodable, Encodable}; #[cfg(feature = "serde")]
use crate::prelude::*; use ::serde::{Deserialize, Serialize};
use internals::error::InputString;
use internals::write_err;
#[cfg(feature = "alloc")]
use crate::prelude::{String, ToString};
/// A set of denominations in which amounts can be expressed. /// A set of denominations in which amounts can be expressed.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use core::str::FromStr; /// # use core::str::FromStr;
/// # use bitcoin::Amount; /// # use bitcoin_units::Amount;
/// ///
/// assert_eq!(Amount::from_str("1 BTC").unwrap(), Amount::from_sat(100_000_000)); /// assert_eq!(Amount::from_str("1 BTC").unwrap(), Amount::from_sat(100_000_000));
/// assert_eq!(Amount::from_str("1 cBTC").unwrap(), Amount::from_sat(1_000_000)); /// assert_eq!(Amount::from_str("1 cBTC").unwrap(), Amount::from_sat(1_000_000));
@ -113,7 +117,7 @@ impl fmt::Display for Denomination {
} }
impl FromStr for Denomination { impl FromStr for Denomination {
type Err = ParseAmountError; type Err = ParseDenominationError;
/// Convert from a str to Denomination. /// Convert from a str to Denomination.
/// ///
@ -123,15 +127,15 @@ impl FromStr for Denomination {
/// ///
/// Due to ambiguity between mega and milli, pico and peta we prohibit usage of leading capital 'M', 'P'. /// Due to ambiguity between mega and milli, pico and peta we prohibit usage of leading capital 'M', 'P'.
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::ParseAmountError::*; use self::ParseDenominationError::*;
if CONFUSING_FORMS.contains(&s) { if CONFUSING_FORMS.contains(&s) {
return Err(PossiblyConfusingDenomination(s.to_owned())); return Err(PossiblyConfusing(PossiblyConfusingDenominationError(s.into())));
}; };
let form = self::Denomination::forms(s); let form = self::Denomination::forms(s);
form.ok_or_else(|| UnknownDenomination(s.to_owned())) form.ok_or_else(|| Unknown(UnknownDenominationError(s.into())))
} }
} }
@ -151,10 +155,8 @@ pub enum ParseAmountError {
InputTooLarge, InputTooLarge,
/// Invalid character in input. /// Invalid character in input.
InvalidCharacter(char), InvalidCharacter(char),
/// The denomination was unknown. /// Invalid denomination.
UnknownDenomination(String), InvalidDenomination(ParseDenominationError),
/// The denomination has multiple possible interpretations.
PossiblyConfusingDenomination(String),
} }
impl fmt::Display for ParseAmountError { impl fmt::Display for ParseAmountError {
@ -168,16 +170,7 @@ impl fmt::Display for ParseAmountError {
InvalidFormat => f.write_str("invalid number format"), InvalidFormat => f.write_str("invalid number format"),
InputTooLarge => f.write_str("input string was too large"), InputTooLarge => f.write_str("input string was too large"),
InvalidCharacter(c) => write!(f, "invalid character in input: {}", c), InvalidCharacter(c) => write!(f, "invalid character in input: {}", c),
UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d), InvalidDenomination(ref e) => write_err!(f, "invalid denomination"; e),
PossiblyConfusingDenomination(ref d) => {
let (letter, upper, lower) = match d.chars().next() {
Some('M') => ('M', "Mega", "milli"),
Some('P') => ('P', "Peta", "pico"),
// This panic could be avoided by adding enum ConfusingDenomination { Mega, Peta } but is it worth it?
_ => panic!("invalid error information"),
};
write!(f, "the '{}' at the beginning of {} should technically mean '{}' but that denomination is uncommon and maybe '{}' was intended", letter, d, upper, lower)
}
} }
} }
} }
@ -188,18 +181,81 @@ impl std::error::Error for ParseAmountError {
use ParseAmountError::*; use ParseAmountError::*;
match *self { match *self {
Negative Negative | TooBig | TooPrecise | InvalidFormat | InputTooLarge
| TooBig | InvalidCharacter(_) => None,
| TooPrecise InvalidDenomination(ref e) => Some(e),
| InvalidFormat
| InputTooLarge
| InvalidCharacter(_)
| UnknownDenomination(_)
| PossiblyConfusingDenomination(_) => None,
} }
} }
} }
impl From<ParseDenominationError> for ParseAmountError {
fn from(e: ParseDenominationError) -> Self { Self::InvalidDenomination(e) }
}
/// An error during amount parsing.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseDenominationError {
/// The denomination was unknown.
Unknown(UnknownDenominationError),
/// The denomination has multiple possible interpretations.
PossiblyConfusing(PossiblyConfusingDenominationError),
}
impl fmt::Display for ParseDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
use ParseDenominationError::*;
match *self {
Unknown(ref e) => write_err!(f, "denomination parse error"; e),
PossiblyConfusing(ref e) => write_err!(f, "denomination parse error"; e),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for ParseDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
use ParseDenominationError::*;
match *self {
Unknown(_) | PossiblyConfusing(_) => None,
}
}
}
/// Parsing error, unknown denomination.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct UnknownDenominationError(InputString);
impl fmt::Display for UnknownDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.unknown_variant("bitcoin denomination", f)
}
}
#[cfg(feature = "std")]
impl std::error::Error for UnknownDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
/// Parsing error, possibly confusing denomination.
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct PossiblyConfusingDenominationError(InputString);
impl fmt::Display for PossiblyConfusingDenominationError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}: possibly confusing denomination - we intentionally do not support 'M' and 'P' so as to not confuse mega/milli and peta/pico", self.0.display_cannot_parse("bitcoin denomination"))
}
}
#[cfg(feature = "std")]
impl std::error::Error for PossiblyConfusingDenominationError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { None }
}
fn is_too_precise(s: &str, precision: usize) -> bool { fn is_too_precise(s: &str, precision: usize) -> bool {
match s.find('.') { match s.find('.') {
Some(pos) => Some(pos) =>
@ -493,7 +549,6 @@ fn fmt_satoshi_in(
/// ///
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
pub struct Amount(u64); pub struct Amount(u64);
impl Amount { impl Amount {
@ -582,7 +637,7 @@ impl Amount {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use bitcoin::{Amount, Denomination}; /// # use bitcoin_units::amount::{Amount, Denomination};
/// let amount = Amount::from_sat(100_000); /// let amount = Amount::from_sat(100_000);
/// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin)) /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin))
/// ``` /// ```
@ -687,20 +742,6 @@ impl Amount {
} }
} }
impl Decodable for Amount {
#[inline]
fn consensus_decode<R: io::Read + ?Sized>(r: &mut R) -> Result<Self, encode::Error> {
Ok(Amount(Decodable::consensus_decode(r)?))
}
}
impl Encodable for Amount {
#[inline]
fn consensus_encode<W: io::Write + ?Sized>(&self, w: &mut W) -> Result<usize, io::Error> {
self.0.consensus_encode(w)
}
}
impl default::Default for Amount { impl default::Default for Amount {
fn default() -> Self { Amount::ZERO } fn default() -> Self { Amount::ZERO }
} }
@ -1215,12 +1256,11 @@ pub mod serde {
//! //!
//! ```rust,ignore //! ```rust,ignore
//! use serde::{Serialize, Deserialize}; //! use serde::{Serialize, Deserialize};
//! use bitcoin::Amount; //! use bitcoin_units::Amount;
//! //!
//! #[derive(Serialize, Deserialize)] //! #[derive(Serialize, Deserialize)]
//! # #[serde(crate = "actual_serde")]
//! pub struct HasAmount { //! pub struct HasAmount {
//! #[serde(with = "bitcoin::amount::serde::as_btc")] //! #[serde(with = "bitcoin_units::amount::serde::as_btc")]
//! pub amount: Amount, //! pub amount: Amount,
//! } //! }
//! ``` //! ```
@ -1979,13 +2019,24 @@ mod tests {
#[test] #[test]
#[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin.
fn from_str() { fn from_str() {
use ParseDenominationError::*;
use super::ParseAmountError as E; use super::ParseAmountError as E;
assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x'))); assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x')));
assert_eq!(Amount::from_str("xBTC"), Err(E::UnknownDenomination("xBTC".into()))); assert_eq!(
assert_eq!(Amount::from_str("5 BTC BTC"), Err(E::UnknownDenomination("BTC BTC".into()))); Amount::from_str("xBTC"),
Err(Unknown(UnknownDenominationError("xBTC".into())).into()),
);
assert_eq!(
Amount::from_str("5 BTC BTC"),
Err(Unknown(UnknownDenominationError("BTC BTC".into())).into()),
);
assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B'))); assert_eq!(Amount::from_str("5BTC BTC"), Err(E::InvalidCharacter('B')));
assert_eq!(Amount::from_str("5 5 BTC"), Err(E::UnknownDenomination("5 BTC".into()))); assert_eq!(
Amount::from_str("5 5 BTC"),
Err(Unknown(UnknownDenominationError("5 BTC".into())).into()),
);
#[track_caller] #[track_caller]
fn case(s: &str, expected: Result<Amount, ParseAmountError>) { fn case(s: &str, expected: Result<Amount, ParseAmountError>) {
@ -1999,7 +2050,7 @@ mod tests {
assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected); assert_eq!(SignedAmount::from_str(&s.replace(' ', "")), expected);
} }
case("5 BCH", Err(E::UnknownDenomination("BCH".to_owned()))); case("5 BCH", Err(Unknown(UnknownDenominationError("BCH".into())).into()));
case("-1 BTC", Err(E::Negative)); case("-1 BTC", Err(E::Negative));
case("-0.0 BTC", Err(E::Negative)); case("-0.0 BTC", Err(E::Negative));
@ -2109,7 +2160,10 @@ mod tests {
#[test] #[test]
fn to_string_with_denomination_from_str_roundtrip() { fn to_string_with_denomination_from_str_roundtrip() {
use ParseDenominationError::*;
use super::Denomination as D; use super::Denomination as D;
let amt = Amount::from_sat(42); let amt = Amount::from_sat(42);
let denom = Amount::to_string_with_denomination; let denom = Amount::to_string_with_denomination;
assert_eq!(Amount::from_str(&denom(amt, D::Bitcoin)), Ok(amt)); assert_eq!(Amount::from_str(&denom(amt, D::Bitcoin)), Ok(amt));
@ -2123,11 +2177,11 @@ mod tests {
assert_eq!( assert_eq!(
Amount::from_str("42 satoshi BTC"), Amount::from_str("42 satoshi BTC"),
Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
); );
assert_eq!( assert_eq!(
SignedAmount::from_str("-42 satoshi BTC"), SignedAmount::from_str("-42 satoshi BTC"),
Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()),
); );
} }
@ -2135,7 +2189,6 @@ mod tests {
#[test] #[test]
fn serde_as_sat() { fn serde_as_sat() {
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(with = "crate::amount::serde::as_sat")] #[serde(with = "crate::amount::serde::as_sat")]
pub amt: Amount, pub amt: Amount,
@ -2163,7 +2216,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(with = "crate::amount::serde::as_btc")] #[serde(with = "crate::amount::serde::as_btc")]
pub amt: Amount, pub amt: Amount,
@ -2199,7 +2251,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(default, with = "crate::amount::serde::as_btc::opt")] #[serde(default, with = "crate::amount::serde::as_btc::opt")]
pub amt: Option<Amount>, pub amt: Option<Amount>,
@ -2241,7 +2292,6 @@ mod tests {
use serde_json; use serde_json;
#[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)]
#[serde(crate = "actual_serde")]
struct T { struct T {
#[serde(default, with = "crate::amount::serde::as_sat::opt")] #[serde(default, with = "crate::amount::serde::as_sat::opt")]
pub amt: Option<Amount>, pub amt: Option<Amount>,
@ -2351,7 +2401,7 @@ mod tests {
for denom in confusing.iter() { for denom in confusing.iter() {
match Denomination::from_str(denom) { match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom), Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseAmountError::PossiblyConfusingDenomination(_)) => {} Err(ParseDenominationError::PossiblyConfusing(_)) => {}
Err(e) => panic!("unexpected error: {}", e), Err(e) => panic!("unexpected error: {}", e),
} }
} }
@ -2364,7 +2414,7 @@ mod tests {
for denom in unknown.iter() { for denom in unknown.iter() {
match Denomination::from_str(denom) { match Denomination::from_str(denom) {
Ok(_) => panic!("from_str should error for {}", denom), Ok(_) => panic!("from_str should error for {}", denom),
Err(ParseAmountError::UnknownDenomination(_)) => {} Err(ParseDenominationError::Unknown(_)) => {}
Err(e) => panic!("unexpected error: {}", e), Err(e) => panic!("unexpected error: {}", e),
} }
} }

47
units/src/lib.rs Normal file
View File

@ -0,0 +1,47 @@
// SPDX-License-Identifier: CC0-1.0
//! Rust Bitcoin units library
//!
//! This library provides basic types used by the Rust Bitcoin ecosystem.
#![cfg_attr(all(not(test), not(feature = "std")), no_std)]
// Experimental features we need.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
// Coding conventions
#![warn(missing_docs)]
// Exclude clippy lints we don't think are valuable
#![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
// Disable 16-bit support at least for now as we can't guarantee it yet.
#[cfg(target_pointer_width = "16")]
compile_error!(
"rust-bitcoin currently only supports architectures with pointers wider than 16 bits, let us
know if you want 16-bit support. Note that we do NOT guarantee that we will implement it!"
);
#[cfg(all(feature = "alloc", not(feature = "std")))]
extern crate alloc;
#[cfg(not(feature = "std"))]
extern crate core;
/// A generic serialization/deserialization framework.
#[cfg(feature = "serde")]
pub extern crate serde;
// TODO: Make amount module less dependent on an allocator.
#[cfg(feature = "alloc")]
pub mod amount;
#[cfg(feature = "alloc")]
#[doc(inline)]
pub use self::amount::{Amount, ParseAmountError, SignedAmount};
#[rustfmt::skip]
mod prelude {
#[cfg(all(feature = "alloc", not(feature = "std"), not(test)))]
pub use alloc::{string::{String, ToString}, borrow::ToOwned};
#[cfg(any(feature = "std", test))]
pub use std::{string::{String, ToString}, borrow::ToOwned};
}