taler-rust

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

amount.rs (17079B)


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