Skip to main content

bitcoin_units/amount/
signed.rs

1// SPDX-License-Identifier: CC0-1.0
2
3//! A signed bitcoin amount.
4
5#[cfg(feature = "alloc")]
6use alloc::string::{String, ToString};
7use core::str::FromStr;
8use core::{default, fmt};
9
10#[cfg(feature = "arbitrary")]
11use arbitrary::{Arbitrary, Unstructured};
12
13use super::error::ParseErrorInner;
14use super::{
15    parse_signed_to_satoshi, split_amount_and_denomination, Amount, Denomination, Display,
16    DisplayStyle, OutOfRangeError, ParseAmountError, ParseError,
17};
18
19mod encapsulate {
20    use super::OutOfRangeError;
21
22    /// A signed amount.
23    ///
24    /// The [`SignedAmount`] type can be used to express Bitcoin amounts that support arithmetic and
25    /// conversion to various denominations. The [`SignedAmount`] type does not implement [`serde`]
26    /// traits but we do provide modules for serializing as satoshis or bitcoin.
27    ///
28    /// **Warning!**
29    ///
30    /// This type implements several arithmetic operations from [`core::ops`].
31    /// To prevent errors due to an overflow when using these operations,
32    /// it is advised to instead use the checked arithmetic methods whose names
33    /// start with `checked_`. The operations from [`core::ops`] that [`SignedAmount`]
34    /// implements will panic when an overflow occurs.
35    ///
36    /// # Examples
37    ///
38    /// ```
39    /// # #[cfg(feature = "serde")] {
40    /// use serde::{Serialize, Deserialize};
41    /// use bitcoin_units::SignedAmount;
42    ///
43    /// #[derive(Serialize, Deserialize)]
44    /// struct Foo {
45    ///     // If you are using `rust-bitcoin` then `bitcoin::amount::serde::as_sat` also works.
46    ///     #[serde(with = "bitcoin_units::amount::serde::as_sat")]  // Also `serde::as_btc`.
47    ///     amount: SignedAmount,
48    /// }
49    /// # }
50    /// ```
51    #[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
52    pub struct SignedAmount(i64);
53
54    impl SignedAmount {
55        /// The maximum value of an amount.
56        pub const MAX: Self = Self(21_000_000 * 100_000_000);
57        /// The minimum value of an amount.
58        pub const MIN: Self = Self(-21_000_000 * 100_000_000);
59
60        /// Gets the number of satoshis in this [`SignedAmount`].
61        ///
62        /// # Examples
63        ///
64        /// ```
65        /// # use bitcoin_units::SignedAmount;
66        /// assert_eq!(SignedAmount::ONE_BTC.to_sat(), 100_000_000);
67        /// ```
68        pub const fn to_sat(self) -> i64 { self.0 }
69
70        /// Constructs a new [`SignedAmount`] from the given number of satoshis.
71        ///
72        /// # Errors
73        ///
74        /// If `satoshi` is outside of valid range (see [`Self::MAX_MONEY`]).
75        ///
76        /// # Examples
77        ///
78        /// ```
79        /// # use bitcoin_units::{amount, SignedAmount};
80        /// # let sat = -100_000;
81        /// let amount = SignedAmount::from_sat(sat)?;
82        /// assert_eq!(amount.to_sat(), sat);
83        /// # Ok::<_, amount::OutOfRangeError>(())
84        /// ```
85        pub const fn from_sat(satoshi: i64) -> Result<Self, OutOfRangeError> {
86            if satoshi < Self::MIN.to_sat() {
87                Err(OutOfRangeError { is_signed: true, is_greater_than_max: false })
88            } else if satoshi > Self::MAX_MONEY.to_sat() {
89                Err(OutOfRangeError { is_signed: true, is_greater_than_max: true })
90            } else {
91                Ok(Self(satoshi))
92            }
93        }
94    }
95}
96#[doc(inline)]
97pub use encapsulate::SignedAmount;
98use internals::const_casts;
99
100impl SignedAmount {
101    /// The zero amount.
102    pub const ZERO: Self = Self::from_sat_i32(0);
103    /// Exactly one satoshi.
104    pub const ONE_SAT: Self = Self::from_sat_i32(1);
105    /// Exactly one bitcoin.
106    pub const ONE_BTC: Self = Self::from_btc_i16(1);
107    /// Exactly fifty bitcoin.
108    pub const FIFTY_BTC: Self = Self::from_btc_i16(50);
109    /// The maximum value allowed as an amount. Useful for sanity checking.
110    pub const MAX_MONEY: Self = Self::MAX;
111
112    /// Constructs a new [`SignedAmount`] with satoshi precision and the given number of satoshis.
113    ///
114    /// Accepts an `i32` which is guaranteed to be in range for the type, but which can only
115    /// represent roughly -21.47 to 21.47 BTC.
116    #[allow(clippy::missing_panics_doc)]
117    pub const fn from_sat_i32(satoshi: i32) -> Self {
118        let sats = satoshi as i64; // cannot use i64::from in a constfn
119        match Self::from_sat(sats) {
120            Ok(amount) => amount,
121            Err(_) => panic!("unreachable - i32 input [-2,147,483,648 to 2,147,483,647 satoshis] is within range"),
122        }
123    }
124
125    /// Converts from a value expressing a decimal number of bitcoin to a [`SignedAmount`].
126    ///
127    /// # Errors
128    ///
129    /// If the amount is too big (positive or negative) or too precise.
130    ///
131    /// Please be aware of the risk of using floating-point numbers.
132    ///
133    /// # Examples
134    ///
135    /// ```
136    /// # use bitcoin_units::{amount, SignedAmount};
137    /// let amount = SignedAmount::from_btc(-0.01)?;
138    /// assert_eq!(amount.to_sat(), -1_000_000);
139    /// # Ok::<_, amount::ParseAmountError>(())
140    /// ```
141    #[cfg(feature = "alloc")]
142    pub fn from_btc(btc: f64) -> Result<Self, ParseAmountError> {
143        Self::from_float_in(btc, Denomination::Bitcoin)
144    }
145
146    /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`].
147    #[allow(clippy::missing_panics_doc)]
148    pub fn from_int_btc<T: Into<i16>>(whole_bitcoin: T) -> Self {
149        Self::from_btc_i16(whole_bitcoin.into())
150    }
151
152    /// Converts from a value expressing a whole number of bitcoin to a [`SignedAmount`]
153    /// in const context.
154    #[allow(clippy::missing_panics_doc)]
155    pub const fn from_btc_i16(whole_bitcoin: i16) -> Self {
156        let btc = const_casts::i16_to_i64(whole_bitcoin);
157        let sats = btc * 100_000_000;
158
159        match Self::from_sat(sats) {
160            Ok(amount) => amount,
161            Err(_) => panic!("unreachable - 32,767 BTC is within range"),
162        }
163    }
164
165    /// Parses a decimal string as a value in the given [`Denomination`].
166    ///
167    /// Note: This only parses the value string. If you want to parse a string
168    /// containing the value with denomination, use [`FromStr`].
169    ///
170    /// # Errors
171    ///
172    /// If the amount is too big (positive or negative) or too precise.
173    pub fn from_str_in(s: &str, denom: Denomination) -> Result<Self, ParseAmountError> {
174        parse_signed_to_satoshi(s, denom)
175            .map(|(_, amount)| amount)
176            .map_err(|error| error.convert(true))
177    }
178
179    /// Parses amounts with denomination suffix as produced by [`Self::to_string_with_denomination`]
180    /// or with [`fmt::Display`].
181    ///
182    /// If you want to parse only the amount without the denomination, use [`Self::from_str_in`].
183    ///
184    /// # Errors
185    ///
186    /// If the amount is too big (positive or negative) or too precise.
187    ///
188    /// # Examples
189    ///
190    /// ```
191    /// # use bitcoin_units::{amount, SignedAmount};
192    /// let amount = SignedAmount::from_str_with_denomination("0.1 BTC")?;
193    /// assert_eq!(amount, SignedAmount::from_sat(10_000_000)?);
194    /// # Ok::<_, amount::ParseError>(())
195    /// ```
196    pub fn from_str_with_denomination(s: &str) -> Result<Self, ParseError> {
197        let (amt, denom) = split_amount_and_denomination(s)?;
198        Self::from_str_in(amt, denom).map_err(Into::into)
199    }
200
201    /// Expresses this [`SignedAmount`] as a floating-point value in the given [`Denomination`].
202    ///
203    /// Please be aware of the risk of using floating-point numbers.
204    ///
205    /// # Examples
206    ///
207    /// ```
208    /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
209    /// let amount = SignedAmount::from_sat(100_000)?;
210    /// assert_eq!(amount.to_float_in(Denomination::Bitcoin), 0.001);
211    /// # Ok::<_, amount::OutOfRangeError>(())
212    /// ```
213    #[cfg(feature = "alloc")]
214    #[allow(clippy::missing_panics_doc)]
215    pub fn to_float_in(self, denom: Denomination) -> f64 {
216        self.to_string_in(denom).parse::<f64>().unwrap()
217    }
218
219    /// Expresses this [`SignedAmount`] as a floating-point value in Bitcoin.
220    ///
221    /// Please be aware of the risk of using floating-point numbers.
222    ///
223    /// # Examples
224    ///
225    /// ```
226    /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
227    /// let amount = SignedAmount::from_sat(100_000)?;
228    /// assert_eq!(amount.to_btc(), amount.to_float_in(Denomination::Bitcoin));
229    /// # Ok::<_, amount::OutOfRangeError>(())
230    /// ```
231    #[cfg(feature = "alloc")]
232    pub fn to_btc(self) -> f64 { self.to_float_in(Denomination::Bitcoin) }
233
234    /// Converts this [`SignedAmount`] in floating-point notation in the given [`Denomination`].
235    ///
236    /// # Errors
237    ///
238    /// If the amount is too big (positive or negative) or too precise.
239    ///
240    /// Please be aware of the risk of using floating-point numbers.
241    #[cfg(feature = "alloc")]
242    pub fn from_float_in(value: f64, denom: Denomination) -> Result<Self, ParseAmountError> {
243        // This is inefficient, but the safest way to deal with this. The parsing logic is safe.
244        // Any performance-critical application should not be dealing with floats.
245        Self::from_str_in(&value.to_string(), denom)
246    }
247
248    /// Constructs a new object that implements [`fmt::Display`] in the given [`Denomination`].
249    ///
250    /// This function is useful if you do not wish to allocate. See also [`Self::to_string_in`].
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
256    /// # use std::fmt::Write;
257    /// let amount = SignedAmount::from_sat(10_000_000)?;
258    /// let mut output = String::new();
259    /// let _ = write!(&mut output, "{}", amount.display_in(Denomination::Bitcoin));
260    /// assert_eq!(output, "0.1");
261    /// # Ok::<_, amount::OutOfRangeError>(())
262    /// ```
263    #[must_use]
264    pub fn display_in(self, denomination: Denomination) -> Display {
265        Display {
266            sats_abs: self.unsigned_abs().to_sat(),
267            is_negative: self.is_negative(),
268            style: DisplayStyle::FixedDenomination { denomination, show_denomination: false },
269        }
270    }
271
272    /// Constructs a new object that implements [`fmt::Display`] dynamically selecting
273    /// [`Denomination`].
274    ///
275    /// This will use BTC for values greater than or equal to 1 BTC and satoshis otherwise. To
276    /// avoid confusion the denomination is always shown.
277    #[must_use]
278    pub fn display_dynamic(self) -> Display {
279        Display {
280            sats_abs: self.unsigned_abs().to_sat(),
281            is_negative: self.is_negative(),
282            style: DisplayStyle::DynamicDenomination,
283        }
284    }
285
286    /// Returns a formatted string representing this [`SignedAmount`] in the given [`Denomination`].
287    ///
288    /// Returned string does not include the denomination.
289    ///
290    /// # Examples
291    ///
292    /// ```
293    /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
294    /// let amount = SignedAmount::from_sat(10_000_000)?;
295    /// assert_eq!(amount.to_string_in(Denomination::Bitcoin), "0.1");
296    /// # Ok::<_, amount::OutOfRangeError>(())
297    /// ```
298    #[cfg(feature = "alloc")]
299    pub fn to_string_in(self, denom: Denomination) -> String { self.display_in(denom).to_string() }
300
301    /// Returns a formatted string representing this [`SignedAmount`] in the given [`Denomination`],
302    /// suffixed with the abbreviation for the denomination.
303    ///
304    /// # Examples
305    ///
306    /// ```
307    /// # use bitcoin_units::amount::{self, SignedAmount, Denomination};
308    /// let amount = SignedAmount::from_sat(10_000_000)?;
309    /// assert_eq!(amount.to_string_with_denomination(Denomination::Bitcoin), "0.1 BTC");
310    /// # Ok::<_, amount::OutOfRangeError>(())
311    /// ```
312    #[cfg(feature = "alloc")]
313    pub fn to_string_with_denomination(self, denom: Denomination) -> String {
314        self.display_in(denom).show_denomination().to_string()
315    }
316
317    /// Gets the absolute value of this [`SignedAmount`].
318    ///
319    /// This function never overflows or panics, unlike `i64::abs()`.
320    #[must_use]
321    #[allow(clippy::missing_panics_doc)]
322    pub const fn abs(self) -> Self {
323        // `i64::abs()` can never overflow because SignedAmount::MIN == -MAX_MONEY.
324        match Self::from_sat(self.to_sat().abs()) {
325            Ok(amount) => amount,
326            Err(_) => panic!("a positive signed amount is always valid"),
327        }
328    }
329
330    /// Gets the absolute value of this [`SignedAmount`] returning [`Amount`].
331    #[must_use]
332    #[allow(clippy::missing_panics_doc)]
333    pub fn unsigned_abs(self) -> Amount {
334        self.abs().to_unsigned().expect("a positive signed amount is always valid")
335    }
336
337    /// Returns a number representing sign of this [`SignedAmount`].
338    ///
339    /// - `0` if the amount is zero
340    /// - `1` if the amount is positive
341    /// - `-1` if the amount is negative
342    #[must_use]
343    pub fn signum(self) -> i64 { self.to_sat().signum() }
344
345    /// Checks if this [`SignedAmount`] is positive.
346    ///
347    /// Returns `true` if this [`SignedAmount`] is positive and `false` if
348    /// this [`SignedAmount`] is zero or negative.
349    pub fn is_positive(self) -> bool { self.to_sat().is_positive() }
350
351    /// Checks if this [`SignedAmount`] is negative.
352    ///
353    /// Returns `true` if this [`SignedAmount`] is negative and `false` if
354    /// this [`SignedAmount`] is zero or positive.
355    pub fn is_negative(self) -> bool { self.to_sat().is_negative() }
356
357    /// Returns the absolute value of this [`SignedAmount`].
358    ///
359    /// Consider using `unsigned_abs` which is often more practical.
360    ///
361    /// Returns [`None`] if overflow occurred. (`self == i64::MIN`)
362    #[must_use]
363    #[deprecated(since = "1.0.0-rc.0", note = "Never returns none, use `abs()` instead")]
364    #[allow(clippy::unnecessary_wraps)] // To match stdlib function definition.
365    pub const fn checked_abs(self) -> Option<Self> { Some(self.abs()) }
366
367    /// Checked addition.
368    ///
369    /// Returns [`None`] if the sum is above [`SignedAmount::MAX`] or below [`SignedAmount::MIN`].
370    #[must_use]
371    pub const fn checked_add(self, rhs: Self) -> Option<Self> {
372        // No `map()` in const context.
373        match self.to_sat().checked_add(rhs.to_sat()) {
374            Some(res) => match Self::from_sat(res) {
375                Ok(amount) => Some(amount),
376                Err(_) => None,
377            },
378            None => None,
379        }
380    }
381
382    /// Checked subtraction.
383    ///
384    /// Returns [`None`] if the difference is above [`SignedAmount::MAX`] or below
385    /// [`SignedAmount::MIN`].
386    #[must_use]
387    pub const fn checked_sub(self, rhs: Self) -> Option<Self> {
388        // No `map()` in const context.
389        match self.to_sat().checked_sub(rhs.to_sat()) {
390            Some(res) => match Self::from_sat(res) {
391                Ok(amount) => Some(amount),
392                Err(_) => None,
393            },
394            None => None,
395        }
396    }
397
398    /// Checked multiplication.
399    ///
400    /// Returns [`None`] if the product is above [`SignedAmount::MAX`] or below
401    /// [`SignedAmount::MIN`].
402    #[must_use]
403    pub const fn checked_mul(self, rhs: i64) -> Option<Self> {
404        // No `map()` in const context.
405        match self.to_sat().checked_mul(rhs) {
406            Some(res) => match Self::from_sat(res) {
407                Ok(amount) => Some(amount),
408                Err(_) => None,
409            },
410            None => None,
411        }
412    }
413
414    /// Checked integer division.
415    ///
416    /// Be aware that integer division loses the remainder if no exact division can be made.
417    ///
418    /// Returns [`None`] if overflow occurred.
419    #[must_use]
420    pub const fn checked_div(self, rhs: i64) -> Option<Self> {
421        // No `map()` in const context.
422        match self.to_sat().checked_div(rhs) {
423            Some(res) => match Self::from_sat(res) {
424                Ok(amount) => Some(amount),
425                Err(_) => None, // Unreachable because of checked_div above.
426            },
427            None => None,
428        }
429    }
430
431    /// Checked remainder.
432    ///
433    /// Returns [`None`] if overflow occurred.
434    #[must_use]
435    pub const fn checked_rem(self, rhs: i64) -> Option<Self> {
436        // No `map()` in const context.
437        match self.to_sat().checked_rem(rhs) {
438            Some(res) => match Self::from_sat(res) {
439                Ok(amount) => Some(amount),
440                Err(_) => None, // Unreachable because of checked_rem above.
441            },
442            None => None,
443        }
444    }
445
446    /// Subtraction that doesn't allow negative [`SignedAmount`]s.
447    ///
448    /// Returns [`None`] if either `self`, `rhs` or the result is strictly negative.
449    #[must_use]
450    pub fn positive_sub(self, rhs: Self) -> Option<Self> {
451        if self.is_negative() || rhs.is_negative() || rhs > self {
452            None
453        } else {
454            self.checked_sub(rhs)
455        }
456    }
457
458    /// Converts to an unsigned amount.
459    ///
460    /// # Errors
461    ///
462    /// If the amount is negative.
463    #[allow(clippy::missing_panics_doc)]
464    pub fn to_unsigned(self) -> Result<Amount, OutOfRangeError> {
465        if self.is_negative() {
466            Err(OutOfRangeError::negative())
467        } else {
468            // Cast ok, checked not negative above.
469            Ok(Amount::from_sat(self.to_sat() as u64)
470                .expect("a positive signed amount is always valid"))
471        }
472    }
473}
474
475crate::internal_macros::impl_fmt_traits_for_u32_wrapper!(SignedAmount, to_sat);
476
477impl default::Default for SignedAmount {
478    fn default() -> Self { Self::ZERO }
479}
480
481impl fmt::Debug for SignedAmount {
482    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
483        write!(f, "SignedAmount({} SAT)", self.to_sat())
484    }
485}
486
487// No one should depend on a binding contract for Display for this type.
488// Just using Bitcoin denominated string.
489impl fmt::Display for SignedAmount {
490    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
491        fmt::Display::fmt(&self.display_in(Denomination::Bitcoin).show_denomination(), f)
492    }
493}
494
495impl FromStr for SignedAmount {
496    type Err = ParseError;
497
498    /// Parses a string slice where the slice includes a denomination.
499    ///
500    /// If the returned value would be zero or negative zero, then no denomination is required.
501    fn from_str(s: &str) -> Result<Self, Self::Err> {
502        let result = Self::from_str_with_denomination(s);
503
504        match result {
505            Err(ParseError(ParseErrorInner::MissingDenomination(_))) => {
506                let d = Self::from_str_in(s, Denomination::Satoshi);
507
508                if d == Ok(Self::ZERO) {
509                    Ok(Self::ZERO)
510                } else {
511                    result
512                }
513            }
514            _ => result,
515        }
516    }
517}
518
519impl From<Amount> for SignedAmount {
520    fn from(value: Amount) -> Self {
521        let v = value.to_sat() as i64; // Cast ok, signed amount and amount share positive range.
522        Self::from_sat(v).expect("all amounts are valid signed amounts")
523    }
524}
525
526#[cfg(feature = "arbitrary")]
527impl<'a> Arbitrary<'a> for SignedAmount {
528    fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
529        let sats = u.int_in_range(Self::MIN.to_sat()..=Self::MAX.to_sat())?;
530        Ok(Self::from_sat(sats).expect("range is valid"))
531    }
532}