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 }