diff options
author | MS <ms@taler.net> | 2023-09-20 23:49:21 +0200 |
---|---|---|
committer | MS <ms@taler.net> | 2023-09-20 23:49:21 +0200 |
commit | 10190a79c63b97ca9e2c241775050e18c06f07ea (patch) | |
tree | 6f232a33ce1911108c1cf7b417f46f7fb6bde41a | |
parent | b14953d651990c952bcb66095a0c0e3512b38b03 (diff) | |
download | libeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.tar.gz libeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.tar.bz2 libeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.zip |
Bank: implementing /admin/add-incoming.
6 files changed, 198 insertions, 19 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt index 6b558441..ebc827bd 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -399,7 +399,7 @@ class Database(private val dbConfig: String) { NO_CREDITOR, NO_DEBTOR, SUCCESS, - CONFLICT + CONFLICT // balance insufficient } fun bankTransactionCreate( tx: BankInternalTransaction @@ -438,6 +438,28 @@ class Database(private val dbConfig: String) { } } + /** + * Only checks if a bank transaction with the given subject + * exists. That's only used in the /admin/add-incoming, to + * prevent a public key from being reused. + * + * Returns the row ID if found, null otherwise. + */ + fun bankTransactionCheckExists(subject: String): Long? { + reconnect() + val stmt = prepare(""" + SELECT bank_transaction_id + FROM bank_account_transactions + WHERE subject = ?; + """) + stmt.setString(1, subject) + val res = stmt.executeQuery() + res.use { + if (!it.next()) return null + return it.getLong("bank_transaction_id") + } + } + // Get the bank transaction whose row ID is rowId fun bankTransactionGetFromInternalId(rowId: Long): BankAccountTransaction? { reconnect() diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt index b0e9e46c..179f4212 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -227,6 +227,6 @@ val webApp: Application.() -> Unit = { this.transactionsHandlers() this.talerWebHandlers() this.talerIntegrationHandlers() - // this.talerWireGatewayHandlers() + this.talerWireGatewayHandlers() } }
\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt index 665691dc..02d980d0 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt @@ -77,6 +77,12 @@ fun Routing.talerIntegrationHandlers() { ) } val dbSuccess: Boolean = if (!op.selectionDone) { + // Check if reserve pub. was used in _another_ withdrawal. + if (db.bankTransactionCheckExists(req.reserve_pub) != null) + throw conflict( + "Reserve pub. already used", + TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT + ) val exchangePayto = req.selected_exchange ?: (db.configGet("suggested_exchange") ?: throw internalServerError("Suggested exchange not found") diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt new file mode 100644 index 00000000..672d9bdb --- /dev/null +++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt @@ -0,0 +1,96 @@ +/* + * 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 Wire Gateway API handlers. + +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.getNowUs + +fun Routing.talerWireGatewayHandlers() { + get("/accounts/{USERNAME}/taler-wire-gateway/config") { + val internalCurrency = db.configGet("internal_currency") + ?: throw internalServerError("Could not find bank own currency.") + call.respond(TWGConfigResponse(currency = internalCurrency)) + return@get + } + get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") { + return@get + } + post("/accounts/{USERNAME}/taler-wire-gateway/transfer") { + return@post + } + post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") { + val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized() + if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) throw forbidden() + val req = call.receive<AddIncomingRequest>() + val amount = parseTalerAmount(req.amount) + val internalCurrency = db.configGet("internal_currency") + ?: throw internalServerError("Bank didn't find own currency.") + if (amount.currency != internalCurrency) + throw badRequest( + "Currency mismatch", + TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH + ) + if (db.bankTransactionCheckExists(req.reserve_pub) != null) + throw conflict( + "Reserve pub. already used", + TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT + ) + val walletAccount = db.bankAccountGetFromInternalPayto(req.debit_account) + ?: throw notFound( + "debit_account not found", + TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT + ) + val exchangeAccount = db.bankAccountGetFromOwnerId(c.expectRowId()) + ?: throw internalServerError("exchange bank account not found, despite it's a customer") + val txTimestamp = getNowUs() + val op = BankInternalTransaction( + debtorAccountId = walletAccount.expectRowId(), + amount = amount, + creditorAccountId = exchangeAccount.expectRowId(), + transactionDate = txTimestamp, + subject = req.reserve_pub + ) + val res = db.bankTransactionCreate(op) + /** + * Other possible errors are highly unlikely, because of the + * previous checks on the existence of the involved bank accounts. + */ + if (res == Database.BankTransactionResult.CONFLICT) + throw conflict( + "Insufficient balance", + TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT + ) + val rowId = db.bankTransactionCheckExists(req.reserve_pub) + ?: throw internalServerError("Could not find the just inserted bank transaction") + call.respond( + AddIncomingResponse( + row_id = rowId, + timestamp = txTimestamp + )) + return@post + } +} + diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt b/bank/src/main/kotlin/tech/libeufin/bank/types.kt index 8daaf8ce..6311e3e9 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt @@ -492,8 +492,37 @@ data class BankWithdrawalOperationPostRequest( val selected_exchange: String? = null // Use suggested exchange if that's missing. ) +/** + * Response to the wallet after it selects the exchange + * and the reserve pub. + */ @Serializable data class BankWithdrawalOperationPostResponse( val transfer_done: Boolean, val confirm_transfer_url: String? = null +) + +/** + * Request to an /admin/add-incoming request from + * the Taler Wire Gateway API. + */ +@Serializable +data class AddIncomingRequest( + val amount: String, + val reserve_pub: String, + val debit_account: String +) + +// Response to /admin/add-incoming +@Serializable +data class AddIncomingResponse( + val timestamp: Long, + val row_id: Long +) + +@Serializable +data class TWGConfigResponse( + val name: String = "taler-wire-gateway", + val version: String = "0:0:0:", + val currency: String )
\ No newline at end of file diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt index 06e4cbe4..7f5c407b 100644 --- a/bank/src/test/kotlin/TalerApiTest.kt +++ b/bank/src/test/kotlin/TalerApiTest.kt @@ -27,6 +27,49 @@ class TalerApiTest { hasDebt = false, maxDebt = TalerAmount(10, 1, "KUDOS") ) + val bankAccountBar = BankAccount( + internalPaytoUri = "BAR-IBAN-ABC", + lastNexusFetchRowId = 1L, + owningCustomerId = 2L, + hasDebt = false, + maxDebt = TalerAmount(10, 1, "KUDOS") + ) + val customerBar = Customer( + login = "bar", + passwordHash = "hash", + name = "Bar", + phone = "+00", + email = "foo@b.ar", + cashoutPayto = "payto://external-IBAN", + cashoutCurrency = "KUDOS" + ) + @Test + fun addIncoming() { + val db = initDb() + assert(db.customerCreate(customerFoo) != null) + assert(db.bankAccountCreate(bankAccountFoo)) + assert(db.customerCreate(customerBar) != null) + assert(db.bankAccountCreate(bankAccountBar)) + assert(db.bankAccountSetMaxDebt( + 2L, + TalerAmount(1000, 0) + )) + testApplication { + application(webApp) + client.post("/accounts/foo/taler-wire-gateway/admin/add-incoming") { + expectSuccess = true + contentType(ContentType.Application.Json) + basicAuth("foo", "pw") + setBody(""" + {"amount": "KUDOS:44", + "reserve_pub": "RESERVE-PUB-TEST", + "debit_account": "BAR-IBAN-ABC" + } + """.trimIndent()) + } + } + + } // Selecting withdrawal details from the Integrtion API endpoint. @Test fun intSelect() { @@ -136,23 +179,6 @@ class TalerApiTest { @Test fun withdrawalConfirmation() { val db = initDb() - val bankAccountBar = BankAccount( - internalPaytoUri = "BAR-IBAN-ABC", - lastNexusFetchRowId = 1L, - owningCustomerId = 2L, - hasDebt = false, - maxDebt = TalerAmount(10, 1, "KUDOS") - ) - val customerBar = Customer( - login = "bar", - passwordHash = "hash", - name = "Bar", - phone = "+00", - email = "foo@b.ar", - cashoutPayto = "payto://external-IBAN", - cashoutCurrency = "KUDOS" - ) - // Creating Foo as the wallet owner and Bar as the exchange. assert(db.customerCreate(customerFoo) != null) assert(db.bankAccountCreate(bankAccountFoo)) |