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 }