taler-rust

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

wire.rs (8735B)


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