bitcoin_payment_instructions/
amount.rs

1//! Because lightning uses "milli-satoshis" rather than satoshis for its native currency amount,
2//! parsing payment instructions requires amounts with sub-satoshi precision.
3//!
4//! Thus, here, we define an [`Amount`] type similar to [`bitcoin::Amount`] but with sub-satoshi
5//! precision.
6
7use core::fmt;
8
9/// An amount of Bitcoin
10///
11/// Sadly, because lightning uses "milli-satoshis" we cannot directly use rust-bitcon's `Amount`
12/// type.
13///
14/// In general, when displaying amounts to the user, you should use [`Self::sats_rounding_up`].
15#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
16// TODO: Move this into lightning-types
17pub struct Amount(u64);
18
19impl fmt::Debug for Amount {
20	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
21		write!(f, "{} milli-satoshis", self.0)
22	}
23}
24
25impl Amount {
26	/// The amount in milli-satoshis
27	#[inline]
28	pub const fn msats(&self) -> u64 {
29		self.0
30	}
31
32	/// The amount in satoshis, if it is exactly a whole number of sats.
33	#[inline]
34	pub const fn sats(&self) -> Result<u64, ()> {
35		if self.0 % 1000 == 0 {
36			Ok(self.0 / 1000)
37		} else {
38			Err(())
39		}
40	}
41
42	/// The amount in satoshis, rounding up to the next whole satoshi.
43	#[inline]
44	pub const fn sats_rounding_up(&self) -> u64 {
45		(self.0 + 999) / 1000
46	}
47
48	/// Constructs a new [`Amount`] for the given number of milli-satoshis.
49	#[inline]
50	pub const fn from_milli_sats(msats: u64) -> Self {
51		Amount(msats)
52	}
53
54	/// Constructs a new [`Amount`] for the given number of satoshis.
55	#[inline]
56	pub const fn from_sats(sats: u64) -> Self {
57		Amount(sats * 1000)
58	}
59
60	/// Adds an [`Amount`] to this [`Amount`], saturating to avoid overflowing 21 million bitcoin.
61	#[inline]
62	pub fn saturating_add(self, rhs: Amount) -> Amount {
63		match self.0.checked_add(rhs.0) {
64			Some(amt) if amt <= 21_000_000_0000_0000_000 => Amount(amt),
65			_ => Amount(21_000_000_0000_0000_000),
66		}
67	}
68
69	/// Returns an object that implements [`core::fmt::Display`] which writes out the amount, in
70	/// bitcoin, with a decimal point between the whole-bitcoin and partial-bitcoin amounts, with
71	/// any milli-satoshis rounded up to the next whole satoshi.
72	#[inline]
73	pub fn btc_decimal_rounding_up_to_sats(self) -> FormattedAmount {
74		FormattedAmount(self)
75	}
76}
77
78#[derive(Clone, Copy)]
79/// A simple type which wraps an [`Amount`] and formats it according to instructions when it was
80/// generated.
81pub struct FormattedAmount(Amount);
82
83impl fmt::Display for FormattedAmount {
84	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
85		let total_sats = self.0.sats_rounding_up();
86		let btc = total_sats / 1_0000_0000;
87		let mut sats = total_sats % 1_0000_0000;
88		write!(f, "{}", btc)?;
89		if sats != 0 {
90			let mut digits = 8;
91			while sats % 10 == 0 {
92				digits -= 1;
93				sats /= 10;
94			}
95			write!(f, ".{:0digits$}", sats, digits = digits)?;
96		}
97		Ok(())
98	}
99}
100
101#[cfg(test)]
102mod test {
103	use super::Amount;
104
105	use alloc::string::ToString;
106
107	#[test]
108	#[rustfmt::skip]
109	fn test_display() {
110		assert_eq!(Amount::from_milli_sats(0).btc_decimal_rounding_up_to_sats().to_string(),     "0");
111		assert_eq!(Amount::from_milli_sats(1).btc_decimal_rounding_up_to_sats().to_string(),     "0.00000001");
112		assert_eq!(Amount::from_sats(1).btc_decimal_rounding_up_to_sats().to_string(),           "0.00000001");
113		assert_eq!(Amount::from_sats(10).btc_decimal_rounding_up_to_sats().to_string(),          "0.0000001");
114		assert_eq!(Amount::from_sats(15).btc_decimal_rounding_up_to_sats().to_string(),          "0.00000015");
115		assert_eq!(Amount::from_sats(1_0000).btc_decimal_rounding_up_to_sats().to_string(),      "0.0001");
116		assert_eq!(Amount::from_sats(1_2345).btc_decimal_rounding_up_to_sats().to_string(),      "0.00012345");
117		assert_eq!(Amount::from_sats(1_2345_6789).btc_decimal_rounding_up_to_sats().to_string(), "1.23456789");
118		assert_eq!(Amount::from_sats(1_0000_0000).btc_decimal_rounding_up_to_sats().to_string(), "1");
119		assert_eq!(Amount::from_sats(5_0000_0000).btc_decimal_rounding_up_to_sats().to_string(), "5");
120	}
121}