taler-rust

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

config.rs (7253B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 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::{str::FromStr as _, sync::LazyLock, time::Duration};
     18 
     19 use aws_lc_rs::signature::{ParsedPublicKey, RSA_PKCS1_2048_8192_SHA256};
     20 use compact_str::CompactString;
     21 use taler_api::{
     22     Serve,
     23     config::{ApiCfg, DbCfg},
     24 };
     25 use taler_common::{
     26     config::{CfgErr, Config},
     27     config_bail, map_config,
     28     types::{
     29         amount::Currency,
     30         payto::{ACH, BankID},
     31     },
     32 };
     33 use taler_macros::EnumMeta;
     34 use url::Url;
     35 
     36 use crate::payto::WiseAccount;
     37 
     38 #[derive(Debug, Clone, Copy)]
     39 pub enum AccountType {
     40     Exchange,
     41     Normal,
     42 }
     43 
     44 #[derive(Debug, Clone, Copy, EnumMeta)]
     45 #[allow(non_camel_case_types)]
     46 #[enum_meta(Str)]
     47 pub enum ApiEnv {
     48     production,
     49     sandbox,
     50 }
     51 
     52 impl ApiEnv {
     53     pub const fn api(self) -> &'static LazyLock<Url> {
     54         static PROD: LazyLock<Url> =
     55             LazyLock::new(|| Url::from_str("https://api.wise.com").unwrap());
     56         static SANDBOX: LazyLock<Url> =
     57             LazyLock::new(|| Url::from_str("https://api.wise-sandbox.com").unwrap());
     58         match self {
     59             ApiEnv::production => &PROD,
     60             ApiEnv::sandbox => &SANDBOX,
     61         }
     62     }
     63 
     64     pub fn public_key(self) -> ParsedPublicKey {
     65         let pem = match self {
     66             ApiEnv::production => {
     67                 "-----BEGIN PUBLIC KEY-----
     68 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvO8vXV+JksBzZAY6GhSO
     69 XdoTCfhXaaiZ+qAbtaDBiu2AGkGVpmEygFmWP4Li9m5+Ni85BhVvZOodM9epgW3F
     70 bA5Q1SexvAF1PPjX4JpMstak/QhAgl1qMSqEevL8cmUeTgcMuVWCJmlge9h7B1CS
     71 D4rtlimGZozG39rUBDg6Qt2K+P4wBfLblL0k4C4YUdLnpGYEDIth+i8XsRpFlogx
     72 CAFyH9+knYsDbR43UJ9shtc42Ybd40Afihj8KnYKXzchyQ42aC8aZ/h5hyZ28yVy
     73 Oj3Vos0VdBIs/gAyJ/4yyQFCXYte64I7ssrlbGRaco4nKF3HmaNhxwyKyJafz19e
     74 HwIDAQAB
     75 -----END PUBLIC KEY-----"
     76             }
     77             ApiEnv::sandbox => {
     78                 "-----BEGIN PUBLIC KEY-----
     79 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwpb91cEYuyJNQepZAVfP
     80 ZIlPZfNUefH+n6w9SW3fykqKu938cR7WadQv87oF2VuT+fDt7kqeRziTmPSUhqPU
     81 ys/V2Q1rlfJuXbE+Gga37t7zwd0egQ+KyOEHQOpcTwKmtZ81ieGHynAQzsn1We3j
     82 wt760MsCPJ7GMT141ByQM+yW1Bx+4SG3IGjXWyqOWrcXsxAvIXkpUD/jK/L958Cg
     83 nZEgz0BSEh0QxYLITnW1lLokSx/dTianWPFEhMC9BgijempgNXHNfcVirg1lPSyg
     84 z7KqoKUN0oHqWLr2U1A+7kqrl6O2nx3CKs1bj1hToT1+p4kcMoHXA7kA+VBLUpEs
     85 VwIDAQAB
     86 -----END PUBLIC KEY-----"
     87             }
     88         };
     89         let parsed = pem::parse(pem).unwrap();
     90         ParsedPublicKey::new(&RSA_PKCS1_2048_8192_SHA256, parsed.contents()).unwrap()
     91     }
     92 }
     93 
     94 pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> {
     95     DbCfg::parse(cfg.section("wisedb-postgres"))
     96 }
     97 
     98 pub struct WiseBalance {
     99     pub id: u32,
    100     pub currency: Currency,
    101     pub payto: WiseAccount,
    102 }
    103 
    104 fn balances(cfg: &Config) -> Result<Vec<WiseBalance>, CfgErr> {
    105     let mut balances = Vec::new();
    106     let mut ids = std::collections::BTreeSet::new();
    107     for s in cfg
    108         .sections()
    109         .filter(|s| s.name.starts_with("wise-balance-"))
    110     {
    111         let id = s.number("ID").require()?;
    112         if !ids.insert(id) {
    113             return Err(s.invalid("ID", format!("duplicate balance ID {id}")));
    114         }
    115         balances.push(WiseBalance {
    116             id,
    117             currency: s.currency("CURRENCY").require()?,
    118             payto: map_config!(s, "payto type", "PAYTO_TYPE",
    119                 "iban" => {
    120                     WiseAccount::IBAN(BankID {
    121                         iban: s.parse("IBAN", "iban").require()?,
    122                         bic: s.parse("BIC", "bic").opt()?
    123                     })
    124                 },
    125                 "ach" => {
    126                     WiseAccount::ACH(ACH {
    127                         routing_number: s.parse("ACH routing number", "routing_number").require()?,
    128                         account_number: s.parse("ACH account_number", "account_number").require()?
    129                     })
    130                 },
    131             )
    132             .require()?,
    133         });
    134     }
    135     Ok(balances)
    136 }
    137 
    138 /// taler-wise setup config
    139 pub struct SetupCfg {
    140     pub api: ApiEnv,
    141     pub token: String,
    142     pub profile_id: Option<u64>,
    143     pub name: Option<CompactString>,
    144     pub balances: Vec<WiseBalance>,
    145 }
    146 
    147 impl SetupCfg {
    148     pub fn parse(cfg: &Config) -> Result<Self, CfgErr> {
    149         let ms = cfg.section("wise");
    150         let s = cfg.section("wise-worker");
    151         Ok(Self {
    152             api: ms.parse("env", "API_ENV").require()?,
    153             profile_id: ms.number("PROFILE_ID").opt()?,
    154             token: s.str("TOKEN").require()?,
    155             name: ms.cstr("NAME").opt()?,
    156             balances: balances(cfg)?,
    157         })
    158     }
    159 }
    160 
    161 /// taler-wise httpd config
    162 pub struct ServeCfg {
    163     pub api: ApiEnv,
    164     pub profile_id: u64,
    165     pub name: CompactString,
    166     pub serve: Serve,
    167     pub balances: Vec<WiseBalance>,
    168     pub wire_gateway: Option<ApiCfg>,
    169     pub revenue: Option<ApiCfg>,
    170     pub observability: Option<ApiCfg>,
    171 }
    172 
    173 impl ServeCfg {
    174     pub fn parse(cfg: &Config) -> Result<Self, CfgErr> {
    175         let s = cfg.section("wise");
    176         Ok(Self {
    177             api: s.parse("env", "API_ENV").require()?,
    178             profile_id: s.number("PROFILE_ID").require()?,
    179             name: s.cstr("NAME").require()?,
    180             serve: Serve::parse(&cfg.section("wise-httpd"))?,
    181             wire_gateway: ApiCfg::parse(cfg.section("wise-httpd-wire-gateway-api"))?,
    182             revenue: ApiCfg::parse(cfg.section("wise-httpd-revenue-api"))?,
    183             observability: ApiCfg::parse(cfg.section("wise-httpd-observability-api"))?,
    184             balances: balances(cfg)?,
    185         })
    186     }
    187 }
    188 
    189 /// taler-wise worker config
    190 pub struct WorkerCfg {
    191     pub api: ApiEnv,
    192     pub profile_id: u64,
    193     pub token: String,
    194     pub frequency: Duration,
    195     pub balances: Vec<WiseBalance>,
    196 }
    197 
    198 impl WorkerCfg {
    199     pub fn parse(cfg: &Config) -> Result<Self, CfgErr> {
    200         let ms = cfg.section("wise");
    201         let s = cfg.section("wise-worker");
    202         let cfg = Self {
    203             api: ms.parse("env", "API_ENV").require()?,
    204             profile_id: ms.number("PROFILE_ID").require()?,
    205             token: s.str("TOKEN").require()?,
    206             frequency: s.duration("FREQUENCY").require()?,
    207             balances: balances(cfg)?,
    208         };
    209         if cfg.balances.is_empty() {
    210             config_bail!("Missing configured balance in a section [wise-balance-*]");
    211         }
    212         if cfg.frequency.is_zero() {
    213             return Err(s.invalid("FREQUENCY", "must be positive"));
    214         }
    215         if cfg.token.is_empty()
    216             || hyper::header::HeaderValue::from_str(&format!("Bearer {}", cfg.token)).is_err()
    217         {
    218             return Err(s.invalid("TOKEN", "must be a nonempty valid HTTP header value"));
    219         }
    220         Ok(cfg)
    221     }
    222 }