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    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::fmt::Debug for Amount {
128    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129        // Note: lack of space is intentional: in large Debug outputs extra space just
130        // make it harder to tell where fields being and end.
131        write!(f, "{}msat", self.msats)
132    }
133}
134
135impl std::ops::Rem for Amount {
136    type Output = Self;
137
138    fn rem(self, rhs: Self) -> Self::Output {
139        Self {
140            msats: self.msats % rhs.msats,
141        }
142    }
143}
144
145impl std::ops::RemAssign for Amount {
146    fn rem_assign(&mut self, rhs: Self) {
147        self.msats %= rhs.msats;
148    }
149}
150
151impl std::ops::Div for Amount {
152    type Output = u64;
153
154    fn div(self, rhs: Self) -> Self::Output {
155        self.msats / rhs.msats
156    }
157}
158
159impl std::ops::SubAssign for Amount {
160    fn sub_assign(&mut self, rhs: Self) {
161        self.msats -= rhs.msats;
162    }
163}
164
165impl std::ops::Mul<u64> for Amount {
166    type Output = Self;
167
168    fn mul(self, rhs: u64) -> Self::Output {
169        Self {
170            msats: self.msats * rhs,
171        }
172    }
173}
174
175impl std::ops::Mul<Amount> for u64 {
176    type Output = Amount;
177
178    fn mul(self, rhs: Amount) -> Self::Output {
179        Amount {
180            msats: self * rhs.msats,
181        }
182    }
183}
184
185impl std::ops::Add for Amount {
186    type Output = Self;
187
188    fn add(self, rhs: Self) -> Self::Output {
189        Self {
190            msats: self.msats + rhs.msats,
191        }
192    }
193}
194
195impl std::ops::AddAssign for Amount {
196    fn add_assign(&mut self, rhs: Self) {
197        *self = *self + rhs;
198    }
199}
200
201impl std::iter::Sum for Amount {
202    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
203        Self {
204            msats: iter.map(|amt| amt.msats).sum::<u64>(),
205        }
206    }
207}
208
209impl FromStr for Amount {
210    type Err = ParseAmountError;
211
212    fn from_str(s: &str) -> Result<Self, Self::Err> {
213        if let Some(i) = s.find(char::is_alphabetic) {
214            let (amt, denom) = s.split_at(i);
215            Self::from_str_in(amt.trim(), denom.trim().parse()?)
216        } else {
217            // default to millisatoshi
218            Self::from_str_in(s.trim(), Denomination::MilliSatoshi)
219        }
220    }
221}
222
223impl From<bitcoin::Amount> for Amount {
224    fn from(amt: bitcoin::Amount) -> Self {
225        assert!(amt.to_sat() <= 2_100_000_000_000_000);
226        Self {
227            msats: amt.to_sat() * 1000,
228        }
229    }
230}
231
232impl TryFrom<Amount> for bitcoin::Amount {
233    type Error = anyhow::Error;
234
235    fn try_from(value: Amount) -> anyhow::Result<Self> {
236        value.try_into_sats().map(Self::from_sat)
237    }
238}
239
240#[derive(Error, Debug)]
241pub enum ParseAmountError {
242    #[error("Error parsing string as integer: {0}")]
243    NotANumber(#[from] ParseIntError),
244    #[error("Error parsing string as a bitcoin amount: {0}")]
245    WrongBitcoinAmount(#[from] bitcoin::amount::ParseAmountError),
246    #[error("Error parsing string as a bitcoin denomination: {0}")]
247    WrongBitcoinDenomination(#[from] bitcoin_units::amount::ParseDenominationError),
248}
249
250#[cfg(test)]
251mod tests;