taler-rust

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

client.rs (4103B)


      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 as _, sync::LazyLock};
     18 
     19 use http_client::{ApiErr, ClientErr, builder::Req};
     20 use hyper::{
     21     Method, StatusCode,
     22     header::{AUTHORIZATION, HeaderValue, RETRY_AFTER},
     23 };
     24 use jiff::Timestamp;
     25 use serde::de::DeserializeOwned;
     26 use taler_common::types::amount::Currency;
     27 use thiserror::Error;
     28 use url::Url;
     29 
     30 use crate::wise_api::types::{Balance, BalanceStatement, MultiCurrencyAccount, Profile};
     31 
     32 #[derive(Error, Debug)]
     33 pub enum WiseErr {
     34     #[error("status {0}")]
     35     Status(StatusCode),
     36     #[error("status {0} '{1}'")]
     37     StatusCause(StatusCode, String),
     38     #[error("Rate limited for {0}")]
     39     TooManyRequest(String),
     40     #[error(transparent)]
     41     Client(#[from] ClientErr),
     42 }
     43 pub type ApiResult<R> = std::result::Result<R, ApiErr<WiseErr>>;
     44 
     45 static API_URL: LazyLock<Url> = LazyLock::new(|| Url::from_str("https://api.wise.com").unwrap());
     46 
     47 pub struct Client<'a> {
     48     client: &'a http_client::Client,
     49     token: HeaderValue,
     50 }
     51 
     52 impl<'a> Client<'a> {
     53     pub fn new(client: &'a http_client::Client, token: &'a str) -> Self {
     54         Self {
     55             client,
     56             token: HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
     57         }
     58     }
     59 
     60     fn request(&self, method: Method, path: impl Into<Cow<'static, str>>) -> Req {
     61         Req::new(self.client, method, &API_URL, path)
     62     }
     63 
     64     async fn send<T: DeserializeOwned>(&self, req: Req) -> ApiResult<T> {
     65         let (ctx, res) = req
     66             .header(AUTHORIZATION, self.token.clone())
     67             .send()
     68             .await
     69             .map_err(|(ctx, e)| ctx.wrap(e.into()))?;
     70         match res.status() {
     71             StatusCode::OK => {
     72                 // Parse json
     73                 res.json().await.map_err(|e| ctx.wrap(e.into()))
     74             }
     75             StatusCode::TOO_MANY_REQUESTS => Err(ctx.wrap(WiseErr::TooManyRequest(
     76                 res.str_header(RETRY_AFTER.as_str()).unwrap_or_default(),
     77             ))),
     78             other => Err(ctx.wrap(WiseErr::Status(other))),
     79         }
     80     }
     81 
     82     pub async fn profiles(&self) -> ApiResult<Vec<Profile>> {
     83         Self::send(self, self.request(Method::GET, "/v2/profiles")).await
     84     }
     85 
     86     pub async fn standard_balances(&self, profile_id: u64) -> ApiResult<Vec<Balance>> {
     87         Self::send(
     88             self,
     89             self.request(
     90                 Method::GET,
     91                 format!("/v4/profiles/{profile_id}/balances?types=STANDARD"),
     92             ),
     93         )
     94         .await
     95     }
     96 
     97     pub async fn borderless_accounts(
     98         &self,
     99         profile_id: u64,
    100     ) -> ApiResult<Vec<MultiCurrencyAccount>> {
    101         Self::send(
    102             self,
    103             self.request(
    104                 Method::GET,
    105                 format!("/v1/borderless-accounts?profileId={profile_id}"),
    106             ),
    107         )
    108         .await
    109     }
    110 
    111     pub async fn balance_statement(
    112         &self,
    113         profile_id: u64,
    114         balance_id: u32,
    115         currency: &Currency,
    116         start: Timestamp,
    117         end: Timestamp,
    118     ) -> ApiResult<BalanceStatement> {
    119         Self::send(
    120             self,
    121             self.request(
    122                 Method::GET,
    123                 format!("/v1/profiles/{profile_id}/balance-statements/{balance_id}/statement.json"),
    124             )
    125             .query("currency", currency)
    126             .query("intervalStart", start)
    127             .query("intervalEnd", end),
    128         )
    129         .await
    130     }
    131 }