taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

amount.rs (16928B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2024, 2025, 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU Affero General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU Affero General Public License for more details.
     12 
     13   You should have received a copy of the GNU Affero General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 
     17 //! Type for the Taler Amount <https://docs.taler.net/core/api-common.html#tsref-type-Amount>
     18 
     19 use std::{
     20     fmt::{Debug, Display},
     21     num::ParseIntError,
     22     str::FromStr,
     23 };
     24 
     25 use compact_str::format_compact;
     26 
     27 use super::utils::InlineStr;
     28 use crate::signature::Buf;
     29 
     30 /** Number of characters we use to represent currency names */
     31 // We use the same value than the exchange -1 because we use a byte for the len instead of 0 termination
     32 pub const CURRENCY_LEN: usize = 11;
     33 
     34 /** Maximum legal value for an amount, based on IEEE double */
     35 pub const MAX_VALUE: u64 = 2 << 51;
     36 
     37 /** The number of digits in a fraction part of an amount */
     38 pub const FRAC_BASE_NB_DIGITS: u8 = 8;
     39 
     40 /** The fraction part of an amount represents which fraction of the value */
     41 pub const FRAC_BASE: u32 = 10u32.pow(FRAC_BASE_NB_DIGITS as u32);
     42 
     43 const CENT_FRACTION: u32 = 10u32.pow((FRAC_BASE_NB_DIGITS - 2) as u32);
     44 
     45 #[derive(
     46     Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     47 )]
     48 /// Inlined ISO 4217 currency string
     49 pub struct Currency(InlineStr<CURRENCY_LEN>);
     50 
     51 impl AsRef<str> for Currency {
     52     fn as_ref(&self) -> &str {
     53         self.0.as_ref()
     54     }
     55 }
     56 
     57 #[derive(Debug, thiserror::Error)]
     58 pub enum CurrencyErrorKind {
     59     #[error("contains illegal characters (only A-Z allowed)")]
     60     Invalid,
     61     #[error("too long (max {CURRENCY_LEN} chars)")]
     62     Big,
     63     #[error("is empty")]
     64     Empty,
     65 }
     66 
     67 #[derive(Debug, thiserror::Error)]
     68 #[error("currency code name '{currency}' {kind}")]
     69 pub struct ParseCurrencyError {
     70     currency: String,
     71     pub kind: CurrencyErrorKind,
     72 }
     73 
     74 impl Currency {
     75     pub const TEST: Self = Self::const_parse("TEST");
     76     pub const KUDOS: Self = Self::const_parse("KUDOS");
     77     pub const EUR: Self = Self::const_parse("EUR");
     78     pub const CHF: Self = Self::const_parse("CHF");
     79     pub const HUF: Self = Self::const_parse("HUF");
     80 
     81     pub const fn const_parse(s: &str) -> Currency {
     82         let bytes = s.as_bytes();
     83         let len = bytes.len();
     84 
     85         if bytes.is_empty() {
     86             panic!("empty")
     87         } else if len > CURRENCY_LEN {
     88             panic!("too big")
     89         }
     90         let mut i = 0;
     91         while i < bytes.len() {
     92             if !bytes[i].is_ascii_uppercase() {
     93                 panic!("invalid")
     94             }
     95             i += 1;
     96         }
     97         Self(InlineStr::copy_from_slice(bytes))
     98     }
     99 }
    100 
    101 impl FromStr for Currency {
    102     type Err = ParseCurrencyError;
    103 
    104     fn from_str(s: &str) -> Result<Self, Self::Err> {
    105         let bytes = s.as_bytes();
    106         let len = bytes.len();
    107         if bytes.is_empty() {
    108             Err(CurrencyErrorKind::Empty)
    109         } else if len > CURRENCY_LEN {
    110             Err(CurrencyErrorKind::Big)
    111         } else if !bytes.iter().all(|c| c.is_ascii_uppercase()) {
    112             Err(CurrencyErrorKind::Invalid)
    113         } else {
    114             Ok(Self(InlineStr::copy_from_slice(bytes)))
    115         }
    116         .map_err(|kind| ParseCurrencyError {
    117             currency: s.to_owned(),
    118             kind,
    119         })
    120     }
    121 }
    122 
    123 impl Debug for Currency {
    124     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    125         Debug::fmt(&self.as_ref(), f)
    126     }
    127 }
    128 
    129 impl Display for Currency {
    130     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    131         Display::fmt(&self.as_ref(), f)
    132     }
    133 }
    134 
    135 #[derive(sqlx::Type)]
    136 #[sqlx(type_name = "taler_amount")]
    137 struct PgTalerAmount {
    138     pub val: i64,
    139     pub frac: i32,
    140 }
    141 
    142 #[derive(
    143     Clone,
    144     Copy,
    145     PartialEq,
    146     Eq,
    147     PartialOrd,
    148     Ord,
    149     serde_with::DeserializeFromStr,
    150     serde_with::SerializeDisplay,
    151 )]
    152 pub struct Decimal {
    153     /** Integer part */
    154     pub val: u64,
    155     /** Factional part, multiple of FRAC_BASE */
    156     pub frac: u32,
    157 }
    158 
    159 impl Decimal {
    160     pub const fn new(val: u64, frac: u32) -> Self {
    161         Self { val, frac }
    162     }
    163 
    164     pub const ZERO: Self = Self::new(0, 0);
    165     pub const MAX: Self = Self::new(MAX_VALUE, FRAC_BASE - 1);
    166 
    167     const fn normalize(mut self) -> Option<Self> {
    168         let Some(val) = self.val.checked_add((self.frac / FRAC_BASE) as u64) else {
    169             return None;
    170         };
    171         self.val = val;
    172         self.frac %= FRAC_BASE;
    173         if self.val > MAX_VALUE {
    174             return None;
    175         }
    176         Some(self)
    177     }
    178 
    179     pub fn try_add(mut self, rhs: &Self) -> Option<Self> {
    180         self.val = self.val.checked_add(rhs.val)?;
    181         self.frac = self
    182             .frac
    183             .checked_add(rhs.frac)
    184             .expect("amount fraction overflow should never happen with normalized amounts");
    185         self.normalize()
    186     }
    187 
    188     pub fn try_sub(mut self, rhs: &Self) -> Option<Self> {
    189         if rhs.frac > self.frac {
    190             self.val = self.val.checked_sub(1)?;
    191             self.frac += FRAC_BASE;
    192         }
    193         self.val = self.val.checked_sub(rhs.val)?;
    194         self.frac = self.frac.checked_sub(rhs.frac)?;
    195         self.normalize()
    196     }
    197 
    198     pub const fn to_amount(self, currency: &Currency) -> Amount {
    199         Amount::new_decimal(currency, self)
    200     }
    201 }
    202 
    203 #[derive(Debug, thiserror::Error)]
    204 pub enum DecimalErrKind {
    205     #[error("value overflow (must be <= {MAX_VALUE})")]
    206     Overflow,
    207     #[error("invalid value ({0})")]
    208     InvalidValue(ParseIntError),
    209     #[error("invalid fraction ({0})")]
    210     InvalidFraction(ParseIntError),
    211     #[error("fraction overflow (max {FRAC_BASE_NB_DIGITS} digits)")]
    212     FractionOverflow,
    213 }
    214 
    215 #[derive(Debug, thiserror::Error)]
    216 #[error("decimal '{decimal}' {kind}")]
    217 pub struct ParseDecimalErr {
    218     decimal: String,
    219     pub kind: DecimalErrKind,
    220 }
    221 
    222 impl FromStr for Decimal {
    223     type Err = ParseDecimalErr;
    224 
    225     fn from_str(s: &str) -> Result<Self, Self::Err> {
    226         let (value, fraction) = s.split_once('.').unwrap_or((s, ""));
    227 
    228         // TODO use try block when stable
    229         (|| {
    230             let value: u64 = value.parse().map_err(DecimalErrKind::InvalidValue)?;
    231             if value > MAX_VALUE {
    232                 return Err(DecimalErrKind::Overflow);
    233             }
    234 
    235             if fraction.len() > FRAC_BASE_NB_DIGITS as usize {
    236                 return Err(DecimalErrKind::FractionOverflow);
    237             }
    238             let fraction: u32 = if fraction.is_empty() {
    239                 0
    240             } else {
    241                 fraction
    242                     .parse::<u32>()
    243                     .map_err(DecimalErrKind::InvalidFraction)?
    244                     * 10u32.pow(FRAC_BASE_NB_DIGITS as u32 - fraction.len() as u32)
    245             };
    246             Ok(Self {
    247                 val: value,
    248                 frac: fraction,
    249             })
    250         })()
    251         .map_err(|kind| ParseDecimalErr {
    252             decimal: s.to_owned(),
    253             kind,
    254         })
    255     }
    256 }
    257 
    258 impl Display for Decimal {
    259     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    260         if self.frac == 0 {
    261             f.write_fmt(format_args!("{}", self.val))
    262         } else {
    263             let num = format_compact!("{:08}", self.frac);
    264             f.write_fmt(format_args!("{}.{}", self.val, num.trim_end_matches('0')))
    265         }
    266     }
    267 }
    268 
    269 impl Debug for Decimal {
    270     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    271         Display::fmt(&self, f)
    272     }
    273 }
    274 
    275 impl sqlx::Type<sqlx::Postgres> for Decimal {
    276     fn type_info() -> sqlx::postgres::PgTypeInfo {
    277         PgTalerAmount::type_info()
    278     }
    279 }
    280 
    281 impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Decimal {
    282     fn encode_by_ref(
    283         &self,
    284         buf: &mut sqlx::postgres::PgArgumentBuffer,
    285     ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
    286         PgTalerAmount {
    287             val: self.val as i64,
    288             frac: self.frac as i32,
    289         }
    290         .encode_by_ref(buf)
    291     }
    292 }
    293 
    294 impl<'r> sqlx::Decode<'r, sqlx::Postgres> for Decimal {
    295     fn decode(value: sqlx::postgres::PgValueRef<'r>) -> Result<Self, sqlx::error::BoxDynError> {
    296         let pg = PgTalerAmount::decode(value)?;
    297         Ok(Self {
    298             val: pg.val as u64,
    299             frac: pg.frac as u32,
    300         })
    301     }
    302 }
    303 
    304 #[track_caller]
    305 pub fn decimal(decimal: impl AsRef<str>) -> Decimal {
    306     decimal.as_ref().parse().expect("Invalid decimal constant")
    307 }
    308 
    309 /// <https://docs.taler.net/core/api-common.html#tsref-type-Amount>
    310 #[derive(
    311     Clone, Copy, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
    312 )]
    313 pub struct Amount {
    314     pub currency: Currency,
    315     pub val: u64,
    316     pub frac: u32,
    317 }
    318 
    319 impl Amount {
    320     pub const fn new_decimal(currency: &Currency, decimal: Decimal) -> Self {
    321         Self {
    322             currency: *currency,
    323             val: decimal.val,
    324             frac: decimal.frac,
    325         }
    326     }
    327 
    328     pub const fn new(currency: &Currency, val: u64, frac: u32) -> Self {
    329         Self::new_decimal(currency, Decimal { val, frac })
    330     }
    331 
    332     pub const fn max(currency: &Currency) -> Self {
    333         Self::new_decimal(currency, Decimal::MAX)
    334     }
    335 
    336     pub const fn zero(currency: &Currency) -> Self {
    337         Self::new_decimal(currency, Decimal::ZERO)
    338     }
    339 
    340     pub fn is_zero(&self) -> bool {
    341         self.decimal() == Decimal::ZERO
    342     }
    343 
    344     /* Check is amount has fractional amount < 0.01 */
    345     pub const fn is_sub_cent(&self) -> bool {
    346         !self.frac.is_multiple_of(CENT_FRACTION)
    347     }
    348 
    349     pub const fn decimal(&self) -> Decimal {
    350         Decimal {
    351             val: self.val,
    352             frac: self.frac,
    353         }
    354     }
    355 
    356     pub fn normalize(self) -> Option<Self> {
    357         let decimal = self.decimal().normalize()?;
    358         Some((self.currency, decimal).into())
    359     }
    360 
    361     pub fn try_add(self, rhs: &Self) -> Option<Self> {
    362         if self.currency != rhs.currency {
    363             return None;
    364         }
    365         let decimal = self.decimal().try_add(&rhs.decimal())?;
    366         Some((self.currency, decimal).into())
    367     }
    368 
    369     pub fn try_sub(self, rhs: &Self) -> Option<Self> {
    370         if self.currency != rhs.currency {
    371             return None;
    372         }
    373         let decimal = self.decimal().try_sub(&rhs.decimal())?;
    374         Some((self.currency, decimal).into())
    375     }
    376 
    377     /** Encode amount for signature */
    378     pub fn signature_bytes(self) -> [u8; 24] {
    379         Buf::new()
    380             .put_u64(self.val)
    381             .put_u32(self.frac)
    382             .put(&self.currency.0)
    383             .finish()
    384     }
    385 }
    386 
    387 impl From<(Currency, Decimal)> for Amount {
    388     fn from((currency, decimal): (Currency, Decimal)) -> Self {
    389         Self::new_decimal(&currency, decimal)
    390     }
    391 }
    392 
    393 #[track_caller]
    394 pub fn amount(amount: impl AsRef<str>) -> Amount {
    395     amount.as_ref().parse().expect("Invalid amount constant")
    396 }
    397 
    398 #[derive(Debug, thiserror::Error)]
    399 pub enum AmountErrKind {
    400     #[error("invalid format")]
    401     Format,
    402     #[error("currency {0}")]
    403     Currency(#[from] CurrencyErrorKind),
    404     #[error(transparent)]
    405     Decimal(#[from] DecimalErrKind),
    406 }
    407 
    408 #[derive(Debug, thiserror::Error)]
    409 #[error("amount '{amount}' {kind}")]
    410 pub struct ParseAmountErr {
    411     amount: String,
    412     pub kind: AmountErrKind,
    413 }
    414 
    415 impl FromStr for Amount {
    416     type Err = ParseAmountErr;
    417 
    418     fn from_str(s: &str) -> Result<Self, Self::Err> {
    419         // TODO use try block when stable
    420         (|| {
    421             let (currency, amount) = s.trim().split_once(':').ok_or(AmountErrKind::Format)?;
    422             let currency = currency.parse().map_err(|e: ParseCurrencyError| e.kind)?;
    423             let decimal = amount.parse().map_err(|e: ParseDecimalErr| e.kind)?;
    424             Ok((currency, decimal).into())
    425         })()
    426         .map_err(|kind| ParseAmountErr {
    427             amount: s.to_owned(),
    428             kind,
    429         })
    430     }
    431 }
    432 
    433 impl Display for Amount {
    434     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    435         f.write_fmt(format_args!("{}:{}", self.currency, self.decimal()))
    436     }
    437 }
    438 
    439 impl Debug for Amount {
    440     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    441         Display::fmt(&self, f)
    442     }
    443 }
    444 
    445 impl sqlx::Type<sqlx::Postgres> for Amount {
    446     fn type_info() -> sqlx::postgres::PgTypeInfo {
    447         PgTalerAmount::type_info()
    448     }
    449 }
    450 
    451 impl<'q> sqlx::Encode<'q, sqlx::Postgres> for Amount {
    452     fn encode_by_ref(
    453         &self,
    454         buf: &mut sqlx::postgres::PgArgumentBuffer,
    455     ) -> Result<sqlx::encode::IsNull, sqlx::error::BoxDynError> {
    456         self.decimal().encode_by_ref(buf)
    457     }
    458 }
    459 
    460 #[test]
    461 fn constants() {
    462     assert_eq!(format!("{}", Amount::zero(&Currency::KUDOS)), "KUDOS:0");
    463     assert_eq!(
    464         format!("{}", Amount::max(&Currency::KUDOS)),
    465         "KUDOS:4503599627370496.99999999"
    466     );
    467 }
    468 
    469 #[test]
    470 fn test_amount_parse() {
    471     const TALER_AMOUNT_FRAC_BASE: u32 = 100000000;
    472     // https://git.taler.net/exchange.git/tree/src/util/test_amount.c
    473 
    474     const INVALID_AMOUNTS: [&str; 6] = [
    475         "EUR:4a",                                                                     // non-numeric,
    476         "EUR:4.4a",                                                                   // non-numeric
    477         "EUR:4.a4",                                                                   // non-numeric
    478         ":4.a4",                                                                      // no currency
    479         "EUR:4.123456789", // precision to high
    480         "EUR:1234567890123456789012345678901234567890123456789012345678901234567890", // value to big
    481     ];
    482 
    483     for str in INVALID_AMOUNTS {
    484         let amount = Amount::from_str(str);
    485         assert!(amount.is_err(), "invalid {} got {:?}", str, &amount);
    486     }
    487 
    488     let eur: Currency = Currency::EUR;
    489     let local: Currency = Currency::CHF;
    490     let valid_amounts: Vec<(&str, &str, Amount)> = vec![
    491         ("EUR:4", "EUR:4", Amount::new(&eur, 4, 0)), // without fraction
    492         (
    493             "EUR:0.02",
    494             "EUR:0.02",
    495             Amount::new(&eur, 0, TALER_AMOUNT_FRAC_BASE / 100 * 2),
    496         ), // leading zero fraction
    497         (
    498             " EUR:4.12",
    499             "EUR:4.12",
    500             Amount::new(&eur, 4, TALER_AMOUNT_FRAC_BASE / 100 * 12),
    501         ), // leading space and fraction
    502         (
    503             " CHF:4444.1000",
    504             "CHF:4444.1",
    505             Amount::new(&local, 4444, TALER_AMOUNT_FRAC_BASE / 10),
    506         ), // local currency
    507     ];
    508     for (raw, expected, goal) in valid_amounts {
    509         let amount = Amount::from_str(raw);
    510         assert!(amount.is_ok(), "Valid {} got {:?}", raw, amount);
    511         assert_eq!(
    512             *amount.as_ref().unwrap(),
    513             goal,
    514             "Expected {:?} got {:?} for {}",
    515             goal,
    516             amount,
    517             raw
    518         );
    519         let amount = amount.unwrap();
    520         let str = amount.to_string();
    521         assert_eq!(str, expected);
    522         assert_eq!(amount, Amount::from_str(&str).unwrap(), "{str}");
    523     }
    524 }
    525 
    526 #[test]
    527 fn test_amount_add() {
    528     let eur: Currency = Currency::EUR;
    529     assert_eq!(
    530         Amount::max(&eur).try_add(&Amount::zero(&eur)),
    531         Some(Amount::max(&eur))
    532     );
    533     assert_eq!(
    534         Amount::zero(&eur).try_add(&Amount::zero(&eur)),
    535         Some(Amount::zero(&eur))
    536     );
    537     assert_eq!(
    538         amount("EUR:6.41").try_add(&amount("EUR:4.69")),
    539         Some(amount("EUR:11.1"))
    540     );
    541     assert_eq!(
    542         amount(format!("EUR:{MAX_VALUE}")).try_add(&amount("EUR:0.99999999")),
    543         Some(Amount::max(&eur))
    544     );
    545 
    546     assert_eq!(
    547         amount(format!("EUR:{}", MAX_VALUE - 5)).try_add(&amount("EUR:6")),
    548         None
    549     );
    550     assert_eq!(
    551         Amount::new(&eur, u64::MAX, 0).try_add(&amount("EUR:1")),
    552         None
    553     );
    554     assert_eq!(
    555         amount(format!("EUR:{}.{}", MAX_VALUE - 5, FRAC_BASE - 1))
    556             .try_add(&amount("EUR:5.00000002")),
    557         None
    558     );
    559 }
    560 
    561 #[test]
    562 fn test_amount_normalize() {
    563     let eur: Currency = "EUR".parse().unwrap();
    564     assert_eq!(
    565         Amount::new(&eur, 4, 2 * FRAC_BASE).normalize(),
    566         Some(amount("EUR:6"))
    567     );
    568     assert_eq!(
    569         Amount::new(&eur, 4, 2 * FRAC_BASE + 1).normalize(),
    570         Some(amount("EUR:6.00000001"))
    571     );
    572     assert_eq!(
    573         Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1).normalize(),
    574         Some(Amount::new(&eur, MAX_VALUE, FRAC_BASE - 1))
    575     );
    576     assert_eq!(Amount::new(&eur, u64::MAX, FRAC_BASE).normalize(), None);
    577     assert_eq!(Amount::new(&eur, MAX_VALUE, FRAC_BASE).normalize(), None);
    578 
    579     for amount in [Amount::max(&eur), Amount::zero(&eur)] {
    580         assert_eq!(amount.normalize(), Some(amount))
    581     }
    582 }