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}