prepared.rs (4791B)
1 /* 2 This file is part of TALER 3 Copyright (C) 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::{str::FromStr, sync::Arc}; 18 19 use axum::{ 20 Json, Router, 21 extract::State, 22 http::StatusCode, 23 response::IntoResponse as _, 24 routing::{get, post}, 25 }; 26 use jiff::{SignedDuration, Timestamp}; 27 use taler_common::{ 28 api::prepared::{ 29 PreparedTransferConfig, RegistrationRequest, RegistrationResponse, SubjectFormat, 30 TransferSubject, Unregistration, 31 }, 32 db::IncomingType, 33 error_code::ErrorCode, 34 types::amount::Currency, 35 }; 36 37 use super::TalerApi; 38 use crate::{ 39 api::{Validation, check_currency}, 40 constants::PREPARED_TRANSFER_API_VERSION, 41 crypto::check_eddsa_signature, 42 error::{ApiResult, failure, failure_code}, 43 extract::Req, 44 subject::fmt_in_subject, 45 }; 46 47 pub trait PreparedTransfer: TalerApi { 48 fn supported_formats(&self) -> &[SubjectFormat]; 49 fn registration( 50 &self, 51 req: RegistrationRequest, 52 ) -> impl std::future::Future<Output = ApiResult<RegistrationResponse>> + Send; 53 fn unregistration( 54 &self, 55 req: Unregistration, 56 ) -> impl std::future::Future<Output = ApiResult<bool>> + Send; 57 } 58 59 impl Validation for RegistrationRequest { 60 fn check(&self, currency: &Currency) -> ApiResult<()> { 61 if !check_eddsa_signature( 62 &self.authorization_pub, 63 self.account_pub.as_ref(), 64 &self.authorization_sig, 65 ) { 66 return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE)); 67 } 68 check_currency(currency, &self.credit_amount) 69 } 70 } 71 72 impl Validation for Unregistration { 73 fn check(&self, _: &Currency) -> ApiResult<()> { 74 let timestamp = Timestamp::from_str(&self.timestamp).map_err(|e| { 75 failure(ErrorCode::GENERIC_JSON_INVALID, e.to_string()).with_path("timestamp") 76 })?; 77 if timestamp.duration_until(Timestamp::now()) > SignedDuration::from_mins(5) { 78 return Err(failure_code(ErrorCode::BANK_OLD_TIMESTAMP)); 79 } 80 81 if !check_eddsa_signature( 82 &self.authorization_pub, 83 self.timestamp.as_ref(), 84 &self.authorization_sig, 85 ) { 86 return Err(failure_code(ErrorCode::BANK_BAD_SIGNATURE)); 87 } 88 Ok(()) 89 } 90 } 91 92 pub fn simple_subject(req: RegistrationRequest) -> TransferSubject { 93 TransferSubject::Simple { 94 credit_amount: req.credit_amount, 95 subject: if req.authorization_pub == req.account_pub && !req.recurrent { 96 fmt_in_subject(req.r#type.into(), &req.account_pub).to_string() 97 } else { 98 fmt_in_subject(IncomingType::map, &req.authorization_pub).to_string() 99 }, 100 } 101 } 102 103 pub fn router<I: PreparedTransfer>(state: Arc<I>) -> Router { 104 Router::new() 105 .route( 106 "/registration", 107 post( 108 async |State(state): State<Arc<I>>, Req(req): Req<RegistrationRequest>| { 109 req.check(&state.currency())?; 110 let res = state.registration(req).await?; 111 ApiResult::Ok(Json(res)) 112 }, 113 ), 114 ) 115 .route( 116 "/unregistration", 117 post( 118 async |State(state): State<Arc<I>>, Req(req): Req<Unregistration>| { 119 req.check(&state.currency())?; 120 if state.unregistration(req).await? { 121 ApiResult::Ok(StatusCode::NO_CONTENT) 122 } else { 123 Err(failure_code(ErrorCode::BANK_TRANSACTION_NOT_FOUND)) 124 } 125 }, 126 ), 127 ) 128 .route( 129 "/config", 130 get(async |State(state): State<Arc<I>>| { 131 Json(PreparedTransferConfig { 132 name: (), 133 version: PREPARED_TRANSFER_API_VERSION, 134 currency: state.currency(), 135 implementation: Some(state.implementation()), 136 supported_formats: state.supported_formats().to_vec(), 137 }) 138 .into_response() 139 }), 140 ) 141 .with_state(state) 142 }