commit d2497386c0a8edc3ebcdd782ca92e555d82e189d
parent cdb6712523aacb3e6f0a44d005405862e09fdc0e
Author: Antoine A <>
Date: Mon, 20 Jan 2025 17:15:28 +0100
common: add wire gateway account check
Diffstat:
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()