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 }