aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMS <ms@taler.net>2023-09-20 23:49:21 +0200
committerMS <ms@taler.net>2023-09-20 23:49:21 +0200
commit10190a79c63b97ca9e2c241775050e18c06f07ea (patch)
tree6f232a33ce1911108c1cf7b417f46f7fb6bde41a
parentb14953d651990c952bcb66095a0c0e3512b38b03 (diff)
downloadlibeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.tar.gz
libeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.tar.bz2
libeufin-10190a79c63b97ca9e2c241775050e18c06f07ea.zip
Bank: implementing /admin/add-incoming.
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Database.kt24
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/talerIntegrationHandlers.kt6
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt96
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/types.kt29
-rw-r--r--bank/src/test/kotlin/TalerApiTest.kt60
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))