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 bitcoin::Amount as BitcoinAmount;
8
9use core::fmt;
10
11/// An amount of Bitcoin
12///
13/// Sadly, because lightning uses "milli-satoshis" we cannot directly use rust-bitcoin's `Amount`
14/// type.
15///
16/// In general, when displaying amounts to the user, you should use [`Self::sats_rounding_up`].
17#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
18pub struct Amount(u64);
19
20impl fmt::Debug for Amount {
21	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
22		write!(f, "{} milli-satoshis", self.0)
23	}
24}
25
26const MAX_MSATS: u64 = 21_000_000_0000_0000_000;
27
28impl Amount {
29	/// The maximum possible [`Amount`], equal to 21 million BTC
30	pub const MAX: Amount = Amount(MAX_MSATS);
31
32	/// Zero milli-satoshis
33	pub const ZERO: Amount = Amount(0);
34
35	/// The amount in milli-satoshis
36	#[inline]
37	pub const fn milli_sats(&self) -> u64 {
38		self.0
39	}
40
41	/// The amount in satoshis, if it is exactly a whole number of sats.
42	#[inline]
43	pub const fn sats(&self) -> Result<u64, ()> {
44		if self.0 % 1000 == 0 {
45			Ok(self.0 / 1000)
46		} else {
47			Err(())
48		}
49	}
50
51	/// The amount in satoshis, rounding up to the next whole satoshi.
52	#[inline]
53	pub const fn sats_rounding_up(&self) -> u64 {
54		(self.0 + 999) / 1000
55	}
56
57	/// Constructs a new [`Amount`] for the given number of milli-satoshis.
58	///
59	/// Fails only if `msats` is greater than 21 million Bitcoin (in milli-satoshis).
60	#[inline]
61	pub const fn from_milli_sats(msats: u64) -> Result<Self, ()> {
62		if msats > MAX_MSATS {
63			Err(())
64		} else {
65			Ok(Amount(msats))
66		}
67	}
68
69	/// Constructs a new [`Amount`] for the given number of satoshis.
70	///
71	/// Fails only if `sats` is greater than 21 million Bitcoin (in satoshis).
72	#[inline]
73	pub const fn from_sats(sats: u64) -> Result<Self, ()> {
74		Self::from_milli_sats(sats.saturating_mul(1000))
75	}
76
77	/// Constructs a new [`Amount`] for the given number of satoshis, panicking if the amount is
78	/// too large.
79	pub(crate) const fn from_sats_panicy(sats: u64) -> Self {
80		let amt = sats.saturating_mul(1000);
81		if amt > MAX_MSATS {
82			panic!("Sats value greater than 21 million Bitcoin");
83		} else {
84			Amount(amt)
85		}
86	}
87
88	/// Adds an [`Amount`] to this [`Amount`], saturating to avoid overflowing 21 million bitcoin.
89	#[inline]
90	pub const fn saturating_add(self, rhs: Amount) -> Amount {
91		match self.0.checked_add(rhs.0) {
92			Some(amt) if amt <= MAX_MSATS => Amount(amt),
93			_ => Amount(MAX_MSATS),
94		}
95	}
96
97	/// Subtracts an [`Amount`] from this [`Amount`], saturating to avoid underflowing.
98	#[inline]
99	pub const fn saturating_sub(self, rhs: Amount) -> Amount {
100		Amount(self.0.saturating_sub(rhs.0))
101	}
102
103	/// Returns an object that implements [`core::fmt::Display`] which writes out the amount, in
104	/// bitcoin, with a decimal point between the whole-bitcoin and partial-bitcoin amounts, with
105	/// any milli-satoshis rounded up to the next whole satoshi.
106	#[inline]
107	pub fn btc_decimal_rounding_up_to_sats(self) -> FormattedAmount {
108		FormattedAmount(self)
109	}
110}
111
112#[derive(Clone, Copy)]
113/// A simple type which wraps an [`Amount`] and formats it according to instructions when it was
114/// generated.
115pub struct FormattedAmount(Amount);
116
117impl fmt::Display for FormattedAmount {
118	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
119		let total_sats = self.0.sats_rounding_up();
120		let btc = total_sats / 1_0000_0000;
121		let mut sats = total_sats % 1_0000_0000;
122		write!(f, "{}", btc)?;
123		if sats != 0 {
124			let mut digits = 8;
125			while sats % 10 == 0 {
126				digits -= 1;
127				sats /= 10;
128			}
129			write!(f, ".{:0digits$}", sats, digits = digits)?;
130		}
131		Ok(())
132	}
133}
134
135impl From<BitcoinAmount> for Amount {
136	fn from(amt: BitcoinAmount) -> Amount {
137		Amount(amt.to_sat() * 1000)
138	}
139}
140
141#[cfg(test)]
142mod test {
143	use super::Amount;
144
145	use alloc::string::ToString;
146
147	#[test]
148	#[rustfmt::skip]
149	fn test_display() {
150		assert_eq!(Amount::from_milli_sats(0).unwrap().btc_decimal_rounding_up_to_sats().to_string(),     "0");
151		assert_eq!(Amount::from_milli_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(),     "0.00000001");
152		assert_eq!(Amount::from_sats(1).unwrap().btc_decimal_rounding_up_to_sats().to_string(),           "0.00000001");
153		assert_eq!(Amount::from_sats(10).unwrap().btc_decimal_rounding_up_to_sats().to_string(),          "0.0000001");
154		assert_eq!(Amount::from_sats(15).unwrap().btc_decimal_rounding_up_to_sats().to_string(),          "0.00000015");
155		assert_eq!(Amount::from_sats(1_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(),      "0.0001");
156		assert_eq!(Amount::from_sats(1_2345).unwrap().btc_decimal_rounding_up_to_sats().to_string(),      "0.00012345");
157		assert_eq!(Amount::from_sats(1_2345_6789).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1.23456789");
158		assert_eq!(Amount::from_sats(1_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "1");
159		assert_eq!(Amount::from_sats(5_0000_0000).unwrap().btc_decimal_rounding_up_to_sats().to_string(), "5");
160	}
161}