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 }