libeufin

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

commit ddef3b151f6cf3a2b4884698c0bdde2b046b0314
parent f169ea735202c35ab3154213c853b5df41902fc8
Author: MS <ms@taler.net>
Date:   Wed, 20 Sep 2023 16:30:31 +0200

Taler integration API handlers.

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 3++-
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 40++++++++++++++++++++++++++++++++++++++--
Abank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt | 106+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mbank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt | 21---------------------
Mbank/src/main/kotlin/tech/libeufin/bank/types.kt | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
5 files changed, 205 insertions(+), 26 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -226,6 +226,7 @@ val webApp: Application.() -> Unit = { this.tokenHandlers() this.transactionsHandlers() this.talerWebHandlers() - // this.walletIntegrationHandlers() + this.talerIntegrationHandlers() + // this.talerWireGatewayHandlers() } } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -27,6 +27,7 @@ import net.taler.wallet.crypto.Base32Crockford import tech.libeufin.util.* import java.lang.NumberFormatException import java.net.URL +import java.util.* fun ApplicationCall.expectUriComponent(componentName: String) = this.maybeUriComponent(componentName) ?: throw badRequest( @@ -369,4 +370,40 @@ fun getTalerWithdrawUri(baseUrl: String, woId: String) = } pathSegments.add("taler-integration/${woId}") this.appendPathSegments(pathSegments) - } -\ No newline at end of file + } + +fun getWithdrawalConfirmUrl(baseUrl: String, wopId: String) = + url { + val baseUrlObj = URL(baseUrl) + protocol = URLProtocol(name = baseUrlObj.protocol, defaultPort = -1) + host = baseUrlObj.host + // Removing potential double slashes: + baseUrlObj.path.split("/").forEach { + this.appendPathSegments(it) + } + // Completing the endpoint: + this.appendPathSegments("${wopId}/confirm") + } + + +/** + * This handler factors out the checking of the query param + * and the retrieval of the related withdrawal database row. + * It throws 404 if the operation is not found, and throws 400 + * if the query param doesn't parse into a UUID. Currently + * used by the Taler Web/SPA and Integration API handlers. + */ +fun getWithdrawal(opIdParam: String): TalerWithdrawalOperation { + val opId = try { + UUID.fromString(opIdParam) + } catch (e: Exception) { + logger.error(e.message) + throw badRequest("withdrawal_id query parameter was malformed") + } + val op = db.talerWithdrawalGet(opId) + ?: throw notFound( + hint = "Withdrawal operation ${opIdParam} not found", + talerEc = TalerErrorCode.TALER_EC_END + ) + return op +} diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt @@ -0,0 +1,106 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2019 Stanisci and Dold. + + * LibEuFin is free software; you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation; either version 3, or + * (at your option) any later version. + + * LibEuFin is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General + * Public License for more details. + + * You should have received a copy of the GNU Affero General Public + * License along with LibEuFin; see the file COPYING. If not, see + * <http://www.gnu.org/licenses/> + */ + +/* This file contains the Taler Integration API endpoints, +* that are typically requested by wallets. */ +package tech.libeufin.bank + +import io.ktor.server.application.* +import io.ktor.server.request.* +import io.ktor.server.response.* +import io.ktor.server.routing.* +import net.taler.common.errorcodes.TalerErrorCode +import tech.libeufin.util.getBaseUrl + +fun Routing.talerIntegrationHandlers() { + get("/taler-integration/config") { + val internalCurrency: String = db.configGet("internal_currency") + ?: throw internalServerError("Currency not found") + call.respond(TalerIntegrationConfigResponse(currency = internalCurrency)) + return@get + } + // Note: wopid acts as an authentication token. + get("/taler-integration/withdrawal-operation/{wopid}") { + val wopid = call.expectUriComponent("wopid") + val op = getWithdrawal(wopid) // throws 404 if not found. + val relatedBankAccount = db.bankAccountGetFromOwnerId(op.walletBankAccount) + if (relatedBankAccount == null) + throw internalServerError("Bank has a withdrawal not related to any bank account.") + val suggestedExchange = db.configGet("suggested_exchange") + ?: throw internalServerError("Bank does not have an exchange to suggest.") + val confirmUrl = getWithdrawalConfirmUrl( + baseUrl = call.request.getBaseUrl() ?: throw internalServerError("Could not get bank own base URL."), + wopId = wopid + ) + call.respond(BankWithdrawalOperationStatus( + aborted = op.aborted, + selection_done = op.selectionDone, + transfer_done = op.confirmationDone, + amount = op.amount.toString(), + sender_wire = relatedBankAccount.internalPaytoUri, + suggested_exchange = suggestedExchange, + confirm_transfer_url = confirmUrl + )) + return@get + } + post("/taler-integration/withdrawal-operation/{wopid}") { + val wopid = call.expectUriComponent("wopid") + val req = call.receive<BankWithdrawalOperationPostRequest>() + val op = getWithdrawal(wopid) // throws 404 if not found. + if (op.selectionDone) { + // idempotency + if (op.selectedExchangePayto != req.selected_exchange && + op.reservePub != req.reserve_pub) + throw conflict( + hint = "Cannot select different exchange and reserve pub. under the same withdrawal operation", + talerEc = TalerErrorCode.TALER_EC_BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT + ) + } + val dbSuccess: Boolean = if (!op.selectionDone) { + val exchangePayto = req.selected_exchange + ?: (db.configGet("suggested_exchange") + ?: throw internalServerError("Suggested exchange not found") + ) + db.talerWithdrawalSetDetails( + op.withdrawalUuid, + exchangePayto, + req.reserve_pub + ) + } + else // DB succeeded in the past. + true + if (!dbSuccess) + // Whatever the problem, the bank missed it: respond 500. + throw internalServerError("Bank failed at selecting the withdrawal.") + val resp = BankWithdrawalOperationPostResponse( + transfer_done = op.confirmationDone, + confirm_transfer_url = if (!op.confirmationDone) + getWithdrawalConfirmUrl( + baseUrl = call.request.getBaseUrl() + ?: throw internalServerError("Could not get bank own base URL."), + wopId = wopid + ) + else + null + ) + call.respond(resp) + return@post + } +} + diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt @@ -36,27 +36,6 @@ import tech.libeufin.util.getBaseUrl import tech.libeufin.util.getNowUs import java.util.* -/** - * This handler factors out the checking of the query param - * and the retrieval of the related withdrawal database row. - * It throws 404 if the operation is not found, and throws 400 - * if the query param doesn't parse into an UUID. - */ -private fun getWithdrawal(opIdParam: String): TalerWithdrawalOperation { - val opId = try { - UUID.fromString(opIdParam) - } catch (e: Exception) { - logger.error(e.message) - throw badRequest("withdrawal_id query parameter was malformed") - } - val op = db.talerWithdrawalGet(opId) - ?: throw notFound( - hint = "Withdrawal operation ${opIdParam} not found", - talerEc = TalerErrorCode.TALER_EC_END - ) - return op -} - fun Routing.talerWebHandlers() { post("/accounts/{USERNAME}/withdrawals") { val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt b/bank/src/main/kotlin/tech/libeufin/bank/types.kt @@ -439,4 +439,61 @@ enum class WithdrawalConfirmationResult { OP_NOT_FOUND, EXCHANGE_NOT_FOUND, BALANCE_INSUFFICIENT -} -\ No newline at end of file +} + +// GET /config response from the Taler Integration API. +@Serializable +data class TalerIntegrationConfigResponse( + val name: String = "taler-bank-integration", + val version: String = "0:0:0:", + val currency: String +) + +// Withdrawal status as spec'd in the Taler Integration API. +@Serializable +data class BankWithdrawalOperationStatus( + // Indicates whether the withdrawal was aborted. + val aborted: Boolean, + + /* Has the wallet selected parameters for the withdrawal operation + (exchange and reserve public key) and successfully sent it + to the bank? */ + val selection_done: Boolean, + + /* The transfer has been confirmed and registered by the bank. + Does not guarantee that the funds have arrived at the exchange + already. */ + val transfer_done: Boolean, + + /* Amount that will be withdrawn with this operation + (raw amount without fee considerations). */ + val amount: String, + + /* Bank account of the customer that is withdrawing, as a + ``payto`` URI. */ + val sender_wire: String? = null, + + // Suggestion for an exchange given by the bank. + val suggested_exchange: String? = null, + + /* URL that the user needs to navigate to in order to + complete some final confirmation (e.g. 2FA). + It may contain withdrawal operation id */ + val confirm_transfer_url: String? = null, + + // Wire transfer types supported by the bank. + val wire_types: MutableList<String> = mutableListOf("iban") +) + +// Selection request on a Taler withdrawal. +@Serializable +data class BankWithdrawalOperationPostRequest( + val reserve_pub: String, + val selected_exchange: String? = null // Use suggested exchange if that's missing. +) + +@Serializable +data class BankWithdrawalOperationPostResponse( + val transfer_done: Boolean, + val confirm_transfer_url: String? = null +) +\ No newline at end of file