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 }