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