PreparedTransferApi.kt (5461B)
1 /* 2 * This file is part of LibEuFin. 3 * Copyright (C) 2026 Taler Systems S.A. 4 5 * LibEuFin is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU Affero General Public License as 7 * published by the Free Software Foundation; either version 3, or 8 * (at your option) any later version. 9 10 * LibEuFin is distributed in the hope that it will be useful, but 11 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 12 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General 13 * Public License for more details. 14 15 * You should have received a copy of the GNU Affero General Public 16 * License along with LibEuFin; see the file COPYING. If not, see 17 * <http://www.gnu.org/licenses/> 18 */ 19 20 package tech.libeufin.bank.api 21 22 import io.ktor.http.* 23 import io.ktor.server.request.* 24 import io.ktor.server.response.* 25 import io.ktor.server.routing.* 26 import io.github.smiley4.ktoropenapi.* 27 import io.ktor.util.pipeline.* 28 import tech.libeufin.bank.* 29 import tech.libeufin.bank.auth.pathUsername 30 import tech.libeufin.bank.db.Database 31 import tech.libeufin.bank.db.TransferDAO.RegistrationResult 32 import tech.libeufin.common.* 33 import tech.libeufin.common.crypto.CryptoUtil 34 import java.time.Instant 35 import java.time.Duration 36 37 fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { 38 get("/taler-prepared-transfer/config", { 39 operationId = "getPreparedTransferConfig" 40 description = "Get the configuration of the prepared transfer API" 41 tags = listOf("Prepared Transfer") 42 request { 43 pathParameter<String>("USERNAME") { description = "Account username" } 44 } 45 response { 46 code(HttpStatusCode.OK) { description = "Configuration of the prepared transfer API"; body<PreparedTransferConfig>() } 47 } 48 }) { 49 call.respond( 50 PreparedTransferConfig( 51 currency = cfg.regionalCurrency, 52 supported_formats = listOf(SubjectFormat.SIMPLE) 53 ) 54 ) 55 } 56 post("/taler-prepared-transfer/registration", { 57 operationId = "registerTransfer" 58 description = "Register a prepared transfer" 59 tags = listOf("Prepared Transfer") 60 request { 61 body<SubjectRequest>() 62 } 63 response { 64 code(HttpStatusCode.OK) { description = "Registration successful"; body<SubjectResult>() } 65 code(HttpStatusCode.Conflict) { description = "Reserve pub or subject derivation already used" } 66 } 67 }) { 68 val req = call.receive<SubjectRequest>() 69 cfg.checkRegionalCurrency(req.credit_amount) 70 71 if (!req.verify()) 72 throw forbidden( 73 "invalid signature", 74 TalerErrorCode.BANK_BAD_SIGNATURE 75 ) 76 77 when (val result = db.transfer.register( 78 req.credit_account, 79 req.type, 80 req.account_pub, 81 req.authorization_pub, 82 req.authorization_sig, 83 req.recurrent, 84 req.credit_amount, 85 Instant.now() 86 )) { 87 RegistrationResult.NotExchange -> throw notExchange(req.credit_account.canonical) 88 RegistrationResult.ReservePubReuse -> throw conflict( 89 "reserve_pub used already", 90 TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT 91 ) 92 RegistrationResult.UnknownCreditor -> throw unknownCreditorAccount(req.credit_account.canonical) 93 is RegistrationResult.Success -> { 94 val subjects = mutableListOf<TransferSubject>() 95 if (result.uuid != null) 96 subjects.add(TransferSubject.Uri(cfg.talerWithdrawUri(result.uuid), req.credit_amount)) 97 subjects.add(TransferSubject.Simple(fmtIncomingSubject(IncomingType.map, req.authorization_pub), req.credit_amount)) 98 call.respond( 99 SubjectResult( 100 subjects, 101 TalerTimestamp.never() 102 ) 103 ) 104 } 105 } 106 } 107 post("/taler-prepared-transfer/unregistration", { 108 operationId = "unregisterTransfer" 109 description = "Unregister a prepared subject" 110 tags = listOf("Prepared Transfer") 111 response { 112 code(HttpStatusCode.NoContent) { description = "Successfully unregistered" } 113 code(HttpStatusCode.NotFound) { description = "Prepared transfer not found" } 114 code(HttpStatusCode.Conflict) { description = "Invalid signature or timestamp too old" } 115 } 116 }) { 117 val req = call.receive<Unregistration>(); 118 119 logger.error("${req.nbo().joinToString(", ") { (it.toInt() and 0xFF).toString() }}") 120 121 if (req.timestamp.instant.isBefore(Instant.now().minus(Duration.ofMinutes(15)))) 122 throw conflict( 123 "timestamp too old", 124 TalerErrorCode.BANK_OLD_TIMESTAMP 125 ) 126 127 if (!req.verify()) 128 throw forbidden( 129 "invalid signature", 130 TalerErrorCode.BANK_BAD_SIGNATURE 131 ) 132 133 if (db.transfer.unregister(req.authorization_pub, Instant.now())) { 134 call.respond(HttpStatusCode.NoContent) 135 } else { 136 throw notFound( 137 "Prepared transfer '${req.authorization_pub}' not found", 138 TalerErrorCode.BANK_TRANSACTION_NOT_FOUND 139 ) 140 } 141 } 142 }