Skip to main content

fedimint_core/
amount.rs

1use std::num::ParseIntError;
2use std::str::FromStr;
3
4use bitcoin::Denomination;
5use serde::{Deserialize, Serialize};
6use thiserror::Error;
7
8use crate::encoding::{Decodable, Encodable};
9
10pub const SATS_PER_BITCOIN: u64 = 100_000_000;
11
12/// Shorthand for [`Amount::from_msats`]
13pub fn msats(msats: u64) -> Amount {
14    Amount::from_msats(msats)
15}
16
17/// Shorthand for [`Amount::from_sats`]
18pub fn sats(amount: u64) -> Amount {
19    Amount::from_sats(amount)
20}
21
22/// Represents an amount of BTC. The base denomination is millisatoshis, which
23/// is why the `Amount` type from rust-bitcoin isn't used instead.
24#[derive(
25    Clone,
26    Copy,
27    Eq,
28    PartialEq,
29    Ord,
30    PartialOrd,
31    Hash,
32    Deserialize,
33    Serialize,
34    Encodable,
35    Decodable,
36    Default,
37)]
38#[serde(transparent)]
39pub struct Amount {
40    // TODO: rename to `units`, with backward compat for the serialization?
41    pub msats: u64,
42}
43
44impl Amount {
45    pub const ZERO: Self = Self { msats: 0 };
46
47    /// Create an amount from a number of millisatoshis.
48    pub const fn from_msats(msats: u64) -> Self {
49        Self { msats }
50    }
51
52    pub const fn from_units(units: u64) -> Self {
53        Self { msats: units }
54    }
55
56    /// Create an amount from a number of satoshis.
57    pub const fn from_sats(sats: u64) -> Self {
58        Self::from_msats(sats * 1000)
59    }
60
61    /// Create an amount from a number of whole bitcoins.
62    pub const fn from_bitcoins(bitcoins: u64) -> Self {
63        Self::from_sats(bitcoins * SATS_PER_BITCOIN)
64    }
65
66    /// Parse a decimal string as a value in the given denomination.
67    ///
68    /// Note: This only parses the value string.  If you want to parse a value
69    /// with denomination, use [`FromStr`].
70    pub fn from_str_in(s: &str, denom: Denomination) -> Result<Self, ParseAmountError> {
71        if denom == Denomination::MilliSatoshi {
72            return Ok(Self::from_msats(s.parse()?));
73        }
74        let btc_amt = bitcoin::amount::Amount::from_str_in(s, denom)?;
75        // `bitcoin::Amount` parsing only bounds the value by `u64` sats, while
76        // `From<bitcoin::Amount>` asserts the 21M BTC supply cap
77        if btc_amt > bitcoin::Amount::MAX_MONEY {
78            return Err(ParseAmountError::TooLarge);
79        }
80        Ok(Self::from(btc_amt))
81    }
82
83    pub fn saturating_sub(self, other: Self) -> Self {
84        Self {
85            msats: self.msats.saturating_sub(other.msats),
86        }
87    }
88
89    pub fn mul_u64(self, other: u64) -> Self {
90        Self {
91            msats: self.msats * other,
92        }
93    }
94
95    /// Returns an error if the amount is more precise than satoshis (i.e. if it
96    /// has a milli-satoshi remainder). Otherwise, returns `Ok(())`.
97    pub fn ensure_sats_precision(&self) -> Result<(), AmountConversionError> {
98        if !self.msats.is_multiple_of(1000) {
99            return Err(AmountConversionError::SubSatoshiPrecision { msats: self.msats });
100        }
101        Ok(())
102    }
103
104    pub fn try_into_sats(&self) -> Result<u64, AmountConversionError> {
105        self.ensure_sats_precision()?;
106        Ok(self.msats / 1000)
107    }
108
109    pub const fn sats_round_down(&self) -> u64 {
110        self.msats / 1000
111    }
112
113    pub fn sats_f64(&self) -> f64 {
114        self.msats as f64 / 1000.0
115    }
116
117    pub fn checked_sub(self, other: Self) -> Option<Self> {
118        Some(Self {
119            msats: self.msats.checked_sub(other.msats)?,
120        })
121    }
122
123    pub fn checked_add(self, other: Self) -> Option<Self> {
124        Some(Self {
125            msats: self.msats.checked_add(other.msats)?,
126        })
127    }
128
129    pub fn checked_mul(self, other: u64) -> Option<Self> {
130        Some(Self {
131            msats: self.msats.checked_mul(other)?,
132        })
133    }
134}
135
136impl std::fmt::Display for Amount {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        write!(f, "{} msat", self.msats)
139    }
140}
141
142impl std::fmt::Debug for Amount {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        // Note: lack of space is intentional: in large Debug outputs extra space just
145        // make it harder to tell where fields being and end.
146        write!(f, "{}msat", self.msats)
147    }
148}
149
150impl std::ops::Rem for Amount {
151    type Output = Self;
152
153    fn rem(self, rhs: Self) -> Self::Output {
154        Self {
155            msats: self.msats % rhs.msats,
156        }
157    }
158}
159
160impl std::ops::RemAssign for Amount {
161    fn rem_assign(&mut self, rhs: Self) {
162        self.msats %= rhs.msats;
163    }
164}
165
166impl std::ops::Div for Amount {
167    type Output = u64;
168
169    fn div(self, rhs: Self) -> Self::Output {
170        self.msats / rhs.msats
171    }
172}
173
174impl std::ops::SubAssign for Amount {
175    fn sub_assign(&mut self, rhs: Self) {
176        self.msats -= rhs.msats;
177    }
178}
179
180impl std::ops::Mul<u64> for Amount {
181    type Output = Self;
182
183    fn mul(self, rhs: u64) -> Self::Output {
184        Self {
185            msats: self.msats * rhs,
186        }
187    }
188}
189
190impl std::ops::Mul<Amount> for u64 {
191    type Output = Amount;
192
193    fn mul(self, rhs: Amount) -> Self::Output {
194        Amount {
195            msats: self * rhs.msats,
196        }
197    }
198}
199
200impl std::ops::Add for Amount {
201    type Output = Self;
202
203    fn add(self, rhs: Self) -> Self::Output {
204        Self {
205            msats: self.msats + rhs.msats,
206        }
207    }
208}
209
210impl std::ops::Sub for Amount {
211    type Output = Self;
212
213    fn sub(self, rhs: Self) -> Self::Output {
214        Self {
215            msats: self.msats - rhs.msats,
216        }
217    }
218}
219
220impl std::ops::AddAssign for Amount {
221    fn add_assign(&mut self, rhs: Self) {
222        *self = *self + rhs;
223    }
224}
225
226impl std::iter::Sum for Amount {
227    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
228        Self {
229            msats: iter.map(|amt| amt.msats).sum::<u64>(),
230        }
231    }
232}
233
234impl FromStr for Amount {
235    type Err = ParseAmountError;
236
237    fn from_str(s: &str) -> Result<Self, Self::Err> {
238        if let Some(i) = s.find(char::is_alphabetic) {
239            let (amt, denom) = s.split_at(i);
240            Self::from_str_in(
241                amt.trim(),
242                denom
243                    .trim()
244                    .parse()
245                    .map_err(ParseAmountError::WrongBitcoinDenomination)?,
246            )
247        } else {
248            // default to millisatoshi
249            Self::from_str_in(s.trim(), Denomination::MilliSatoshi)
250        }
251    }
252}
253
254impl From<bitcoin::Amount> for Amount {
255    fn from(amt: bitcoin::Amount) -> Self {
256        assert!(amt.to_sat() <= 2_100_000_000_000_000);
257        Self {
258            msats: amt.to_sat() * 1000,
259        }
260    }
261}
262
263impl TryFrom<Amount> for bitcoin::Amount {
264    type Error = AmountConversionError;
265
266    fn try_from(value: Amount) -> Result<Self, Self::Error> {
267        value.try_into_sats().map(Self::from_sat)
268    }
269}
270
271/// Failure to convert an [`Amount`] to a coarser unit.
272#[derive(Debug, Error, Clone, Eq, PartialEq)]
273#[non_exhaustive]
274pub enum AmountConversionError {
275    /// The amount has a milli-satoshi remainder and cannot be expressed in
276    /// satoshis.
277    #[error("Amount {msats} msat is more precise than a satoshi, cannot convert to satoshis")]
278    SubSatoshiPrecision { msats: u64 },
279}
280
281#[derive(Error, Debug)]
282pub enum ParseAmountError {
283    #[error("Error parsing string as integer: {0}")]
284    NotANumber(#[from] ParseIntError),
285    #[error("Error parsing string as a bitcoin amount: {0}")]
286    WrongBitcoinAmount(#[from] bitcoin::amount::ParseAmountError),
287    #[error("Error parsing string as a bitcoin denomination: {0}")]
288    WrongBitcoinDenomination(#[source] <Denomination as FromStr>::Err),
289    #[error("Amount exceeds the maximum bitcoin supply of 21,000,000 BTC")]
290    TooLarge,
291}
292
293#[cfg(test)]
294mod tests;