fedimint_core/
amount.rs

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