diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 1e7f58f2..865f7c92 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -17,7 +17,6 @@ jobs: matrix: fuzz_target: [ bitcoin_deserialize_address, - bitcoin_deserialize_amount, bitcoin_deserialize_block, bitcoin_deserialize_prefilled_transaction, bitcoin_deserialize_psbt, @@ -34,6 +33,7 @@ jobs: hashes_sha256, hashes_sha512_256, hashes_sha512, + units_deserialize_amount, ] steps: - name: Install test dependencies diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index c8fcbe8f..1cae13ca 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -39,6 +39,7 @@ dependencies = [ "bincode", "bitcoin-internals", "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", "bitcoinconsensus", "hex-conservative", @@ -72,6 +73,16 @@ dependencies = [ name = "bitcoin-io" version = "0.1.0" +[[package]] +name = "bitcoin-units" +version = "0.1.0" +dependencies = [ + "bitcoin-internals", + "serde", + "serde_json", + "serde_test", +] + [[package]] name = "bitcoin_hashes" version = "0.13.0" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index c6af81cf..90a84ae2 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -38,6 +38,7 @@ dependencies = [ "bincode", "bitcoin-internals", "bitcoin-io", + "bitcoin-units", "bitcoin_hashes", "bitcoinconsensus", "hex-conservative", @@ -71,6 +72,16 @@ dependencies = [ name = "bitcoin-io" version = "0.1.0" +[[package]] +name = "bitcoin-units" +version = "0.1.0" +dependencies = [ + "bitcoin-internals", + "serde", + "serde_json", + "serde_test", +] + [[package]] name = "bitcoin_hashes" version = "0.13.0" diff --git a/Cargo.toml b/Cargo.toml index dda25c25..77469170 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["bitcoin", "hashes", "internals", "fuzz", "io"] +members = ["bitcoin", "hashes", "internals", "fuzz", "io", "units"] resolver = "2" [patch.crates-io.bitcoin] @@ -13,3 +13,6 @@ path = "internals" [patch.crates-io.bitcoin-io] path = "io" + +[patch.crates-io.bitcoin-units] +path = "units" diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index 92341af9..f06cc572 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -15,10 +15,10 @@ exclude = ["tests", "contrib"] [features] 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 = ["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-recovery = ["secp256k1/recovery"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] @@ -35,6 +35,7 @@ hex_lit = "0.1.1" internals = { package = "bitcoin-internals", version = "0.2.0" } io = { package = "bitcoin-io", version = "0.1", default-features = false, features = ["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 } # Only use this feature for no-std builds, otherwise use bitcoinconsensus-std. diff --git a/bitcoin/embedded/Cargo.toml b/bitcoin/embedded/Cargo.toml index 0442c8fc..3f26fce5 100644 --- a/bitcoin/embedded/Cargo.toml +++ b/bitcoin/embedded/Cargo.toml @@ -35,3 +35,6 @@ path = "../../internals" [patch.crates-io.bitcoin-io] path = "../../io" + +[patch.crates-io.bitcoin-units] +path = "../../units" diff --git a/bitcoin/src/lib.rs b/bitcoin/src/lib.rs index 39adb07e..d65fe1e6 100644 --- a/bitcoin/src/lib.rs +++ b/bitcoin/src/lib.rs @@ -91,7 +91,6 @@ mod serde_utils; #[macro_use] pub mod p2p; pub mod address; -pub mod amount; pub mod base58; pub mod bip152; pub mod bip158; @@ -187,3 +186,35 @@ mod prelude { 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: &mut R) -> Result { + Ok(Amount::from_sat(Decodable::consensus_decode(r)?)) + } + } + + impl Encodable for Amount { + #[inline] + fn consensus_encode(&self, w: &mut W) -> Result { + self.to_sat().consensus_encode(w) + } + } +} diff --git a/contrib/test.sh b/contrib/test.sh index 1e40fb1e..243c3927 100755 --- a/contrib/test.sh +++ b/contrib/test.sh @@ -2,7 +2,7 @@ set -ex -CRATES="bitcoin hashes internals fuzz" +CRATES="bitcoin hashes units internals fuzz" DEPS="recent minimal" for dep in $DEPS diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 9abf5cd6..8d7dd702 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -21,10 +21,6 @@ serde_cbor = "0.9" name = "bitcoin_deserialize_address" path = "fuzz_targets/bitcoin/deserialize_address.rs" -[[bin]] -name = "bitcoin_deserialize_amount" -path = "fuzz_targets/bitcoin/deserialize_amount.rs" - [[bin]] name = "bitcoin_deserialize_block" path = "fuzz_targets/bitcoin/deserialize_block.rs" @@ -88,3 +84,7 @@ path = "fuzz_targets/hashes/sha512_256.rs" [[bin]] name = "hashes_sha512" path = "fuzz_targets/hashes/sha512.rs" + +[[bin]] +name = "units_deserialize_amount" +path = "fuzz_targets/units/deserialize_amount.rs" diff --git a/fuzz/fuzz_targets/bitcoin/deserialize_amount.rs b/fuzz/fuzz_targets/units/deserialize_amount.rs similarity index 100% rename from fuzz/fuzz_targets/bitcoin/deserialize_amount.rs rename to fuzz/fuzz_targets/units/deserialize_amount.rs diff --git a/hashes/embedded/Cargo.toml b/hashes/embedded/Cargo.toml index bd3e388d..0aa39f0a 100644 --- a/hashes/embedded/Cargo.toml +++ b/hashes/embedded/Cargo.toml @@ -36,3 +36,6 @@ path = "../../internals" [patch.crates-io.bitcoin-io] path = "../../io" + +[patch.crates-io.bitcoin-units] +path = "../../units" diff --git a/hashes/extended_tests/schemars/Cargo.toml b/hashes/extended_tests/schemars/Cargo.toml index d25cec04..8d262dff 100644 --- a/hashes/extended_tests/schemars/Cargo.toml +++ b/hashes/extended_tests/schemars/Cargo.toml @@ -24,3 +24,6 @@ path = "../../../internals" [patch.crates-io.bitcoin-io] path = "../../../io" + +[patch.crates-io.bitcoin-units] +path = "../../../units" diff --git a/internals/src/error/input_string.rs b/internals/src/error/input_string.rs index b69751c0..2949d17d 100644 --- a/internals/src/error/input_string.rs +++ b/internals/src/error/input_string.rs @@ -17,7 +17,29 @@ pub struct InputString(Storage); impl InputString { /// Displays a message saying `failed to parse as `. /// - /// 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 '' 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> where T: fmt::Display + ?Sized, @@ -28,6 +50,24 @@ impl InputString { /// Formats a message saying ` is not a known `. /// /// 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 "'' is not a known foo" + /// self.0.unknown_variant("foo", f) + /// } + /// } + /// ``` pub fn unknown_variant(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result where T: fmt::Display + ?Sized, diff --git a/internals/src/error/parse_error.rs b/internals/src/error/parse_error.rs index 7bcf8ceb..d8a99d21 100644 --- a/internals/src/error/parse_error.rs +++ b/internals/src/error/parse_error.rs @@ -36,7 +36,7 @@ macro_rules! parse_error_type { impl core::fmt::Display for $name { 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) } } diff --git a/units/Cargo.toml b/units/Cargo.toml new file mode 100644 index 00000000..6f677deb --- /dev/null +++ b/units/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "bitcoin-units" +version = "0.1.0" +authors = ["Andrew Poelstra "] +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" diff --git a/units/README.md b/units/README.md new file mode 100644 index 00000000..4b419c4f --- /dev/null +++ b/units/README.md @@ -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/). diff --git a/units/contrib/test.sh b/units/contrib/test.sh new file mode 100755 index 00000000..e23decf7 --- /dev/null +++ b/units/contrib/test.sh @@ -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 diff --git a/bitcoin/src/amount.rs b/units/src/amount.rs similarity index 95% rename from bitcoin/src/amount.rs rename to units/src/amount.rs index 496245ed..e61cfaa6 100644 --- a/bitcoin/src/amount.rs +++ b/units/src/amount.rs @@ -4,22 +4,26 @@ //! //! This module mainly introduces the [Amount] and [SignedAmount] types. //! We refer to the documentation on the types for more information. -//! use core::cmp::Ordering; use core::fmt::{self, Write}; use core::str::FromStr; use core::{default, ops}; -use crate::consensus::encode::{self, Decodable, Encodable}; -use crate::prelude::*; +#[cfg(feature = "serde")] +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. /// /// # Examples /// ``` /// # 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 cBTC").unwrap(), Amount::from_sat(1_000_000)); @@ -113,7 +117,7 @@ impl fmt::Display for Denomination { } impl FromStr for Denomination { - type Err = ParseAmountError; + type Err = ParseDenominationError; /// 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'. fn from_str(s: &str) -> Result { - use self::ParseAmountError::*; + use self::ParseDenominationError::*; if CONFUSING_FORMS.contains(&s) { - return Err(PossiblyConfusingDenomination(s.to_owned())); + return Err(PossiblyConfusing(PossiblyConfusingDenominationError(s.into()))); }; 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, /// Invalid character in input. InvalidCharacter(char), - /// The denomination was unknown. - UnknownDenomination(String), - /// The denomination has multiple possible interpretations. - PossiblyConfusingDenomination(String), + /// Invalid denomination. + InvalidDenomination(ParseDenominationError), } impl fmt::Display for ParseAmountError { @@ -168,16 +170,7 @@ impl fmt::Display for ParseAmountError { InvalidFormat => f.write_str("invalid number format"), InputTooLarge => f.write_str("input string was too large"), InvalidCharacter(c) => write!(f, "invalid character in input: {}", c), - UnknownDenomination(ref d) => write!(f, "unknown denomination: {}", d), - 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) - } + InvalidDenomination(ref e) => write_err!(f, "invalid denomination"; e), } } } @@ -188,18 +181,81 @@ impl std::error::Error for ParseAmountError { use ParseAmountError::*; match *self { - Negative - | TooBig - | TooPrecise - | InvalidFormat - | InputTooLarge - | InvalidCharacter(_) - | UnknownDenomination(_) - | PossiblyConfusingDenomination(_) => None, + Negative | TooBig | TooPrecise | InvalidFormat | InputTooLarge + | InvalidCharacter(_) => None, + InvalidDenomination(ref e) => Some(e), } } } +impl From 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 { match s.find('.') { Some(pos) => @@ -493,7 +549,6 @@ fn fmt_satoshi_in( /// #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] -#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))] pub struct Amount(u64); impl Amount { @@ -582,7 +637,7 @@ impl Amount { /// /// # Examples /// ``` - /// # use bitcoin::{Amount, Denomination}; + /// # use bitcoin_units::amount::{Amount, Denomination}; /// let amount = Amount::from_sat(100_000); /// 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: &mut R) -> Result { - Ok(Amount(Decodable::consensus_decode(r)?)) - } -} - -impl Encodable for Amount { - #[inline] - fn consensus_encode(&self, w: &mut W) -> Result { - self.0.consensus_encode(w) - } -} - impl default::Default for Amount { fn default() -> Self { Amount::ZERO } } @@ -1215,12 +1256,11 @@ pub mod serde { //! //! ```rust,ignore //! use serde::{Serialize, Deserialize}; - //! use bitcoin::Amount; + //! use bitcoin_units::Amount; //! //! #[derive(Serialize, Deserialize)] - //! # #[serde(crate = "actual_serde")] //! pub struct HasAmount { - //! #[serde(with = "bitcoin::amount::serde::as_btc")] + //! #[serde(with = "bitcoin_units::amount::serde::as_btc")] //! pub amount: Amount, //! } //! ``` @@ -1979,13 +2019,24 @@ mod tests { #[test] #[allow(clippy::inconsistent_digit_grouping)] // Group to show 100,000,000 sats per bitcoin. fn from_str() { + use ParseDenominationError::*; + use super::ParseAmountError as E; assert_eq!(Amount::from_str("x BTC"), Err(E::InvalidCharacter('x'))); - assert_eq!(Amount::from_str("xBTC"), Err(E::UnknownDenomination("xBTC".into()))); - assert_eq!(Amount::from_str("5 BTC BTC"), Err(E::UnknownDenomination("BTC BTC".into()))); + assert_eq!( + 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("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] fn case(s: &str, expected: Result) { @@ -1999,7 +2050,7 @@ mod tests { 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("-0.0 BTC", Err(E::Negative)); @@ -2109,7 +2160,10 @@ mod tests { #[test] fn to_string_with_denomination_from_str_roundtrip() { + use ParseDenominationError::*; + use super::Denomination as D; + let amt = Amount::from_sat(42); let denom = Amount::to_string_with_denomination; assert_eq!(Amount::from_str(&denom(amt, D::Bitcoin)), Ok(amt)); @@ -2123,11 +2177,11 @@ mod tests { assert_eq!( Amount::from_str("42 satoshi BTC"), - Err(ParseAmountError::UnknownDenomination("satoshi BTC".into())), + Err(Unknown(UnknownDenominationError("satoshi BTC".into())).into()), ); assert_eq!( 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] fn serde_as_sat() { #[derive(Serialize, Deserialize, PartialEq, Debug)] - #[serde(crate = "actual_serde")] struct T { #[serde(with = "crate::amount::serde::as_sat")] pub amt: Amount, @@ -2163,7 +2216,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug)] - #[serde(crate = "actual_serde")] struct T { #[serde(with = "crate::amount::serde::as_btc")] pub amt: Amount, @@ -2199,7 +2251,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] - #[serde(crate = "actual_serde")] struct T { #[serde(default, with = "crate::amount::serde::as_btc::opt")] pub amt: Option, @@ -2241,7 +2292,6 @@ mod tests { use serde_json; #[derive(Serialize, Deserialize, PartialEq, Debug, Eq)] - #[serde(crate = "actual_serde")] struct T { #[serde(default, with = "crate::amount::serde::as_sat::opt")] pub amt: Option, @@ -2351,7 +2401,7 @@ mod tests { for denom in confusing.iter() { match Denomination::from_str(denom) { Ok(_) => panic!("from_str should error for {}", denom), - Err(ParseAmountError::PossiblyConfusingDenomination(_)) => {} + Err(ParseDenominationError::PossiblyConfusing(_)) => {} Err(e) => panic!("unexpected error: {}", e), } } @@ -2364,7 +2414,7 @@ mod tests { for denom in unknown.iter() { match Denomination::from_str(denom) { Ok(_) => panic!("from_str should error for {}", denom), - Err(ParseAmountError::UnknownDenomination(_)) => {} + Err(ParseDenominationError::Unknown(_)) => {} Err(e) => panic!("unexpected error: {}", e), } } diff --git a/units/src/lib.rs b/units/src/lib.rs new file mode 100644 index 00000000..b3afb43a --- /dev/null +++ b/units/src/lib.rs @@ -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}; +}