taler-rust

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

wire.rs (7970B)


      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::sync::{Arc, LazyLock};
     18 
     19 use axum::{
     20     Json, Router,
     21     extract::{Path, Query, State},
     22     http::StatusCode,
     23     response::IntoResponse as _,
     24     routing::{get, post},
     25 };
     26 use regex::Regex;
     27 use taler_common::{
     28     api_params::{AccountParams, History, HistoryParams, Page, TransferParams},
     29     api_wire::{
     30         AccountInfo, AddIncomingRequest, AddIncomingResponse, AddKycauthRequest,
     31         AddKycauthResponse, IncomingHistory, OutgoingHistory, TransferList, TransferRequest,
     32         TransferResponse, TransferState, TransferStatus, WireConfig,
     33     },
     34     error_code::ErrorCode,
     35 };
     36 
     37 use crate::{
     38     api::RouterUtils as _,
     39     auth::AuthMethod,
     40     constants::{MAX_PAGE_SIZE, MAX_TIMEOUT_MS, WIRE_GATEWAY_API_VERSION},
     41     error::{ApiResult, failure, failure_code, failure_status},
     42     json::Req,
     43 };
     44 
     45 use super::TalerApi;
     46 
     47 pub trait WireGateway: TalerApi {
     48     fn transfer(
     49         &self,
     50         req: TransferRequest,
     51     ) -> impl std::future::Future<Output = ApiResult<TransferResponse>> + Send;
     52     fn transfer_page(
     53         &self,
     54         page: Page,
     55         status: Option<TransferState>,
     56     ) -> impl std::future::Future<Output = ApiResult<TransferList>> + Send;
     57     fn transfer_by_id(
     58         &self,
     59         id: u64,
     60     ) -> impl std::future::Future<Output = ApiResult<Option<TransferStatus>>> + Send;
     61     fn outgoing_history(
     62         &self,
     63         params: History,
     64     ) -> impl std::future::Future<Output = ApiResult<OutgoingHistory>> + Send;
     65     fn incoming_history(
     66         &self,
     67         params: History,
     68     ) -> impl std::future::Future<Output = ApiResult<IncomingHistory>> + Send;
     69     fn add_incoming_reserve(
     70         &self,
     71         req: AddIncomingRequest,
     72     ) -> impl std::future::Future<Output = ApiResult<AddIncomingResponse>> + Send;
     73     fn add_incoming_kyc(
     74         &self,
     75         req: AddKycauthRequest,
     76     ) -> impl std::future::Future<Output = ApiResult<AddKycauthResponse>> + Send;
     77 
     78     fn support_account_check(&self) -> bool;
     79 
     80     fn account_check(
     81         &self,
     82         _params: AccountParams,
     83     ) -> impl std::future::Future<Output = ApiResult<Option<AccountInfo>>> + Send {
     84         async {
     85             Err(failure_status(
     86                 ErrorCode::END,
     87                 "API not implemented",
     88                 StatusCode::NOT_IMPLEMENTED,
     89             ))
     90         }
     91     }
     92 }
     93 
     94 static METADATA_PATTERN: LazyLock<Regex> =
     95     LazyLock::new(|| Regex::new("^[a-zA-Z0-9-.:]{1, 40}$").unwrap());
     96 
     97 pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
     98     Router::new()
     99         .route(
    100             "/transfer",
    101             post(
    102                 async |State(state): State<Arc<I>>, Req(req): Req<TransferRequest>| {
    103                     state.check_currency(&req.amount)?;
    104                     if let Some(metadata) = &req.metadata
    105                         && !METADATA_PATTERN.is_match(metadata)
    106                     {
    107                         return Err(failure(
    108                             ErrorCode::GENERIC_JSON_INVALID,
    109                             format!(
    110                                 "metadata '{metadata}' is malformed, must match {}",
    111                                 METADATA_PATTERN.as_str()
    112                             ),
    113                         ));
    114                     }
    115                     ApiResult::Ok(Json(state.transfer(req).await?))
    116                 },
    117             ),
    118         )
    119         .route(
    120             "/transfers",
    121             get(
    122                 async |State(state): State<Arc<I>>, Query(params): Query<TransferParams>| {
    123                     let page = params.pagination.check(MAX_PAGE_SIZE)?;
    124                     let list = state.transfer_page(page, params.status).await?;
    125                     ApiResult::Ok(if list.transfers.is_empty() {
    126                         StatusCode::NO_CONTENT.into_response()
    127                     } else {
    128                         Json(list).into_response()
    129                     })
    130                 },
    131             ),
    132         )
    133         .route(
    134             "/transfers/{id}",
    135             get(async |State(state): State<Arc<I>>, Path(id): Path<u64>| {
    136                 match state.transfer_by_id(id).await? {
    137                     Some(it) => Ok(Json(it)),
    138                     None => Err(failure(
    139                         ErrorCode::BANK_TRANSACTION_NOT_FOUND,
    140                         format!("Transfer '{id}' not found"),
    141                     )),
    142                 }
    143             }),
    144         )
    145         .route(
    146             "/history/incoming",
    147             get(
    148                 async |State(state): State<Arc<I>>, Query(params): Query<HistoryParams>| {
    149                     let params = params.check(MAX_PAGE_SIZE, MAX_TIMEOUT_MS)?;
    150                     let history = state.incoming_history(params).await?;
    151                     ApiResult::Ok(if history.incoming_transactions.is_empty() {
    152                         StatusCode::NO_CONTENT.into_response()
    153                     } else {
    154                         Json(history).into_response()
    155                     })
    156                 },
    157             ),
    158         )
    159         .route(
    160             "/history/outgoing",
    161             get(
    162                 async |State(state): State<Arc<I>>, Query(params): Query<HistoryParams>| {
    163                     let params = params.check(MAX_PAGE_SIZE, MAX_TIMEOUT_MS)?;
    164                     let history = state.outgoing_history(params).await?;
    165                     ApiResult::Ok(if history.outgoing_transactions.is_empty() {
    166                         StatusCode::NO_CONTENT.into_response()
    167                     } else {
    168                         Json(history).into_response()
    169                     })
    170                 },
    171             ),
    172         )
    173         .route(
    174             "/admin/add-incoming",
    175             post(
    176                 async |State(state): State<Arc<I>>, Req(req): Req<AddIncomingRequest>| {
    177                     state.check_currency(&req.amount)?;
    178                     ApiResult::Ok(Json(state.add_incoming_reserve(req).await?))
    179                 },
    180             ),
    181         )
    182         .route(
    183             "/admin/add-kycauth",
    184             post(
    185                 async |State(state): State<Arc<I>>, Req(req): Req<AddKycauthRequest>| {
    186                     state.check_currency(&req.amount)?;
    187                     ApiResult::Ok(Json(state.add_incoming_kyc(req).await?))
    188                 },
    189             ),
    190         )
    191         .route(
    192             "/account/check",
    193             get(
    194                 async |State(state): State<Arc<I>>, Query(params): Query<AccountParams>| match state
    195                     .account_check(params)
    196                     .await?
    197                 {
    198                     Some(it) => Ok(Json(it)),
    199                     None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)),
    200                 },
    201             ),
    202         )
    203         .auth(auth, "taler-wire-gateway")
    204         .route(
    205             "/config",
    206             get(async |State(state): State<Arc<I>>| {
    207                 Json(WireConfig {
    208                     name: "taler-wire-gateway",
    209                     version: WIRE_GATEWAY_API_VERSION,
    210                     currency: state.currency(),
    211                     implementation: Some(state.implementation()),
    212                     support_account_check: state.support_account_check(),
    213                 })
    214                 .into_response()
    215             }),
    216         )
    217         .with_state(state)
    218 }