libeufin

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

commit d2497386c0a8edc3ebcdd782ca92e555d82e189d
parent cdb6712523aacb3e6f0a44d005405862e09fdc0e
Author: Antoine A <>
Date:   Mon, 20 Jan 2025 17:15:28 +0100

common: add wire gateway account check

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Error.kt | 9++++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt | 5+----
Mbank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt | 39++++++++++++++++++---------------------
Mbank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 14+++++++++++++-
Mbank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 4++--
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 8++++++++
Mcommon/src/main/kotlin/ApiError.kt | 11++++++++---
Mcommon/src/main/kotlin/Constants.kt | 2+-
Mcommon/src/main/kotlin/TalerMessage.kt | 15++++++++++-----
Mcommon/src/main/kotlin/client.kt | 6+++---
Mcommon/src/main/kotlin/params.kt | 28++++++++++++++++++++++++----
Mnexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt | 6+++++-
Mnexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt | 4++--
Mnexus/src/test/kotlin/WireGatewayApiTest.kt | 6++++++
14 files changed, 109 insertions(+), 48 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -57,4 +57,11 @@ fun unsupportedTanChannel(channel: TanChannel): ApiException { "Unsupported tan channel $channel", TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED ) +} + +fun notExchange(username: String): ApiException { + return conflict( + "Account '$username' is not an exchange account.", + TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE + ) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/BankIntegrationApi.kt @@ -92,10 +92,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) { "Account ${req.selected_exchange} not found", TalerErrorCode.BANK_UNKNOWN_ACCOUNT ) - WithdrawalSelectionResult.AccountIsNotExchange -> throw conflict( - "Account ${req.selected_exchange} is not an exchange", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + WithdrawalSelectionResult.AccountIsNotExchange -> throw notExchange(req.selected_exchange.toString()) WithdrawalSelectionResult.AmountDiffers -> throw conflict( "Given amount is different from the current", TalerErrorCode.BANK_AMOUNT_DIFFERS diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt @@ -42,7 +42,8 @@ import java.time.Instant fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { get("/accounts/{USERNAME}/taler-wire-gateway/config") { call.respond(WireGatewayConfig( - currency = cfg.regionalCurrency + currency = cfg.regionalCurrency, + support_account_check = false )) } auth(db, cfg.pwCrypto, TokenLogicalScope.readwrite_wiregateway, cfg.basicAuthCompat) { @@ -56,10 +57,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { ) when (res) { is TransferResult.UnknownExchange -> throw unknownAccount(call.username) - is TransferResult.NotAnExchange -> throw conflict( - "${call.username} is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + is TransferResult.NotAnExchange -> throw notExchange(call.username) is TransferResult.BothPartyAreExchange -> throw conflict( "Wire transfer attempted with credit and debit party being both exchange account", TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE @@ -90,10 +88,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { val bankAccount = this.bankInfo(db) if (!bankAccount.isTalerExchange) - throw conflict( - "$username is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + throw notExchange(username) val items = db.exchange.dbLambda(params, bankAccount.bankAccountId) @@ -113,10 +108,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { val params = TransferParams.extract(call.request.queryParameters) val bankAccount = call.bankInfo(db) if (!bankAccount.isTalerExchange) - throw conflict( - "${call.username} is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + throw notExchange(call.username) if (params.status != null && params.status != TransferStatusState.success && params.status != TransferStatusState.permanent_failure) { call.respond(HttpStatusCode.NoContent) @@ -133,10 +125,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { get("/accounts/{USERNAME}/taler-wire-gateway/transfers/{ROW_ID}") { val bankAccount = call.bankInfo(db) if (!bankAccount.isTalerExchange) - throw conflict( - "${call.username} is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + throw notExchange(call.username) val txId = call.longPath("ROW_ID") val transfer = db.exchange.getTransfer(bankAccount.bankAccountId, txId) ?: throw notFound( @@ -145,6 +134,17 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { ) call.respond(transfer) } + get("/accounts/{USERNAME}/taler-wire-gateway/account/check") { + val bankAccount = call.bankInfo(db) + if (!bankAccount.isTalerExchange) + throw notExchange(call.username) + + val params = AccountCheckParams.extract(call.request.queryParameters) + val account = params.account.expectRequestIban() + + val info = db.account.checkInfo(account) ?: throw unknownAccount(account.canonical) + call.respond(info) + } } authAdmin(db, cfg.pwCrypto, TokenLogicalScope.readwrite, cfg.basicAuthCompat) { suspend fun ApplicationCall.addIncoming( @@ -165,10 +165,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) { ) when (res) { is AddIncomingResult.UnknownExchange -> throw unknownAccount(username) - is AddIncomingResult.NotAnExchange -> throw conflict( - "$username is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) + is AddIncomingResult.NotAnExchange -> throw notExchange(username) is AddIncomingResult.UnknownDebtor -> throw conflict( "Debtor account $debitAccount was not found", TalerErrorCode.BANK_UNKNOWN_DEBTOR diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023-2024 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -517,6 +517,18 @@ class AccountDAO(private val db: Database) { } } + /** Check bank info of account [payto] */ + suspend fun checkInfo(payto: IbanPayto): AccountInfo? = db.serializable( + """ + SELECT FROM bank_accounts WHERE internal_payto=? + """ + ) { + setString(1, payto.canonical) + oneOrNull { + AccountInfo() + } + } + /** Get data of account [username] */ suspend fun get(username: String): AccountData? = db.serializable( """ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023-2024 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -113,6 +113,6 @@ suspend fun createAdminAccount(db: Database, cfg: BankConfig, pw: String? = null fun Route.conditional(implemented: Boolean, callback: Route.() -> Unit): Route = intercept("Conditional", callback) { if (!implemented) { - throw apiError(HttpStatusCode.NotImplemented, "API not implemented", TalerErrorCode.END) + throw notImplemented() } } \ No newline at end of file diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -382,4 +382,12 @@ class WireGatewayApiTest { } } } + + // POST /taler-wire-gateway/account/check + @Test + fun accountCheck() = bankSetup { + client.getA("/accounts/exchange/taler-wire-gateway/account/check").assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) + client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$unknownPayto").assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + client.getA("/accounts/exchange/taler-wire-gateway/account/check?account=$merchantPayto").assertOkJson<AccountInfo>() + } } \ No newline at end of file diff --git a/common/src/main/kotlin/ApiError.kt b/common/src/main/kotlin/ApiError.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -130,4 +130,9 @@ fun badRequest( fun unsupportedMediaType( hint: String, error: TalerErrorCode = TalerErrorCode.END, -): ApiException = apiError(HttpStatusCode.UnsupportedMediaType, hint, error) -\ No newline at end of file +): ApiException = apiError(HttpStatusCode.UnsupportedMediaType, hint, error) + +fun notImplemented( + hint: String = "API not implemented", + error: TalerErrorCode = TalerErrorCode.END, +): ApiException = apiError(HttpStatusCode.NotImplemented, hint, error) +\ No newline at end of file diff --git a/common/src/main/kotlin/Constants.kt b/common/src/main/kotlin/Constants.kt @@ -26,7 +26,7 @@ const val SERIALIZATION_RETRY: Int = 30 const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB // API version -const val WIRE_GATEWAY_API_VERSION: String = "3:1:2" +const val WIRE_GATEWAY_API_VERSION: String = "4:0:3" const val REVENUE_API_VERSION: String = "1:1:1" // HTTP headers diff --git a/common/src/main/kotlin/TalerMessage.kt b/common/src/main/kotlin/TalerMessage.kt @@ -37,7 +37,8 @@ enum class TransferStatusState { /** Response GET /taler-wire-gateway/config */ @Serializable data class WireGatewayConfig( - val currency: String + val currency: String, + val support_account_check: Boolean ) { val name: String = "taler-wire-gateway" val version: String = WIRE_GATEWAY_API_VERSION @@ -110,14 +111,14 @@ data class AddKycauthRequest( val debit_account: Payto ) -/** Request GET /taler-wire-gateway/history/incoming */ +/** Response GET /taler-wire-gateway/history/incoming */ @Serializable data class IncomingHistory( val incoming_transactions: List<IncomingBankTransaction>, val credit_account: String ) -/** Inner request GET /taler-wire-gateway/history/incoming */ +/** Inner response GET /taler-wire-gateway/history/incoming */ @Serializable sealed interface IncomingBankTransaction { val row_id: Long @@ -159,14 +160,14 @@ data class IncomingWadTransaction( val wad_id: String // TODO 24 bytes Base32 ): IncomingBankTransaction -/** Request GET /taler-wire-gateway/history/outgoing */ +/** Response GET /taler-wire-gateway/history/outgoing */ @Serializable data class OutgoingHistory( val outgoing_transactions: List<OutgoingTransaction>, val debit_account: String ) -/** Inner request GET /taler-wire-gateway/history/outgoing */ +/** Inner response GET /taler-wire-gateway/history/outgoing */ @Serializable data class OutgoingTransaction( val row_id: Long, // DB row ID of the payment. @@ -177,6 +178,10 @@ data class OutgoingTransaction( val exchange_base_url: String, ) +/** Response GET /taler-wire-gateway/account/check */ +@Serializable +class AccountInfo() + /** Response GET /taler-revenue/config */ @Serializable data class RevenueConfig( diff --git a/common/src/main/kotlin/client.kt b/common/src/main/kotlin/client.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2023-2024 Taler Systems S.A. + * Copyright (C) 2023-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -97,8 +97,8 @@ suspend fun HttpResponse.assertStatus(status: HttpStatusCode, err: TalerErrorCod } suspend fun HttpResponse.assertOk(): HttpResponse = assertStatus(HttpStatusCode.OK, null) -suspend fun HttpResponse.assertNoContent(): HttpResponse - = assertStatus(HttpStatusCode.NoContent, null) +suspend fun HttpResponse.assertNoContent(err: TalerErrorCode? = null): HttpResponse + = assertStatus(HttpStatusCode.NoContent, err) suspend fun HttpResponse.assertAccepted(): HttpResponse = assertStatus(HttpStatusCode.Accepted, null) suspend fun HttpResponse.assertNotFound(err: TalerErrorCode): HttpResponse diff --git a/common/src/main/kotlin/params.kt b/common/src/main/kotlin/params.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -33,17 +33,26 @@ fun Parameters.long(name: String): Long? = get(name)?.run { toLongOrNull() ?: throw paramsMalformed("Param '$name' not a number") } fun Parameters.expectLong(name: String): Long = long(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) -fun Parameters.uuid(name: String): UUID? { - return get(name)?.run { +fun Parameters.uuid(name: String): UUID? + = get(name)?.run { try { UUID.fromString(this) } catch (e: Exception) { throw paramsMalformed("Param '$name' not an UUID") } } -} fun Parameters.expectUuid(name: String): UUID = uuid(name) ?: throw badRequest("Missing '$name' UUID parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) +fun Parameters.payto(name: String): Payto? + = get(name)?.run { + try { + Payto.parse(this) + } catch (e: Exception) { + throw paramsMalformed("Param '$name' not a valid payto") + } + } +fun Parameters.expectPayto(name: String): Payto + = payto(name) ?: throw badRequest("Missing '$name' payto parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) fun Parameters.amount(name: String): TalerAmount? = get(name)?.run { try { @@ -121,4 +130,15 @@ data class HistoryParams( return HistoryParams(PageParams.extract(params), PollingParams.extract(params)) } } +} + +data class AccountCheckParams( + val account: Payto +) { + companion object { + fun extract(params: Parameters): AccountCheckParams { + val account = params.expectPayto("account") + return AccountCheckParams(account) + } + } } \ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt @@ -40,7 +40,8 @@ import java.time.Instant fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = conditional(cfg.wireGatewayApiCfg) { get("/taler-wire-gateway/config") { call.respond(WireGatewayConfig( - currency = cfg.currency + currency = cfg.currency, + support_account_check = true )) } auth(cfg.wireGatewayApiCfg) { @@ -103,6 +104,9 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) = conditional(cfg.wir get("/taler-wire-gateway/history/outgoing") { call.historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory) } + get("/taler-wire-gateway/account/check") { + throw notImplemented() + } suspend fun ApplicationCall.addIncoming( amount: TalerAmount, debitAccount: Payto, diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/api/helpers.kt @@ -1,6 +1,6 @@ /* * This file is part of LibEuFin. - * Copyright (C) 2024 Taler Systems S.A. + * Copyright (C) 2024-2025 Taler Systems S.A. * LibEuFin is free software; you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as @@ -63,6 +63,6 @@ fun Route.auth(cfg: ApiConfig?, callback: Route.() -> Unit): Route = fun Route.conditional(cfg: ApiConfig?, callback: Route.() -> Unit): Route = intercept("Conditional", callback) { if (cfg == null) { - throw apiError(HttpStatusCode.NotImplemented, "API not implemented", TalerErrorCode.END) + throw notImplemented() } } \ No newline at end of file diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt @@ -327,6 +327,12 @@ class WireGatewayApiTest { } } + // POST /taler-wire-gateway/account/check + @Test + fun accountCheck() = serverSetup { + client.getA("/taler-wire-gateway/account/check").assertNotImplemented() + } + @Test fun noApi() = serverSetup("mini.conf") { client.get("/taler-wire-gateway/config").assertNotImplemented()