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