taler-rust

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

lib.rs (7237B)


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