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    Debug,
27    Clone,
28    Copy,
29    Eq,
30    PartialEq,
31    Ord,
32    PartialOrd,
33    Hash,
34    Deserialize,
35    Serialize,
36    Encodable,
37    Decodable,
38)]
39#[serde(transparent)]
40pub struct Amount {
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    /// Create an amount from a number of satoshis.
53    pub const fn from_sats(sats: u64) -> Self {
54        Self::from_msats(sats * 1000)
55    }
56
57    /// Create an amount from a number of whole bitcoins.
58    pub const fn from_bitcoins(bitcoins: u64) -> Self {
59        Self::from_sats(bitcoins * SATS_PER_BITCOIN)
60    }
61
62    /// Parse a decimal string as a value in the given denomination.
63    ///
64    /// Note: This only parses the value string.  If you want to parse a value
65    /// with denomination, use [`FromStr`].
66    pub fn from_str_in(s: &str, denom: Denomination) -> Result<Self, ParseAmountError> {
67        if denom == Denomination::MilliSatoshi {
68            return Ok(Self::from_msats(s.parse()?));
69        }
70        let btc_amt = bitcoin::amount::Amount::from_str_in(s, denom)?;
71        Ok(Self::from(btc_amt))
72    }
73
74    pub fn saturating_sub(self, other: Self) -> Self {
75        Self {
76            msats: self.msats.saturating_sub(other.msats),
77        }
78    }
79
80    pub fn mul_u64(self, other: u64) -> Self {
81        Self {
82            msats: self.msats * other,
83        }
84    }
85
86    /// Returns an error if the amount is more precise than satoshis (i.e. if it
87    /// has a milli-satoshi remainder). Otherwise, returns `Ok(())`.
88    pub fn ensure_sats_precision(&self) -> anyhow::Result<()> {
89        if self.msats % 1000 != 0 {
90            bail!("Amount is using a precision smaller than satoshi, cannot convert to satoshis");
91        }
92        Ok(())
93    }
94
95    pub fn try_into_sats(&self) -> anyhow::Result<u64> {
96        self.ensure_sats_precision()?;
97        Ok(self.msats / 1000)
98    }
99
100    pub const fn sats_round_down(&self) -> u64 {
101        self.msats / 1000
102    }
103
104    pub fn sats_f64(&self) -> f64 {
105        self.msats as f64 / 1000.0
106    }
107
108    pub fn checked_sub(self, other: Self) -> Option<Self> {
109        Some(Self {
110            msats: self.msats.checked_sub(other.msats)?,
111        })
112    }
113
114    pub fn checked_add(self, other: Self) -> Option<Self> {
115        Some(Self {
116            msats: self.msats.checked_add(other.msats)?,
117        })
118    }
119}
120
121impl std::fmt::Display for Amount {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{} msat", self.msats)
124    }
125}
126
127impl std::ops::Rem for Amount {
128    type Output = Self;
129
130    fn rem(self, rhs: Self) -> Self::Output {
131        Self {
132            msats: self.msats % rhs.msats,
133        }
134    }
135}
136
137impl std::ops::RemAssign for Amount {
138    fn rem_assign(&mut self, rhs: Self) {
139        self.msats %= rhs.msats;
140    }
141}
142
143impl std::ops::Div for Amount {
144    type Output = u64;
145
146    fn div(self, rhs: Self) -> Self::Output {
147        self.msats / rhs.msats
148    }
149}
150
151impl std::ops::SubAssign for Amount {
152    fn sub_assign(&mut self, rhs: Self) {
153        self.msats -= rhs.msats;
154    }
155}
156
157impl std::ops::Mul<u64> for Amount {
158    type Output = Self;
159
160    fn mul(self, rhs: u64) -> Self::Output {
161        Self {
162            msats: self.msats * rhs,
163        }
164    }
165}
166
167impl std::ops::Mul<Amount> for u64 {
168    type Output = Amount;
169
170    fn mul(self, rhs: Amount) -> Self::Output {
171        Amount {
172            msats: self * rhs.msats,
173        }
174    }
175}
176
177impl std::ops::Add for Amount {
178    type Output = Self;
179
180    fn add(self, rhs: Self) -> Self::Output {
181        Self {
182            msats: self.msats + rhs.msats,
183        }
184    }
185}
186
187impl std::ops::AddAssign for Amount {
188    fn add_assign(&mut self, rhs: Self) {
189        *self = *self + rhs;
190    }
191}
192
193impl std::iter::Sum for Amount {
194    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
195        Self {
196            msats: iter.map(|amt| amt.msats).sum::<u64>(),
197        }
198    }
199}
200
201impl std::ops::Sub for Amount {
202    type Output = Self;
203
204    fn sub(self, rhs: Self) -> Self::Output {
205        Self {
206            msats: self.msats - rhs.msats,
207        }
208    }
209}
210
211impl FromStr for Amount {
212    type Err = ParseAmountError;
213
214    fn from_str(s: &str) -> Result<Self, Self::Err> {
215        if let Some(i) = s.find(char::is_alphabetic) {
216            let (amt, denom) = s.split_at(i);
217            Self::from_str_in(amt.trim(), denom.trim().parse()?)
218        } else {
219            // default to millisatoshi
220            Self::from_str_in(s.trim(), Denomination::MilliSatoshi)
221        }
222    }
223}
224
225impl From<bitcoin::Amount> for Amount {
226    fn from(amt: bitcoin::Amount) -> Self {
227        assert!(amt.to_sat() <= 2_100_000_000_000_000);
228        Self {
229            msats: amt.to_sat() * 1000,
230        }
231    }
232}
233
234impl TryFrom<Amount> for bitcoin::Amount {
235    type Error = anyhow::Error;
236
237    fn try_from(value: Amount) -> anyhow::Result<Self> {
238        value.try_into_sats().map(Self::from_sat)
239    }
240}
241
242#[derive(Error, Debug)]
243pub enum ParseAmountError {
244    #[error("Error parsing string as integer: {0}")]
245    NotANumber(#[from] ParseIntError),
246    #[error("Error parsing string as a bitcoin amount: {0}")]
247    WrongBitcoinAmount(#[from] bitcoin::amount::ParseAmountError),
248    #[error("Error parsing string as a bitcoin denomination: {0}")]
249    WrongBitcoinDenomination(#[from] bitcoin_units::amount::ParseDenominationError),
250}
251
252#[cfg(test)]
253mod tests {
254    use super::*;
255
256    #[test]
257    fn amount_multiplication_by_scalar() {
258        assert_eq!(Amount::from_msats(1000) * 123, Amount::from_msats(123_000));
259    }
260
261    #[test]
262    fn scalar_multiplication_by_amount() {
263        assert_eq!(123 * Amount::from_msats(1000), Amount::from_msats(123_000));
264    }
265
266    #[test]
267    fn test_amount_parsing() {
268        // msats
269        assert_eq!(Amount::from_msats(123), Amount::from_str("123").unwrap());
270        assert_eq!(
271            Amount::from_msats(123),
272            Amount::from_str("123msat").unwrap()
273        );
274        assert_eq!(
275            Amount::from_msats(123),
276            Amount::from_str("123 msat").unwrap()
277        );
278        assert_eq!(
279            Amount::from_msats(123),
280            Amount::from_str("123 msats").unwrap()
281        );
282        // sats
283        assert_eq!(Amount::from_sats(123), Amount::from_str("123sat").unwrap());
284        assert_eq!(Amount::from_sats(123), Amount::from_str("123 sat").unwrap());
285        assert_eq!(
286            Amount::from_sats(123),
287            Amount::from_str("123satoshi").unwrap()
288        );
289        assert_eq!(
290            Amount::from_sats(123),
291            Amount::from_str("123satoshis").unwrap()
292        );
293        // btc
294        assert_eq!(
295            Amount::from_bitcoins(123),
296            Amount::from_str("123btc").unwrap()
297        );
298        assert_eq!(
299            Amount::from_sats(12_345_600_000),
300            Amount::from_str("123.456btc").unwrap()
301        );
302    }
303}