libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

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 }