rgbinvoice/
amount.rs

1// RGB wallet library for smart contracts on Bitcoin & Lightning network
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2019-2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association. All rights reserved.
9//
10// Licensed under the Apache License, Version 2.0 (the "License");
11// you may not use this file except in compliance with the License.
12// You may obtain a copy of the License at
13//
14//     http://www.apache.org/licenses/LICENSE-2.0
15//
16// Unless required by applicable law or agreed to in writing, software
17// distributed under the License is distributed on an "AS IS" BASIS,
18// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19// See the License for the specific language governing permissions and
20// limitations under the License.
21
22use std::cmp::Ordering;
23use std::fmt;
24use std::fmt::{Display, Formatter, Write};
25use std::iter::Sum;
26use std::num::{ParseIntError, TryFromIntError};
27use std::str::FromStr;
28
29use rgb::{FungibleState, RevealedValue};
30#[cfg(feature = "serde")]
31use serde::{Deserialize, Serialize};
32use strict_encoding::{DefaultBasedStrictDumb, StrictDeserialize, StrictSerialize, VariantError};
33use strict_types::StrictVal;
34
35use crate::LIB_NAME_RGB_CONTRACT;
36
37pub const ENC_BASE32_NODIGIT: &[u8; 32] = b"abcdefghkmnABCDEFGHKMNPQRSTVWXYZ";
38fast32::make_base32_alpha!(BASE32_NODIGIT, DEC_BASE32_NODIGIT, ENC_BASE32_NODIGIT);
39
40#[derive(Wrapper, WrapperMut, Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, From)]
41#[wrapper(Add, Sub, Mul, Div, Rem)]
42#[wrapper_mut(AddAssign, SubAssign, MulAssign, DivAssign, RemAssign)]
43#[derive(StrictType, StrictEncode, StrictDecode, StrictDumb)]
44#[strict_type(lib = LIB_NAME_RGB_CONTRACT)]
45#[cfg_attr(
46    feature = "serde",
47    derive(Serialize, Deserialize),
48    serde(crate = "serde_crate", transparent)
49)]
50pub struct Amount(
51    #[from]
52    #[from(u32)]
53    #[from(u16)]
54    #[from(u8)]
55    u64,
56);
57
58impl StrictSerialize for Amount {}
59impl StrictDeserialize for Amount {}
60
61impl From<RevealedValue> for Amount {
62    fn from(value: RevealedValue) -> Self { Amount(value.as_u64()) }
63}
64
65impl From<FungibleState> for Amount {
66    fn from(state: FungibleState) -> Self { Amount(state.as_u64()) }
67}
68
69impl From<Amount> for FungibleState {
70    fn from(amount: Amount) -> Self { FungibleState::Bits64(amount.0) }
71}
72
73impl From<Amount> for RevealedValue {
74    fn from(amount: Amount) -> Self { RevealedValue::from(FungibleState::Bits64(amount.0)) }
75}
76
77impl Amount {
78    pub const ZERO: Self = Amount(0);
79
80    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self {
81        value.unwrap_uint::<u64>().into()
82    }
83
84    pub fn with_precision(amount: u64, precision: impl Into<Precision>) -> Self {
85        precision.into().unchecked_convert(amount)
86    }
87
88    pub fn with_precision_checked(amount: u64, precision: impl Into<Precision>) -> Option<Self> {
89        precision.into().checked_convert(amount)
90    }
91
92    pub fn value(self) -> u64 { self.0 }
93
94    pub fn split(self, precision: impl Into<Precision>) -> (u64, u64) {
95        let precision = precision.into();
96        let int = self.floor(precision);
97        let fract = self.rem(precision);
98        (int, fract)
99    }
100
101    pub fn round(&self, precision: impl Into<Precision>) -> u64 {
102        let precision = precision.into();
103        let mul = precision.multiplier();
104        if self.0 == 0 {
105            return 0;
106        }
107        let inc = 2 * self.rem(precision) / mul;
108        self.0 / mul + inc
109    }
110
111    pub fn ceil(&self, precision: impl Into<Precision>) -> u64 {
112        let precision = precision.into();
113        if self.0 == 0 {
114            return 0;
115        }
116        let inc = if self.rem(precision) > 0 { 1 } else { 0 };
117        self.0 / precision.multiplier() + inc
118    }
119
120    pub fn floor(&self, precision: impl Into<Precision>) -> u64 {
121        if self.0 == 0 {
122            return 0;
123        }
124        self.0 / precision.into().multiplier()
125    }
126
127    pub fn rem(&self, precision: impl Into<Precision>) -> u64 {
128        self.0 % precision.into().multiplier()
129    }
130
131    pub fn saturating_add(&self, other: impl Into<Self>) -> Self {
132        self.0.saturating_add(other.into().0).into()
133    }
134    pub fn saturating_sub(&self, other: impl Into<Self>) -> Self {
135        self.0.saturating_sub(other.into().0).into()
136    }
137
138    pub fn saturating_add_assign(&mut self, other: impl Into<Self>) {
139        *self = self.0.saturating_add(other.into().0).into();
140    }
141    pub fn saturating_sub_assign(&mut self, other: impl Into<Self>) {
142        *self = self.0.saturating_sub(other.into().0).into();
143    }
144
145    #[must_use]
146    pub fn checked_add(&self, other: impl Into<Self>) -> Option<Self> {
147        self.0.checked_add(other.into().0).map(Self)
148    }
149    #[must_use]
150    pub fn checked_sub(&self, other: impl Into<Self>) -> Option<Self> {
151        self.0.checked_sub(other.into().0).map(Self)
152    }
153
154    #[must_use]
155    pub fn checked_add_assign(&mut self, other: impl Into<Self>) -> Option<()> {
156        *self = self.0.checked_add(other.into().0).map(Self)?;
157        Some(())
158    }
159    #[must_use]
160    pub fn checked_sub_assign(&mut self, other: impl Into<Self>) -> Option<()> {
161        *self = self.0.checked_sub(other.into().0).map(Self)?;
162        Some(())
163    }
164}
165
166impl Sum<u64> for Amount {
167    fn sum<I: Iterator<Item = u64>>(iter: I) -> Self {
168        iter.fold(Amount::ZERO, |sum, value| sum.saturating_add(value))
169    }
170}
171
172impl Sum for Amount {
173    fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
174        iter.fold(Amount::ZERO, |sum, value| sum.saturating_add(value))
175    }
176}
177
178impl Display for Amount {
179    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
180        let bytes = self.0.to_le_bytes();
181        let pos = bytes.iter().rposition(|b| *b != 0).unwrap_or(0) + 1;
182        let s = BASE32_NODIGIT.encode(&bytes[..pos]);
183        f.write_str(&s)
184    }
185}
186
187impl FromStr for Amount {
188    type Err = fast32::DecodeError;
189
190    fn from_str(s: &str) -> Result<Self, Self::Err> {
191        let amount = BASE32_NODIGIT.decode_str(s)?;
192        let len = amount.len();
193        if len > 8 {
194            return Err(fast32::DecodeError::InvalidLength {
195                length: amount.len(),
196            });
197        }
198        let mut le = [0u8; 8];
199        le[..len].copy_from_slice(&amount);
200        Ok(Amount(u64::from_le_bytes(le)))
201    }
202}
203
204#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug, Default)]
205#[repr(u8)]
206#[derive(StrictType, StrictEncode, StrictDecode)]
207#[strict_type(lib = LIB_NAME_RGB_CONTRACT, tags = repr, into_u8, try_from_u8)]
208#[cfg_attr(
209    feature = "serde",
210    derive(Serialize, Deserialize),
211    serde(crate = "serde_crate", rename_all = "camelCase")
212)]
213pub enum Precision {
214    Indivisible = 0,
215    Deci = 1,
216    Centi = 2,
217    Milli = 3,
218    DeciMilli = 4,
219    CentiMilli = 5,
220    Micro = 6,
221    DeciMicro = 7,
222    #[default]
223    CentiMicro = 8,
224    Nano = 9,
225    DeciNano = 10,
226    CentiNano = 11,
227    Pico = 12,
228    DeciPico = 13,
229    CentiPico = 14,
230    Femto = 15,
231    DeciFemto = 16,
232    CentiFemto = 17,
233    Atto = 18,
234}
235impl DefaultBasedStrictDumb for Precision {}
236impl StrictSerialize for Precision {}
237impl StrictDeserialize for Precision {}
238
239impl Precision {
240    pub fn from_strict_val_unchecked(value: &StrictVal) -> Self { value.unwrap_enum() }
241    pub const fn decimals(self) -> u8 { self as u8 }
242
243    pub const fn multiplier(self) -> u64 {
244        match self {
245            Precision::Indivisible => 1,
246            Precision::Deci => 10,
247            Precision::Centi => 100,
248            Precision::Milli => 1000,
249            Precision::DeciMilli => 10_000,
250            Precision::CentiMilli => 100_000,
251            Precision::Micro => 1_000_000,
252            Precision::DeciMicro => 10_000_000,
253            Precision::CentiMicro => 100_000_000,
254            Precision::Nano => 1_000_000_000,
255            Precision::DeciNano => 10_000_000_000,
256            Precision::CentiNano => 100_000_000_000,
257            Precision::Pico => 1_000_000_000_000,
258            Precision::DeciPico => 10_000_000_000_000,
259            Precision::CentiPico => 100_000_000_000_000,
260            Precision::Femto => 1_000_000_000_000_000,
261            Precision::DeciFemto => 10_000_000_000_000_000,
262            Precision::CentiFemto => 100_000_000_000_000_000,
263            Precision::Atto => 1_000_000_000_000_000_000,
264        }
265    }
266
267    pub fn unchecked_convert(self, amount: impl Into<u64>) -> Amount {
268        (amount.into() * self.multiplier()).into()
269    }
270
271    pub fn checked_convert(self, amount: impl Into<u64>) -> Option<Amount> {
272        amount
273            .into()
274            .checked_mul(self.multiplier())
275            .map(Amount::from)
276    }
277    pub fn saturating_convert(self, amount: impl Into<u64>) -> Amount {
278        amount.into().saturating_mul(self.multiplier()).into()
279    }
280}
281
282impl From<Precision> for u16 {
283    fn from(value: Precision) -> Self { value as u8 as u16 }
284}
285
286impl From<Precision> for u32 {
287    fn from(value: Precision) -> Self { value as u8 as u32 }
288}
289
290impl From<Precision> for u64 {
291    fn from(value: Precision) -> Self { value as u8 as u64 }
292}
293
294#[derive(Copy, Clone, Eq, PartialEq, Debug, Display, Error)]
295#[display("invalid precision")]
296pub struct PrecisionError;
297
298#[derive(Getters, Copy, Clone, Eq, PartialEq, Hash, Debug)]
299pub struct CoinAmount {
300    #[getter(as_copy)]
301    int: u64,
302    #[getter(as_copy)]
303    fract: u64,
304    #[getter(as_copy)]
305    precision: Precision,
306}
307
308impl CoinAmount {
309    pub fn new(amount: impl Into<Amount>, precision: impl Into<Precision>) -> Self {
310        let precision = precision.into();
311        let amount = amount.into();
312        let (int, fract) = amount.split(precision);
313        CoinAmount {
314            int,
315            fract,
316            precision,
317        }
318    }
319
320    pub fn with(
321        int: u64,
322        fract: u64,
323        precision: impl Into<Precision>,
324    ) -> Result<Self, PrecisionError> {
325        let precision = precision.into();
326        // 2^64 ~ 10^19 < 10^18 (18 is max value for Precision enum)
327        let pow = 10u64.pow(precision.decimals() as u32);
328        // number of decimals can't be larger than the smallest possible integer
329        if fract >= pow {
330            return Err(PrecisionError);
331        }
332        Ok(CoinAmount {
333            int,
334            fract,
335            precision,
336        })
337    }
338
339    pub(crate) fn to_amount_unchecked(self) -> Amount {
340        // 2^64 ~ 10^19 < 10^18 (18 is max value for Precision enum)
341        let pow = 10u64.pow(self.precision.decimals() as u32);
342        // number of decimals can't be larger than the smallest possible integer
343        self.int
344            .checked_mul(pow)
345            .expect("CoinAmount type garantees are broken")
346            .checked_add(self.fract)
347            .expect(
348                "integer has at least the same number of zeros in the lowest digits as much as \
349                 decimals has digits at most, so overflow is not possible",
350            )
351            .into()
352    }
353}
354
355impl Display for CoinAmount {
356    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
357        if ![' ', '_'].contains(&f.fill()) {
358            panic!("disallowed fill character {} in coin amount formatting string", f.fill())
359        }
360        if f.precision().is_some() {
361            panic!("formatting precision must not be used for coin amounts")
362        }
363        let fill = &f.fill().to_string();
364        let to_chunks = |s: &str| -> String {
365            s.chars()
366                .rev()
367                .collect::<String>()
368                .as_bytes()
369                .chunks(3)
370                .map(<[u8]>::to_owned)
371                .map(|mut chunk| unsafe {
372                    chunk.reverse();
373                    String::from_utf8_unchecked(chunk)
374                })
375                .rev()
376                .collect::<Vec<_>>()
377                .join(fill)
378        };
379        let mut int = self.int.to_string();
380        if f.alternate() {
381            int = to_chunks(&int);
382        }
383        f.write_str(&int)?;
384        if self.fract > 0 || f.alternate() {
385            f.write_char('.')?;
386            let mut float = self.fract.to_string();
387            let len = float.len();
388            let decimals = self.precision.decimals() as usize;
389            match len.cmp(&decimals) {
390                Ordering::Less => {
391                    float = format!("{:0>width$}{float}", "", width = decimals - len);
392                }
393                Ordering::Equal => {}
394                Ordering::Greater => panic!("float precision overflow for coin amount {self:?}"),
395            }
396            if f.alternate() {
397                float = to_chunks(&float);
398            } else {
399                float = float.trim_end_matches('0').to_string();
400            }
401            f.write_str(&float)?;
402        }
403        if !f.alternate() {
404            write!(f, "~{}", self.precision.decimals())?;
405        }
406        Ok(())
407    }
408}
409
410#[derive(Clone, Eq, PartialEq, Debug, Display, Error, From)]
411#[display(doc_comments)]
412pub enum AmountParseError {
413    /// invalid amount integer part - {0}
414    InvalidInt(ParseIntError),
415    /// invalid amount fractional part - {0}
416    InvalidFract(ParseIntError),
417    /// invalid amount precision - {0}
418    InvalidPrecision(ParseIntError),
419
420    /// invalid amount precision exceeding 18
421    #[from(TryFromIntError)]
422    PrecisionOverflow,
423
424    /// invalid amount precision exceeding 18
425    #[from]
426    UnknownPrecision(VariantError<u8>),
427}
428
429impl FromStr for CoinAmount {
430    type Err = AmountParseError;
431
432    fn from_str(s: &str) -> Result<Self, Self::Err> {
433        let s = s.replace([' ', '_'], "");
434        let (int, remain) = s.split_once('.').unwrap_or_else(|| (&s, "0"));
435        let (fract, precision) = remain.split_once('~').unwrap_or((remain, ""));
436        let precision = if precision.is_empty() {
437            fract.len() as u64
438        } else {
439            precision
440                .parse()
441                .map_err(AmountParseError::InvalidPrecision)?
442        };
443        let int: u64 = int.parse().map_err(AmountParseError::InvalidInt)?;
444        let fract: u64 = fract.parse().map_err(AmountParseError::InvalidFract)?;
445        let precision = u8::try_from(precision)?;
446        Ok(CoinAmount {
447            int,
448            fract,
449            precision: Precision::try_from(precision)?,
450        })
451    }
452}
453
454#[cfg(test)]
455mod test {
456    use super::*;
457
458    #[test]
459    #[allow(clippy::inconsistent_digit_grouping)]
460    fn int_trailing_zeros() {
461        let amount = CoinAmount::new(10_000__43_608_195u64, Precision::default());
462        assert_eq!(amount.int(), 10_000);
463        assert_eq!(amount.fract(), 436_081_95);
464        assert_eq!(amount.precision(), Precision::default());
465        assert_eq!(format!("{amount}"), "10000.43608195~8");
466        assert_eq!(format!("{amount: >#}"), "10 000.43 608 195");
467    }
468
469    #[test]
470    #[allow(clippy::inconsistent_digit_grouping)]
471    fn sub_fraction() {
472        let amount = CoinAmount::new(10__00_008_195u64, Precision::default());
473        assert_eq!(amount.int(), 10);
474        assert_eq!(amount.fract(), 8195);
475        assert_eq!(amount.precision(), Precision::default());
476        assert_eq!(format!("{amount}"), "10.00008195~8");
477        assert_eq!(format!("{amount:#}"), "10.00 008 195");
478    }
479
480    #[test]
481    #[allow(clippy::inconsistent_digit_grouping)]
482    fn small_fraction() {
483        let amount = CoinAmount::new(10__00_000_500u64, Precision::default());
484        assert_eq!(amount.int(), 10);
485        assert_eq!(amount.fract(), 500);
486        assert_eq!(amount.precision(), Precision::default());
487        assert_eq!(format!("{amount}"), "10.000005~8");
488        assert_eq!(format!("{amount:_>#}"), "10.00_000_500");
489    }
490
491    #[test]
492    #[allow(clippy::inconsistent_digit_grouping)]
493    fn zero_fraction() {
494        let amount = CoinAmount::new(10__00_000_000u64, Precision::default());
495        assert_eq!(amount.int(), 10);
496        assert_eq!(amount.fract(), 0);
497        assert_eq!(amount.precision(), Precision::default());
498        assert_eq!(format!("{amount}"), "10~8");
499        assert_eq!(format!("{amount:_>#}"), "10.00_000_000");
500    }
501}