taler-rust

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

wire.rs (8910B)


      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, validate_base_url},
     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         if let Err(err) = validate_base_url(&self.exchange_base_url) {
    113             return Err(bad_request(err).with_path("exchange_base_url"));
    114         }
    115         check_currency(currency, &self.amount)
    116     }
    117 }
    118 
    119 impl Validation for AddIncomingRequest {
    120     fn check(&self, currency: &Currency) -> ApiResult<()> {
    121         check_currency(currency, &self.amount)
    122     }
    123 }
    124 
    125 impl Validation for AddKycauthRequest {
    126     fn check(&self, currency: &Currency) -> ApiResult<()> {
    127         check_currency(currency, &self.amount)
    128     }
    129 }
    130 
    131 impl Validation for AddMappedRequest {
    132     fn check(&self, currency: &Currency) -> ApiResult<()> {
    133         check_currency(currency, &self.amount)
    134     }
    135 }
    136 
    137 pub fn router<I: WireGateway>(state: Arc<I>, auth: AuthMethod) -> Router {
    138     Router::new()
    139         .route(
    140             "/transfer",
    141             post(
    142                 async |State(state): State<Arc<I>>, Req(req): Req<TransferRequest>| {
    143                     req.check(&state.currency())?;
    144                     ApiResult::Ok(Json(state.transfer(req).await?))
    145                 },
    146             ),
    147         )
    148         .route(
    149             "/transfers",
    150             get(
    151                 async |State(state): State<Arc<I>>, Query(params): Query<TransferParams>| {
    152                     let page = params.pagination.check()?;
    153                     let list = state.transfer_page(page, params.status).await?;
    154                     ApiResult::Ok(if list.transfers.is_empty() {
    155                         StatusCode::NO_CONTENT.into_response()
    156                     } else {
    157                         Json(list).into_response()
    158                     })
    159                 },
    160             ),
    161         )
    162         .route(
    163             "/transfers/{id}",
    164             get(async |State(state): State<Arc<I>>, Path(id): Path<u64>| {
    165                 match state.transfer_by_id(id).await? {
    166                     Some(it) => Ok(Json(it)),
    167                     None => Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)),
    168                 }
    169             }),
    170         )
    171         .route(
    172             "/history/incoming",
    173             get(
    174                 async |State(state): State<Arc<I>>, Query(params): Query<HistoryParams>| {
    175                     let params = params.check()?;
    176                     let history = state.incoming_history(params).await?;
    177                     ApiResult::Ok(if history.incoming_transactions.is_empty() {
    178                         StatusCode::NO_CONTENT.into_response()
    179                     } else {
    180                         Json(history).into_response()
    181                     })
    182                 },
    183             ),
    184         )
    185         .route(
    186             "/history/outgoing",
    187             get(
    188                 async |State(state): State<Arc<I>>, Query(params): Query<HistoryParams>| {
    189                     let params = params.check()?;
    190                     let history = state.outgoing_history(params).await?;
    191                     ApiResult::Ok(if history.outgoing_transactions.is_empty() {
    192                         StatusCode::NO_CONTENT.into_response()
    193                     } else {
    194                         Json(history).into_response()
    195                     })
    196                 },
    197             ),
    198         )
    199         .route(
    200             "/admin/add-incoming",
    201             post(
    202                 async |State(state): State<Arc<I>>, Req(req): Req<AddIncomingRequest>| {
    203                     req.check(&state.currency())?;
    204                     ApiResult::Ok(Json(state.add_incoming_reserve(req).await?))
    205                 },
    206             ),
    207         )
    208         .route(
    209             "/admin/add-kycauth",
    210             post(
    211                 async |State(state): State<Arc<I>>, Req(req): Req<AddKycauthRequest>| {
    212                     req.check(&state.currency())?;
    213                     ApiResult::Ok(Json(state.add_incoming_kyc(req).await?))
    214                 },
    215             ),
    216         )
    217         .route(
    218             "/admin/add-mapped",
    219             post(
    220                 async |State(state): State<Arc<I>>, Req(req): Req<AddMappedRequest>| {
    221                     req.check(&state.currency())?;
    222                     ApiResult::Ok(Json(state.add_incoming_mapped(req).await?))
    223                 },
    224             ),
    225         )
    226         .route(
    227             "/account/check",
    228             get(
    229                 async |State(state): State<Arc<I>>, Query(params): Query<AccountParams>| match state
    230                     .account_check(params)
    231                     .await?
    232                 {
    233                     Some(it) => Ok(Json(it)),
    234                     None => Err(failure_code(ErrorCode::BANK_UNKNOWN_ACCOUNT)),
    235                 },
    236             ),
    237         )
    238         .auth(auth, "taler-wire-gateway")
    239         .route(
    240             "/config",
    241             get(async |State(state): State<Arc<I>>| {
    242                 Json(WireConfig {
    243                     name: (),
    244                     version: WIRE_GATEWAY_API_VERSION,
    245                     currency: state.currency(),
    246                     implementation: Some(state.implementation()),
    247                     support_account_check: state.support_account_check(),
    248                 })
    249                 .into_response()
    250             }),
    251         )
    252         .with_state(state)
    253 }