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 }