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}