Merge rust-bitcoin/rust-bitcoin#2408: Provide `Amount` & co in no-alloc

ac26171c32 Clean up `no_std` usage (Martin Habovstiak)
fce03cec85 Provide `Amount` & co in no-alloc (Martin Habovstiak)

Pull request description:

  Using the crate without allocation was previously disabled making the
  crate empty without the feature. This chage makes it more fine-grained:
  it only disables string and float conversions which use allocator. We
  could later provide float conversions by using a sufficiently-long
  `ArrayString`.

  Note that this is API-breaking because we disallow calling the methods of the sealed `SerdeAmount` trait. However I think it should've been obvious that the thing is internal and calling them is not a great idea. (BTW I only learned this trick recently).

  Closes #2389

ACKs for top commit:
  apoelstra:
    ACK ac26171c32 maaybe `private` should be renamed to `internal_api` or something
  tcharding:
    ACK ac26171c32

Tree-SHA512: 2ca2ff11c3c362f868c3993b5698ace32e6ce2cf2e8028bc43fe65797eb2239b4841c1c722a0184a7c8f4afea3b475b1a049dd77fa30358cb742d60463018e9f
This commit is contained in:
Andrew Poelstra 2024-01-28 23:07:54 +00:00
commit d883c3d5a2
No known key found for this signature in database
GPG Key ID: C588D63CE41B97C1
2 changed files with 117 additions and 76 deletions

View File

@ -6,7 +6,9 @@
//! 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 as _}; use core::fmt;
#[cfg(feature = "alloc")]
use core::fmt::Write as _;
use core::str::FromStr; use core::str::FromStr;
use core::{default, ops}; use core::{default, ops};
@ -16,7 +18,7 @@ use internals::error::InputString;
use internals::write_err; use internals::write_err;
#[cfg(feature = "alloc")] #[cfg(feature = "alloc")]
use crate::prelude::{String, ToString}; use alloc::string::{String, ToString};
/// A set of denominations in which amounts can be expressed. /// A set of denominations in which amounts can be expressed.
/// ///
@ -701,6 +703,7 @@ impl Amount {
pub fn to_sat(self) -> u64 { self.0 } pub fn to_sat(self) -> u64 { self.0 }
/// Convert from a value expressing bitcoins to an [Amount]. /// Convert from a value expressing bitcoins to an [Amount].
#[cfg(feature = "alloc")]
pub fn from_btc(btc: f64) -> Result<Amount, ParseAmountError> { pub fn from_btc(btc: f64) -> Result<Amount, ParseAmountError> {
Amount::from_float_in(btc, Denomination::Bitcoin) Amount::from_float_in(btc, Denomination::Bitcoin)
} }
@ -754,6 +757,7 @@ impl Amount {
/// Express this [Amount] as a floating-point value in the given denomination. /// Express this [Amount] as a floating-point value in the given denomination.
/// ///
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
#[cfg(feature = "alloc")]
pub fn to_float_in(self, denom: Denomination) -> f64 { pub fn to_float_in(self, denom: Denomination) -> f64 {
f64::from_str(&self.to_string_in(denom)).unwrap() f64::from_str(&self.to_string_in(denom)).unwrap()
} }
@ -768,6 +772,7 @@ impl Amount {
/// 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))
/// ``` /// ```
#[cfg(feature = "alloc")]
pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) }
/// Convert this [Amount] in floating-point notation with a given /// Convert this [Amount] in floating-point notation with a given
@ -775,6 +780,7 @@ impl Amount {
/// Can return error if the amount is too big, too precise or negative. /// Can return error if the amount is too big, too precise or negative.
/// ///
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
#[cfg(feature = "alloc")]
pub fn from_float_in(value: f64, denom: Denomination) -> Result<Amount, ParseAmountError> { pub fn from_float_in(value: f64, denom: Denomination) -> Result<Amount, ParseAmountError> {
if value < 0.0 { if value < 0.0 {
return Err(OutOfRangeError::negative().into()); return Err(OutOfRangeError::negative().into());
@ -816,6 +822,7 @@ impl Amount {
/// Get a string number of this [Amount] in the given denomination. /// Get a string number of this [Amount] in the given denomination.
/// ///
/// Does not include the denomination. /// Does not include the denomination.
#[cfg(feature = "alloc")]
pub fn to_string_in(self, denom: Denomination) -> String { pub fn to_string_in(self, denom: Denomination) -> String {
let mut buf = String::new(); let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap(); self.fmt_value_in(&mut buf, denom).unwrap();
@ -824,6 +831,7 @@ impl Amount {
/// Get a formatted string of this [Amount] in the given denomination, /// Get a formatted string of this [Amount] in the given denomination,
/// suffixed with the abbreviation for the denomination. /// suffixed with the abbreviation for the denomination.
#[cfg(feature = "alloc")]
pub fn to_string_with_denomination(self, denom: Denomination) -> String { pub fn to_string_with_denomination(self, denom: Denomination) -> String {
let mut buf = String::new(); let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap(); self.fmt_value_in(&mut buf, denom).unwrap();
@ -875,7 +883,7 @@ impl default::Default for Amount {
impl fmt::Debug for Amount { impl fmt::Debug for Amount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "Amount({:.8} BTC)", self.to_btc()) write!(f, "{} SAT", self.to_sat())
} }
} }
@ -1056,6 +1064,7 @@ impl SignedAmount {
pub fn to_sat(self) -> i64 { self.0 } pub fn to_sat(self) -> i64 { self.0 }
/// Convert from a value expressing bitcoins to an [SignedAmount]. /// Convert from a value expressing bitcoins to an [SignedAmount].
#[cfg(feature = "alloc")]
pub fn from_btc(btc: f64) -> Result<SignedAmount, ParseAmountError> { pub fn from_btc(btc: f64) -> Result<SignedAmount, ParseAmountError> {
SignedAmount::from_float_in(btc, Denomination::Bitcoin) SignedAmount::from_float_in(btc, Denomination::Bitcoin)
} }
@ -1087,6 +1096,7 @@ impl SignedAmount {
/// Express this [SignedAmount] as a floating-point value in the given denomination. /// Express this [SignedAmount] as a floating-point value in the given denomination.
/// ///
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
#[cfg(feature = "alloc")]
pub fn to_float_in(self, denom: Denomination) -> f64 { pub fn to_float_in(self, denom: Denomination) -> f64 {
f64::from_str(&self.to_string_in(denom)).unwrap() f64::from_str(&self.to_string_in(denom)).unwrap()
} }
@ -1096,6 +1106,7 @@ impl SignedAmount {
/// Equivalent to `to_float_in(Denomination::Bitcoin)`. /// Equivalent to `to_float_in(Denomination::Bitcoin)`.
/// ///
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
#[cfg(feature = "alloc")]
pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) } pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) }
/// Convert this [SignedAmount] in floating-point notation with a given /// Convert this [SignedAmount] in floating-point notation with a given
@ -1103,6 +1114,7 @@ impl SignedAmount {
/// Can return error if the amount is too big, too precise or negative. /// Can return error if the amount is too big, too precise or negative.
/// ///
/// Please be aware of the risk of using floating-point numbers. /// Please be aware of the risk of using floating-point numbers.
#[cfg(feature = "alloc")]
pub fn from_float_in( pub fn from_float_in(
value: f64, value: f64,
denom: Denomination, denom: Denomination,
@ -1144,6 +1156,7 @@ impl SignedAmount {
/// Get a string number of this [SignedAmount] in the given denomination. /// Get a string number of this [SignedAmount] in the given denomination.
/// ///
/// Does not include the denomination. /// Does not include the denomination.
#[cfg(feature = "alloc")]
pub fn to_string_in(self, denom: Denomination) -> String { pub fn to_string_in(self, denom: Denomination) -> String {
let mut buf = String::new(); let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap(); self.fmt_value_in(&mut buf, denom).unwrap();
@ -1152,6 +1165,7 @@ impl SignedAmount {
/// Get a formatted string of this [SignedAmount] in the given denomination, /// Get a formatted string of this [SignedAmount] in the given denomination,
/// suffixed with the abbreviation for the denomination. /// suffixed with the abbreviation for the denomination.
#[cfg(feature = "alloc")]
pub fn to_string_with_denomination(self, denom: Denomination) -> String { pub fn to_string_with_denomination(self, denom: Denomination) -> String {
let mut buf = String::new(); let mut buf = String::new();
self.fmt_value_in(&mut buf, denom).unwrap(); self.fmt_value_in(&mut buf, denom).unwrap();
@ -1244,7 +1258,7 @@ impl default::Default for SignedAmount {
impl fmt::Debug for SignedAmount { impl fmt::Debug for SignedAmount {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "SignedAmount({:.8} BTC)", self.to_btc()) write!(f, "SignedAmount({} SAT)", self.to_sat())
} }
} }
@ -1360,7 +1374,7 @@ where
} }
mod private { mod private {
use crate::{Amount, SignedAmount}; use super::{Amount, SignedAmount};
/// Used to seal the `CheckedSum` trait /// Used to seal the `CheckedSum` trait
pub trait SumSeal<A> {} pub trait SumSeal<A> {}
@ -1393,30 +1407,32 @@ pub mod serde {
use core::fmt; use core::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer}; use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::amount::{Amount, Denomination, SignedAmount, ParseAmountError}; use super::{Amount, SignedAmount, ParseAmountError};
#[cfg(feature = "alloc")]
use super::Denomination;
/// This trait is used only to avoid code duplication and naming collisions /// This trait is used only to avoid code duplication and naming collisions
/// of the different serde serialization crates. /// of the different serde serialization crates.
pub trait SerdeAmount: Copy + Sized + private::Sealed { pub trait SerdeAmount: Copy + Sized {
fn ser_sat<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>; fn ser_sat<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
fn des_sat<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error>; fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error>;
fn ser_btc<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>; #[cfg(feature = "alloc")]
fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error>; fn ser_btc<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
#[cfg(feature = "alloc")]
fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error>;
} }
mod private { mod private {
/// add this as a trait bound to traits which consumers of this library /// Controls access to the trait methods.
/// should not be able to implement. pub struct Token;
pub trait Sealed {}
impl Sealed for super::Amount {}
impl Sealed for super::SignedAmount {}
} }
/// This trait is only for internal Amount type serialization/deserialization /// This trait is only for internal Amount type serialization/deserialization
pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount + private::Sealed { pub trait SerdeAmountForOpt: Copy + Sized + SerdeAmount {
fn type_prefix() -> &'static str; fn type_prefix(_: private::Token) -> &'static str;
fn ser_sat_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>; fn ser_sat_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
fn ser_btc_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error>; #[cfg(feature = "alloc")]
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error>;
} }
struct DisplayFullError(ParseAmountError); struct DisplayFullError(ParseAmountError);
@ -1444,16 +1460,18 @@ pub mod serde {
} }
impl SerdeAmount for Amount { impl SerdeAmount for Amount {
fn ser_sat<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { fn ser_sat<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
u64::serialize(&self.to_sat(), s) u64::serialize(&self.to_sat(), s)
} }
fn des_sat<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
Ok(Amount::from_sat(u64::deserialize(d)?)) Ok(Amount::from_sat(u64::deserialize(d)?))
} }
fn ser_btc<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { #[cfg(feature = "alloc")]
fn ser_btc<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) f64::serialize(&self.to_float_in(Denomination::Bitcoin), s)
} }
fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { #[cfg(feature = "alloc")]
fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
use serde::de::Error; use serde::de::Error;
Amount::from_btc(f64::deserialize(d)?) Amount::from_btc(f64::deserialize(d)?)
.map_err(DisplayFullError) .map_err(DisplayFullError)
@ -1462,26 +1480,29 @@ pub mod serde {
} }
impl SerdeAmountForOpt for Amount { impl SerdeAmountForOpt for Amount {
fn type_prefix() -> &'static str { "u" } fn type_prefix(_: private::Token) -> &'static str { "u" }
fn ser_sat_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { fn ser_sat_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_sat()) s.serialize_some(&self.to_sat())
} }
fn ser_btc_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { #[cfg(feature = "alloc")]
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_btc()) s.serialize_some(&self.to_btc())
} }
} }
impl SerdeAmount for SignedAmount { impl SerdeAmount for SignedAmount {
fn ser_sat<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { fn ser_sat<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
i64::serialize(&self.to_sat(), s) i64::serialize(&self.to_sat(), s)
} }
fn des_sat<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { fn des_sat<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
Ok(SignedAmount::from_sat(i64::deserialize(d)?)) Ok(SignedAmount::from_sat(i64::deserialize(d)?))
} }
fn ser_btc<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { #[cfg(feature = "alloc")]
fn ser_btc<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
f64::serialize(&self.to_float_in(Denomination::Bitcoin), s) f64::serialize(&self.to_float_in(Denomination::Bitcoin), s)
} }
fn des_btc<'d, D: Deserializer<'d>>(d: D) -> Result<Self, D::Error> { #[cfg(feature = "alloc")]
fn des_btc<'d, D: Deserializer<'d>>(d: D, _: private::Token) -> Result<Self, D::Error> {
use serde::de::Error; use serde::de::Error;
SignedAmount::from_btc(f64::deserialize(d)?) SignedAmount::from_btc(f64::deserialize(d)?)
.map_err(DisplayFullError) .map_err(DisplayFullError)
@ -1490,11 +1511,12 @@ pub mod serde {
} }
impl SerdeAmountForOpt for SignedAmount { impl SerdeAmountForOpt for SignedAmount {
fn type_prefix() -> &'static str { "i" } fn type_prefix(_: private::Token) -> &'static str { "i" }
fn ser_sat_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { fn ser_sat_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_sat()) s.serialize_some(&self.to_sat())
} }
fn ser_btc_opt<S: Serializer>(self, s: S) -> Result<S::Ok, S::Error> { #[cfg(feature = "alloc")]
fn ser_btc_opt<S: Serializer>(self, s: S, _: private::Token) -> Result<S::Ok, S::Error> {
s.serialize_some(&self.to_btc()) s.serialize_some(&self.to_btc())
} }
} }
@ -1502,23 +1524,26 @@ pub mod serde {
pub mod as_sat { pub mod as_sat {
//! Serialize and deserialize [`Amount`](crate::Amount) as real numbers denominated in satoshi. //! Serialize and deserialize [`Amount`](crate::Amount) as real numbers denominated in satoshi.
//! Use with `#[serde(with = "amount::serde::as_sat")]`. //! Use with `#[serde(with = "amount::serde::as_sat")]`.
//!
use super::private;
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
use crate::amount::serde::SerdeAmount; use crate::amount::serde::SerdeAmount;
pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> {
a.ser_sat(s) a.ser_sat(s, private::Token)
} }
pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> { pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> {
A::des_sat(d) A::des_sat(d, private::Token)
} }
pub mod opt { pub mod opt {
//! Serialize and deserialize [`Option<Amount>`](crate::Amount) as real numbers denominated in satoshi. //! Serialize and deserialize [`Option<Amount>`](crate::Amount) as real numbers denominated in satoshi.
//! Use with `#[serde(default, with = "amount::serde::as_sat::opt")]`. //! Use with `#[serde(default, with = "amount::serde::as_sat::opt")]`.
use super::private;
use core::fmt; use core::fmt;
use core::marker::PhantomData; use core::marker::PhantomData;
@ -1531,7 +1556,7 @@ pub mod serde {
s: S, s: S,
) -> Result<S::Ok, S::Error> { ) -> Result<S::Ok, S::Error> {
match *a { match *a {
Some(a) => a.ser_sat_opt(s), Some(a) => a.ser_sat_opt(s, private::Token),
None => s.serialize_none(), None => s.serialize_none(),
} }
} }
@ -1545,7 +1570,7 @@ pub mod serde {
type Value = Option<X>; type Value = Option<X>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "An Option<{}64>", X::type_prefix()) write!(formatter, "An Option<{}64>", X::type_prefix(private::Token))
} }
fn visit_none<E>(self) -> Result<Self::Value, E> fn visit_none<E>(self) -> Result<Self::Value, E>
@ -1558,7 +1583,7 @@ pub mod serde {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Ok(Some(X::des_sat(d)?)) Ok(Some(X::des_sat(d, private::Token)?))
} }
} }
d.deserialize_option(VisitOptAmt::<A>(PhantomData)) d.deserialize_option(VisitOptAmt::<A>(PhantomData))
@ -1566,26 +1591,30 @@ pub mod serde {
} }
} }
#[cfg(feature = "alloc")]
pub mod as_btc { pub mod as_btc {
//! Serialize and deserialize [`Amount`](crate::Amount) as JSON numbers denominated in BTC. //! Serialize and deserialize [`Amount`](crate::Amount) as JSON numbers denominated in BTC.
//! Use with `#[serde(with = "amount::serde::as_btc")]`. //! Use with `#[serde(with = "amount::serde::as_btc")]`.
use super::private;
use serde::{Deserializer, Serializer}; use serde::{Deserializer, Serializer};
use crate::amount::serde::SerdeAmount; use crate::amount::serde::SerdeAmount;
pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> { pub fn serialize<A: SerdeAmount, S: Serializer>(a: &A, s: S) -> Result<S::Ok, S::Error> {
a.ser_btc(s) a.ser_btc(s, private::Token)
} }
pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> { pub fn deserialize<'d, A: SerdeAmount, D: Deserializer<'d>>(d: D) -> Result<A, D::Error> {
A::des_btc(d) A::des_btc(d, private::Token)
} }
pub mod opt { pub mod opt {
//! Serialize and deserialize `Option<Amount>` as JSON numbers denominated in BTC. //! Serialize and deserialize `Option<Amount>` as JSON numbers denominated in BTC.
//! Use with `#[serde(default, with = "amount::serde::as_btc::opt")]`. //! Use with `#[serde(default, with = "amount::serde::as_btc::opt")]`.
use super::private;
use core::fmt; use core::fmt;
use core::marker::PhantomData; use core::marker::PhantomData;
@ -1598,7 +1627,7 @@ pub mod serde {
s: S, s: S,
) -> Result<S::Ok, S::Error> { ) -> Result<S::Ok, S::Error> {
match *a { match *a {
Some(a) => a.ser_btc_opt(s), Some(a) => a.ser_btc_opt(s, private::Token),
None => s.serialize_none(), None => s.serialize_none(),
} }
} }
@ -1625,7 +1654,7 @@ pub mod serde {
where where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
Ok(Some(X::des_btc(d)?)) Ok(Some(X::des_btc(d, private::Token)?))
} }
} }
d.deserialize_option(VisitOptAmt::<A>(PhantomData)) d.deserialize_option(VisitOptAmt::<A>(PhantomData))
@ -1759,6 +1788,10 @@ mod verification {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use core::str::FromStr; use core::str::FromStr;
#[cfg(feature = "alloc")]
use alloc::format;
#[cfg(feature = "std")] #[cfg(feature = "std")]
use std::panic; use std::panic;
@ -1768,8 +1801,9 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
#[cfg(feature = "alloc")]
fn from_str_zero() { fn from_str_zero() {
let denoms = vec!["BTC", "mBTC", "uBTC", "nBTC", "pBTC", "bits", "sats", "msats"]; let denoms = ["BTC", "mBTC", "uBTC", "nBTC", "pBTC", "bits", "sats", "msats"];
for denom in denoms { for denom in denoms {
for v in &["0", "000"] { for v in &["0", "000"] {
let s = format!("{} {}", v, denom); let s = format!("{} {}", v, denom);
@ -1844,6 +1878,7 @@ mod tests {
assert_eq!(ssat(-6).checked_div(2), Some(ssat(-3))); assert_eq!(ssat(-6).checked_div(2), Some(ssat(-3)));
} }
#[cfg(feature = "alloc")]
#[test] #[test]
fn floating_point() { fn floating_point() {
use super::Denomination as D; use super::Denomination as D;
@ -1905,7 +1940,9 @@ mod tests {
assert_eq!(p("-1.0x", btc), Err(E::InvalidCharacter('x'))); assert_eq!(p("-1.0x", btc), Err(E::InvalidCharacter('x')));
assert_eq!(p("0.0 ", btc), Err(ParseAmountError::InvalidCharacter(' '))); assert_eq!(p("0.0 ", btc), Err(ParseAmountError::InvalidCharacter(' ')));
assert_eq!(p("0.000.000", btc), Err(E::InvalidFormat)); assert_eq!(p("0.000.000", btc), Err(E::InvalidFormat));
#[cfg(feature = "alloc")]
let more_than_max = format!("1{}", Amount::MAX); let more_than_max = format!("1{}", Amount::MAX);
#[cfg(feature = "alloc")]
assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into())); assert_eq!(p(&more_than_max, btc), Err(OutOfRangeError::too_big(false).into()));
assert_eq!(p("0.000000042", btc), Err(E::TooPrecise)); assert_eq!(p("0.000000042", btc), Err(E::TooPrecise));
assert_eq!(p("999.0000000", msat), Err(E::TooPrecise)); assert_eq!(p("999.0000000", msat), Err(E::TooPrecise));
@ -1920,6 +1957,7 @@ mod tests {
assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00))); assert_eq!(p("1", btc), Ok(Amount::from_sat(1_000_000_00)));
assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00))); assert_eq!(sp("-.5", btc), Ok(SignedAmount::from_sat(-500_000_00)));
#[cfg(feature = "alloc")]
assert_eq!(sp(&i64::MIN.to_string(), sat), Ok(SignedAmount::from_sat(i64::MIN))); assert_eq!(sp(&i64::MIN.to_string(), sat), Ok(SignedAmount::from_sat(i64::MIN)));
assert_eq!(p("1.1", btc), Ok(Amount::from_sat(1_100_000_00))); assert_eq!(p("1.1", btc), Ok(Amount::from_sat(1_100_000_00)));
assert_eq!(p("100", sat), Ok(Amount::from_sat(100))); assert_eq!(p("100", sat), Ok(Amount::from_sat(100)));
@ -1935,10 +1973,13 @@ mod tests {
assert_eq!(p("1000.000000000000000000000000000", msat), Ok(Amount::from_sat(1))); assert_eq!(p("1000.000000000000000000000000000", msat), Ok(Amount::from_sat(1)));
// make sure satoshi > i64::MAX is checked. // make sure satoshi > i64::MAX is checked.
#[cfg(feature = "alloc")]
{
let amount = Amount::from_sat(i64::MAX as u64); let amount = Amount::from_sat(i64::MAX as u64);
assert_eq!(Amount::from_str_in(&amount.to_string_in(sat), sat), Ok(amount)); assert_eq!(Amount::from_str_in(&amount.to_string_in(sat), sat), Ok(amount));
assert!(SignedAmount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_err()); assert!(SignedAmount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_err());
assert!(Amount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_ok()); assert!(Amount::from_str_in(&(amount + Amount(1)).to_string_in(sat), sat).is_ok());
}
assert_eq!(p("12.000", Denomination::MilliSatoshi), Err(E::TooPrecise)); assert_eq!(p("12.000", Denomination::MilliSatoshi), Err(E::TooPrecise));
// exactly 50 chars. // exactly 50 chars.
@ -1954,6 +1995,7 @@ mod tests {
} }
#[test] #[test]
#[cfg(feature = "alloc")]
fn to_string() { fn to_string() {
use super::Denomination as D; use super::Denomination as D;
@ -1977,6 +2019,7 @@ mod tests {
} }
// May help identify a problem sooner // May help identify a problem sooner
#[cfg(feature = "alloc")]
#[test] #[test]
fn test_repeat_char() { fn test_repeat_char() {
let mut buf = String::new(); let mut buf = String::new();
@ -1992,6 +2035,7 @@ mod tests {
($denom:ident; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => { ($denom:ident; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => {
$( $(
#[test] #[test]
#[cfg(feature = "alloc")]
fn $test_name() { fn $test_name() {
assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom)), $expected); assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom)), $expected);
assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom)), $expected); assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom)), $expected);
@ -2004,6 +2048,7 @@ mod tests {
($denom:ident, $denom_suffix:literal; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => { ($denom:ident, $denom_suffix:literal; $($test_name:ident, $val:literal, $format_string:literal, $expected:literal);* $(;)?) => {
$( $(
#[test] #[test]
#[cfg(feature = "alloc")]
fn $test_name() { fn $test_name() {
assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix)); assert_eq!(format!($format_string, Amount::from_sat($val).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix)); assert_eq!(format!($format_string, SignedAmount::from_sat($val as i64).display_in(Denomination::$denom).show_denomination()), concat!($expected, $denom_suffix));
@ -2237,9 +2282,11 @@ mod tests {
ok_scase("-5 satoshi", SignedAmount::from_sat(-5)); ok_scase("-5 satoshi", SignedAmount::from_sat(-5));
ok_case("0.10000000 BTC", Amount::from_sat(100_000_00)); ok_case("0.10000000 BTC", Amount::from_sat(100_000_00));
ok_scase("-100 bits", SignedAmount::from_sat(-10_000)); ok_scase("-100 bits", SignedAmount::from_sat(-10_000));
#[cfg(feature = "alloc")]
ok_scase(&format!("{} SAT", i64::MIN), SignedAmount::from_sat(i64::MIN)); ok_scase(&format!("{} SAT", i64::MIN), SignedAmount::from_sat(i64::MIN));
} }
#[cfg(feature = "alloc")]
#[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 to_from_string_in() { fn to_from_string_in() {
@ -2326,6 +2373,7 @@ mod tests {
); );
} }
#[cfg(feature = "alloc")]
#[test] #[test]
fn to_string_with_denomination_from_str_roundtrip() { fn to_string_with_denomination_from_str_roundtrip() {
use ParseDenominationError::*; use ParseDenominationError::*;
@ -2378,6 +2426,7 @@ mod tests {
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
#[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 serde_as_btc() { fn serde_as_btc() {
@ -2409,10 +2458,11 @@ mod tests {
serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}"); serde_json::from_str("{\"amt\": 1000000.000000001, \"samt\": 1}");
assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise.to_string())); assert!(t.unwrap_err().to_string().contains(&ParseAmountError::TooPrecise.to_string()));
let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}"); let t: Result<T, serde_json::Error> = serde_json::from_str("{\"amt\": -1, \"samt\": 1}");
assert!(dbg!(t.unwrap_err().to_string()).contains(&OutOfRangeError::negative().to_string())); assert!(t.unwrap_err().to_string().contains(&OutOfRangeError::negative().to_string()));
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
#[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 serde_as_btc_opt() { fn serde_as_btc_opt() {
@ -2454,6 +2504,7 @@ mod tests {
} }
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
#[cfg(feature = "alloc")]
#[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 serde_as_sat_opt() { fn serde_as_sat_opt() {
@ -2496,14 +2547,14 @@ mod tests {
#[test] #[test]
fn sum_amounts() { fn sum_amounts() {
assert_eq!(Amount::from_sat(0), vec![].into_iter().sum::<Amount>()); assert_eq!(Amount::from_sat(0), [].into_iter().sum::<Amount>());
assert_eq!(SignedAmount::from_sat(0), vec![].into_iter().sum::<SignedAmount>()); assert_eq!(SignedAmount::from_sat(0), [].into_iter().sum::<SignedAmount>());
let amounts = vec![Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)];
let sum = amounts.into_iter().sum::<Amount>(); let sum = amounts.into_iter().sum::<Amount>();
assert_eq!(Amount::from_sat(1400), sum); assert_eq!(Amount::from_sat(1400), sum);
let amounts = vec![ let amounts = [
SignedAmount::from_sat(-42), SignedAmount::from_sat(-42),
SignedAmount::from_sat(1337), SignedAmount::from_sat(1337),
SignedAmount::from_sat(21), SignedAmount::from_sat(21),
@ -2514,19 +2565,19 @@ mod tests {
#[test] #[test]
fn checked_sum_amounts() { fn checked_sum_amounts() {
assert_eq!(Some(Amount::from_sat(0)), vec![].into_iter().checked_sum()); assert_eq!(Some(Amount::from_sat(0)), [].into_iter().checked_sum());
assert_eq!(Some(SignedAmount::from_sat(0)), vec![].into_iter().checked_sum()); assert_eq!(Some(SignedAmount::from_sat(0)), [].into_iter().checked_sum());
let amounts = vec![Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)]; let amounts = [Amount::from_sat(42), Amount::from_sat(1337), Amount::from_sat(21)];
let sum = amounts.into_iter().checked_sum(); let sum = amounts.into_iter().checked_sum();
assert_eq!(Some(Amount::from_sat(1400)), sum); assert_eq!(Some(Amount::from_sat(1400)), sum);
let amounts = let amounts =
vec![Amount::from_sat(u64::MAX), Amount::from_sat(1337), Amount::from_sat(21)]; [Amount::from_sat(u64::MAX), Amount::from_sat(1337), Amount::from_sat(21)];
let sum = amounts.into_iter().checked_sum(); let sum = amounts.into_iter().checked_sum();
assert_eq!(None, sum); assert_eq!(None, sum);
let amounts = vec![ let amounts = [
SignedAmount::from_sat(i64::MIN), SignedAmount::from_sat(i64::MIN),
SignedAmount::from_sat(-1), SignedAmount::from_sat(-1),
SignedAmount::from_sat(21), SignedAmount::from_sat(21),
@ -2534,7 +2585,7 @@ mod tests {
let sum = amounts.into_iter().checked_sum(); let sum = amounts.into_iter().checked_sum();
assert_eq!(None, sum); assert_eq!(None, sum);
let amounts = vec![ let amounts = [
SignedAmount::from_sat(i64::MAX), SignedAmount::from_sat(i64::MAX),
SignedAmount::from_sat(1), SignedAmount::from_sat(1),
SignedAmount::from_sat(21), SignedAmount::from_sat(21),
@ -2542,7 +2593,7 @@ mod tests {
let sum = amounts.into_iter().checked_sum(); let sum = amounts.into_iter().checked_sum();
assert_eq!(None, sum); assert_eq!(None, sum);
let amounts = vec![ let amounts = [
SignedAmount::from_sat(42), SignedAmount::from_sat(42),
SignedAmount::from_sat(3301), SignedAmount::from_sat(3301),
SignedAmount::from_sat(21), SignedAmount::from_sat(21),
@ -2554,7 +2605,7 @@ mod tests {
#[test] #[test]
fn denomination_string_acceptable_forms() { fn denomination_string_acceptable_forms() {
// Non-exhaustive list of valid forms. // Non-exhaustive list of valid forms.
let valid = vec![ let valid = [
"BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI", "satoshi", "SATOSHIS", "BTC", "btc", "mBTC", "mbtc", "uBTC", "ubtc", "SATOSHI", "satoshi", "SATOSHIS",
"satoshis", "SAT", "sat", "SATS", "sats", "bit", "bits", "nBTC", "pBTC", "satoshis", "SAT", "sat", "SATS", "sats", "bit", "bits", "nBTC", "pBTC",
]; ];

View File

@ -4,7 +4,6 @@
//! //!
//! This library provides basic types used by the Rust Bitcoin ecosystem. //! 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. // Experimental features we need.
#![cfg_attr(docsrs, feature(doc_auto_cfg))] #![cfg_attr(docsrs, feature(doc_auto_cfg))]
// Coding conventions // Coding conventions
@ -12,6 +11,8 @@
// Exclude clippy lints we don't think are valuable // Exclude clippy lints we don't think are valuable
#![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134 #![allow(clippy::needless_question_mark)] // https://github.com/rust-bitcoin/rust-bitcoin/pull/2134
#![no_std]
// Disable 16-bit support at least for now as we can't guarantee it yet. // Disable 16-bit support at least for now as we can't guarantee it yet.
#[cfg(target_pointer_width = "16")] #[cfg(target_pointer_width = "16")]
compile_error!( compile_error!(
@ -19,28 +20,17 @@ compile_error!(
know if you want 16-bit support. Note that we do NOT guarantee that we will implement it!" 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")))] #[cfg(feature = "alloc")]
extern crate alloc; extern crate alloc;
#[cfg(not(feature = "std"))] #[cfg(feature = "std")]
extern crate core; extern crate std;
/// A generic serialization/deserialization framework. /// A generic serialization/deserialization framework.
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
pub extern crate serde; pub extern crate serde;
#[cfg(feature = "alloc")]
pub mod amount; pub mod amount;
#[cfg(feature = "alloc")]
#[doc(inline)] #[doc(inline)]
pub use self::amount::{Amount, ParseAmountError, SignedAmount}; 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};
#[cfg(any(feature = "std", test))]
pub use std::string::{String, ToString};
}