commit b73126f1a2ff05f433a8b2558195043fa4c5e580
parent 9ffc007031be5a1935de958b79303938d5bf8128
Author: Antoine A <>
Date: Fri, 25 Sep 2026 16:27:57 +0200
wise: add api env
Diffstat:
8 files changed, 87 insertions(+), 12 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -1654,6 +1654,16 @@ dependencies = [
]
[[package]]
+name = "pem"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d354a98a3d1251555de99e8fdd8afda05573c31b82f59063a7b0a29b5527f120"
+dependencies = [
+ "base64 0.23.1",
+ "serde_core",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2711,6 +2721,7 @@ dependencies = [
"hyper",
"jiff",
"owo-colors",
+ "pem",
"prometheus-client",
"regex",
"serde",
diff --git a/adapters/taler-wise/Cargo.toml b/adapters/taler-wise/Cargo.toml
@@ -9,6 +9,7 @@ homepage.workspace = true
repository.workspace = true
[dependencies]
+pem = "4"
clap.workspace = true
compact_str.workspace = true
jiff.workspace = true
diff --git a/adapters/taler-wise/src/config.rs b/adapters/taler-wise/src/config.rs
@@ -14,8 +14,9 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::time::Duration;
+use std::{str::FromStr as _, sync::LazyLock, time::Duration};
+use aws_lc_rs::rsa::PublicKey;
use compact_str::CompactString;
use taler_api::{
Serve,
@@ -29,6 +30,8 @@ use taler_common::{
payto::{ACH, BankID},
},
};
+use taler_macros::EnumMeta;
+use url::Url;
use crate::payto::WiseAccount;
@@ -38,6 +41,56 @@ pub enum AccountType {
Normal,
}
+#[derive(Debug, Clone, Copy, EnumMeta)]
+#[allow(non_camel_case_types)]
+#[enum_meta(Str)]
+pub enum ApiEnv {
+ production,
+ sandbox,
+}
+
+impl ApiEnv {
+ pub const fn api(self) -> &'static LazyLock<Url> {
+ static PROD: LazyLock<Url> =
+ LazyLock::new(|| Url::from_str("https://api.wise.com").unwrap());
+ static SANDBOX: LazyLock<Url> =
+ LazyLock::new(|| Url::from_str("https://api.wise-sandbox.com").unwrap());
+ match self {
+ ApiEnv::production => &PROD,
+ ApiEnv::sandbox => &SANDBOX,
+ }
+ }
+
+ pub fn public_key(self) -> PublicKey {
+ let pem = match self {
+ ApiEnv::production => {
+ "-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvO8vXV+JksBzZAY6GhSO
+XdoTCfhXaaiZ+qAbtaDBiu2AGkGVpmEygFmWP4Li9m5+Ni85BhVvZOodM9epgW3F
+bA5Q1SexvAF1PPjX4JpMstak/QhAgl1qMSqEevL8cmUeTgcMuVWCJmlge9h7B1CS
+D4rtlimGZozG39rUBDg6Qt2K+P4wBfLblL0k4C4YUdLnpGYEDIth+i8XsRpFlogx
+CAFyH9+knYsDbR43UJ9shtc42Ybd40Afihj8KnYKXzchyQ42aC8aZ/h5hyZ28yVy
+Oj3Vos0VdBIs/gAyJ/4yyQFCXYte64I7ssrlbGRaco4nKF3HmaNhxwyKyJafz19e
+HwIDAQAB
+-----END PUBLIC KEY-----"
+ }
+ ApiEnv::sandbox => {
+ "-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwpb91cEYuyJNQepZAVfP
+ZIlPZfNUefH+n6w9SW3fykqKu938cR7WadQv87oF2VuT+fDt7kqeRziTmPSUhqPU
+ys/V2Q1rlfJuXbE+Gga37t7zwd0egQ+KyOEHQOpcTwKmtZ81ieGHynAQzsn1We3j
+wt760MsCPJ7GMT141ByQM+yW1Bx+4SG3IGjXWyqOWrcXsxAvIXkpUD/jK/L958Cg
+nZEgz0BSEh0QxYLITnW1lLokSx/dTianWPFEhMC9BgijempgNXHNfcVirg1lPSyg
+z7KqoKUN0oHqWLr2U1A+7kqrl6O2nx3CKs1bj1hToT1+p4kcMoHXA7kA+VBLUpEs
+VwIDAQAB
+-----END PUBLIC KEY-----"
+ }
+ };
+ let parsed = pem::parse(pem).unwrap();
+ aws_lc_rs::rsa::PublicKey::from_der(parsed.contents()).unwrap()
+ }
+}
+
pub fn parse_db_cfg(cfg: &Config) -> Result<DbCfg, CfgErr> {
DbCfg::parse(cfg.section("wisedb-postgres"))
}
@@ -83,6 +136,7 @@ fn balances(cfg: &Config) -> Result<Vec<WiseBalance>, CfgErr> {
/// taler-wise setup config
pub struct SetupCfg {
+ pub api: ApiEnv,
pub token: String,
pub profile_id: Option<u64>,
pub name: Option<CompactString>,
@@ -91,11 +145,13 @@ pub struct SetupCfg {
impl SetupCfg {
pub fn parse(cfg: &Config) -> Result<Self, CfgErr> {
+ let ms = cfg.section("wise");
let s = cfg.section("wise-worker");
Ok(Self {
+ api: ms.parse("env", "API_ENV").require()?,
profile_id: s.number("PROFILE_ID").opt()?,
token: s.str("TOKEN").require()?,
- name: cfg.section("wise").cstr("NAME").opt()?,
+ name: ms.cstr("NAME").opt()?,
balances: balances(cfg)?,
})
}
@@ -127,6 +183,7 @@ impl ServeCfg {
/// taler-wise worker config
pub struct WorkerCfg {
+ pub api: ApiEnv,
pub profile_id: u64,
pub token: String,
pub frequency: Duration,
@@ -137,6 +194,7 @@ impl WorkerCfg {
pub fn parse(cfg: &Config) -> Result<Self, CfgErr> {
let s = cfg.section("wise-worker");
let cfg = Self {
+ api: cfg.section("wise").parse("env", "API_ENV").require()?,
profile_id: s.number("PROFILE_ID").require()?,
token: s.str("TOKEN").require()?,
frequency: s.duration("FREQUENCY").require()?,
diff --git a/adapters/taler-wise/src/setup.rs b/adapters/taler-wise/src/setup.rs
@@ -24,7 +24,7 @@ use crate::{
pub async fn setup(cfg: &Config, client: &http_client::Client) -> anyhow::Result<()> {
let cfg = SetupCfg::parse(cfg)?;
- let client = Client::new(client, &cfg.token);
+ let client = Client::new(client, cfg.api, &cfg.token);
info!(target: "setup", "Check API access and configuration");
let mut ready = true;
diff --git a/adapters/taler-wise/src/wise_api/client.rs b/adapters/taler-wise/src/wise_api/client.rs
@@ -14,7 +14,7 @@
TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-use std::{borrow::Cow, str::FromStr as _, sync::LazyLock};
+use std::borrow::Cow;
use http_client::{ApiErr, ClientErr, builder::Req, headers::HeaderParser as _};
use hyper::{
@@ -25,9 +25,11 @@ use jiff::Timestamp;
use serde::de::DeserializeOwned;
use taler_common::types::amount::Currency;
use thiserror::Error;
-use url::Url;
-use crate::wise_api::types::{Balance, BalanceStatement, MultiCurrencyAccount, Profile};
+use crate::{
+ config::ApiEnv,
+ wise_api::types::{Balance, BalanceStatement, MultiCurrencyAccount, Profile},
+};
#[derive(Error, Debug)]
pub enum WiseErr {
@@ -42,23 +44,23 @@ pub enum WiseErr {
}
pub type ApiResult<R> = std::result::Result<R, ApiErr<WiseErr>>;
-static API_URL: LazyLock<Url> = LazyLock::new(|| Url::from_str("https://api.wise.com").unwrap());
-
pub struct Client<'a> {
+ env: ApiEnv,
client: &'a http_client::Client,
token: HeaderValue,
}
impl<'a> Client<'a> {
- pub fn new(client: &'a http_client::Client, token: &'a str) -> Self {
+ pub fn new(client: &'a http_client::Client, env: ApiEnv, token: &'a str) -> Self {
Self {
client,
+ env,
token: HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
}
}
fn request(&self, method: Method, path: impl Into<Cow<'static, str>>) -> Req {
- Req::new(self.client, method, &API_URL, path)
+ Req::new(self.client, method, self.env.api(), path)
}
async fn send<T: DeserializeOwned>(&self, req: Req) -> ApiResult<T> {
diff --git a/adapters/taler-wise/src/worker.rs b/adapters/taler-wise/src/worker.rs
@@ -99,7 +99,7 @@ pub async fn run_worker(
transient: bool,
) -> anyhow::Result<()> {
let cfg = WorkerCfg::parse(cfg)?;
- let client = Client::new(client, &cfg.token);
+ let client = Client::new(client, cfg.api, &cfg.token);
let cfg = &cfg;
let client = &client;
let workers = cfg.balances.iter().map(|balance| {
diff --git a/adapters/taler-wise/wise.conf b/adapters/taler-wise/wise.conf
@@ -2,6 +2,9 @@
# Legal entity that is associated with the Wise account
NAME =
+# Api environment, this can either be production or sandbox
+API_ENV = production
+
[wise-worker]
# Adapter Wise profile ID
PROFILE_ID =
diff --git a/common/taler-common/src/config.rs b/common/taler-common/src/config.rs
@@ -1384,7 +1384,7 @@ mod test {
|sect, value| sect.duration(value),
&[(&["1.5s", "1500ms"], Duration::from_secs_f64(1.5))],
&[(&["-1s", "-1ns"], |_| {
- format!("temporal must not be negative")
+ "temporal must not be negative".to_string()
})],
)
}