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