taler-rust

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

wire.rs (8458B)


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