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 }