taler-rust

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

lib.rs (7040B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 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 use std::{borrow::Cow, str::FromStr, sync::Arc};
     18 
     19 use sqlx::PgPool;
     20 use taler_api::api::{Router, TalerRouter as _};
     21 use taler_common::{
     22     config::Config,
     23     types::{
     24         iban::{Country, IBAN, IbanErrKind, ParseIbanErr},
     25         payto::{FullPayto, IbanPayto, Payto, PaytoErr, PaytoImpl, PaytoURI, TransferPayto},
     26     },
     27 };
     28 
     29 use crate::{api::MagnetApi, config::ServeCfg};
     30 
     31 pub mod api;
     32 pub mod config;
     33 pub mod constants;
     34 pub mod db;
     35 pub mod dev;
     36 pub mod magnet_api;
     37 pub mod setup;
     38 pub mod worker;
     39 
     40 pub async fn run_serve(cfg: &Config, pool: PgPool) -> anyhow::Result<()> {
     41     let cfg = ServeCfg::parse(cfg)?;
     42     let api = Arc::new(MagnetApi::start(pool, cfg.payto).await);
     43     let mut router = Router::new();
     44     if let Some(cfg) = cfg.wire_gateway {
     45         router = router.wire_gateway(api.clone(), cfg.auth.method());
     46     }
     47     if let Some(cfg) = cfg.revenue {
     48         router = router.revenue(api.clone(), cfg.auth.method());
     49     }
     50     if let Some(cfg) = cfg.observability {
     51         router = router.observability(api, cfg.auth.method());
     52     }
     53     router.serve(&cfg.serve, None).await?;
     54     Ok(())
     55 }
     56 
     57 #[derive(
     58     Debug, Clone, PartialEq, Eq, serde_with::DeserializeFromStr, serde_with::SerializeDisplay,
     59 )]
     60 pub struct HuIban(IBAN);
     61 
     62 impl HuIban {
     63     #[allow(clippy::identity_op)]
     64     pub fn checksum(b: &[u8]) -> Result<(), (u8, u8)> {
     65         let expected_digit = b[7] - b'0';
     66         let sum = ((b[0] - b'0') * 9) as u16
     67             + ((b[1] - b'0') * 7) as u16
     68             + ((b[2] - b'0') * 3) as u16
     69             + ((b[3] - b'0') * 1) as u16
     70             + ((b[4] - b'0') * 9) as u16
     71             + ((b[5] - b'0') * 7) as u16
     72             + ((b[6] - b'0') * 3) as u16;
     73         let modulo = ((10 - (sum % 10)) % 10) as u8;
     74         if expected_digit != modulo {
     75             Err((expected_digit, modulo))
     76         } else {
     77             Ok(())
     78         }
     79     }
     80 
     81     fn check_bban(bban: &str) -> Result<(), HuIbanErr> {
     82         let bban = bban.as_bytes();
     83         if bban.len() != 16 && bban.len() != 24 {
     84             return Err(HuIbanErr::BbanSize(bban.len()));
     85         } else if !bban.iter().all(u8::is_ascii_digit) {
     86             return Err(HuIbanErr::Invalid);
     87         }
     88         Self::checksum(&bban[..8]).map_err(|e| HuIbanErr::checksum("bank-branch number", e))?;
     89         if bban.len() == 16 {
     90             Self::checksum(&bban[8..]).map_err(|e| HuIbanErr::checksum("account number", e))?;
     91         } else {
     92             Self::checksum(&bban[8..16])
     93                 .map_err(|e| HuIbanErr::checksum("account number first group", e))?;
     94             Self::checksum(&bban[16..])
     95                 .map_err(|e| HuIbanErr::checksum("account number second group", e))?;
     96         }
     97         Ok(())
     98     }
     99 
    100     pub fn from_bban(bban: &str) -> Result<Self, HuIbanErr> {
    101         Self::check_bban(bban)?;
    102         let full_bban = if bban.len() == 16 {
    103             Cow::Owned(format!("{bban}00000000"))
    104         } else {
    105             Cow::Borrowed(bban)
    106         };
    107         let iban = IBAN::from_parts(Country::HU, &full_bban);
    108         Ok(Self(iban))
    109     }
    110 
    111     pub fn bban(&self) -> &str {
    112         let bban = self.0.bban();
    113         bban.strip_suffix("00000000").unwrap_or(bban)
    114     }
    115 
    116     pub fn iban(&self) -> &str {
    117         self.0.as_ref()
    118     }
    119 }
    120 
    121 #[derive(Debug, thiserror::Error)]
    122 pub enum HuIbanErr {
    123     #[error("contains illegal characters (only 0-9 allowed)")]
    124     Invalid,
    125     #[error("expected an hungarian IBAN starting with HU got {0}")]
    126     Country(Country),
    127     #[error("invalid length expected 16 or 24 chars got {0}")]
    128     BbanSize(usize),
    129     #[error("invalid checksum for {0} expected {1} got {2}")]
    130     Checksum(&'static str, u8, u8),
    131     #[error(transparent)]
    132     Iban(IbanErrKind),
    133 }
    134 
    135 impl From<ParseIbanErr> for HuIbanErr {
    136     fn from(value: ParseIbanErr) -> Self {
    137         Self::Iban(value.kind)
    138     }
    139 }
    140 
    141 impl HuIbanErr {
    142     const fn checksum(part: &'static str, (expected, checksum): (u8, u8)) -> Self {
    143         Self::Checksum(part, expected, checksum)
    144     }
    145 }
    146 
    147 impl TryFrom<IBAN> for HuIban {
    148     type Error = HuIbanErr;
    149 
    150     fn try_from(iban: IBAN) -> Result<Self, Self::Error> {
    151         let country = iban.country();
    152         if country != Country::HU {
    153             return Err(HuIbanErr::Country(country));
    154         }
    155 
    156         Self::check_bban(iban.bban())?;
    157 
    158         Ok(Self(iban))
    159     }
    160 }
    161 
    162 impl PaytoImpl for HuIban {
    163     fn as_uri(&self) -> PaytoURI {
    164         PaytoURI::from_parts("iban", format_args!("/{}", self.0))
    165     }
    166 
    167     fn parse(raw: &PaytoURI) -> Result<Self, PaytoErr> {
    168         let iban_payto = IbanPayto::try_from(raw)?;
    169         Self::try_from(iban_payto.into_inner().iban)
    170             .map_err(|e| PaytoErr::malformed_segment("iban", e))
    171     }
    172 }
    173 
    174 impl FromStr for HuIban {
    175     type Err = HuIbanErr;
    176 
    177     fn from_str(s: &str) -> Result<Self, Self::Err> {
    178         let iban: IBAN = s.parse()?;
    179         Self::try_from(iban)
    180     }
    181 }
    182 
    183 impl std::fmt::Display for HuIban {
    184     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    185         self.0.fmt(f)
    186     }
    187 }
    188 
    189 /// Parse a magnet payto URI, panic if malformed
    190 pub fn magnet_payto(url: impl AsRef<str>) -> FullHuPayto {
    191     url.as_ref().parse().expect("invalid magnet payto")
    192 }
    193 
    194 pub type HuPayto = Payto<HuIban>;
    195 pub type FullHuPayto = FullPayto<HuIban>;
    196 pub type TransferHuPayto = TransferPayto<HuIban>;
    197 
    198 #[cfg(test)]
    199 mod test {
    200     use taler_common::types::{
    201         iban::IBAN,
    202         payto::{Payto, PaytoImpl, payto},
    203     };
    204 
    205     use crate::HuIban;
    206 
    207     #[test]
    208     fn hu_iban() {
    209         for (valid, account) in [
    210             (
    211                 payto("payto://iban/HU30162000031000163100000000"),
    212                 "1620000310001631",
    213             ),
    214             (
    215                 payto("payto://iban/HU02162000031000164800000000"),
    216                 "1620000310001648",
    217             ),
    218             (
    219                 payto("payto://iban/HU60162000101006446300000000"),
    220                 "1620001010064463",
    221             ),
    222         ] {
    223             // Parsing
    224             let iban_payto: Payto<IBAN> = (&valid).try_into().unwrap();
    225             let hu_payto: HuIban = iban_payto.into_inner().try_into().unwrap();
    226             assert_eq!(hu_payto.bban(), account);
    227             // Roundtrip
    228             let iban = HuIban::from_bban(account).unwrap();
    229             let payto = iban.as_uri();
    230             assert_eq!(payto, valid);
    231         }
    232     }
    233 }