gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Implementing TWG /history/incoming.


From: gnunet
Subject: [libeufin] branch master updated: Implementing TWG /history/incoming.
Date: Thu, 21 Sep 2023 09:03:15 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 36f3e24e Implementing TWG /history/incoming.
36f3e24e is described below

commit 36f3e24e88b91903b5d9e821ca88d6fc6c29888a
Author: MS <ms@taler.net>
AuthorDate: Thu Sep 21 09:02:52 2023 +0200

    Implementing TWG /history/incoming.
---
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 49 +++++++++++++++++-----
 bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 39 ++++++++++++++++-
 .../tech/libeufin/bank/talerWireGatewayHandlers.kt | 30 +++++++++++++
 .../tech/libeufin/bank/transactionsHandlers.kt     | 26 ++----------
 bank/src/main/kotlin/tech/libeufin/bank/types.kt   | 24 ++++++++++-
 bank/src/test/kotlin/DatabaseTest.kt               | 10 +++--
 bank/src/test/kotlin/LibeuFinApiTest.kt            |  2 +-
 bank/src/test/kotlin/TalerApiTest.kt               | 41 ++++++++++++++++--
 8 files changed, 178 insertions(+), 43 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index ebc827bd..1ceb2763 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -33,6 +33,8 @@ private const val DB_CTR_LIMIT = 1000000
 fun Customer.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Cutsomer '$login' had no DB row ID.")
 fun BankAccount.expectBalance(): TalerAmount = this.balance ?: throw 
internalServerError("Bank account '${this.internalPaytoUri}' lacks balance.")
 fun BankAccount.expectRowId(): Long = this.bankAccountId ?: throw 
internalServerError("Bank account '${this.internalPaytoUri}' lacks database row 
ID.")
+fun BankAccountTransaction.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Bank account transaction (${this.subject}) lacks database 
row ID.")
+
 
 
 class Database(private val dbConfig: String) {
@@ -510,6 +512,15 @@ class Database(private val dbConfig: String) {
             )
         }
     }
+
+    /**
+     * The following function returns the list of transactions, according
+     * to the history parameters.  The parameters take at least the 'start'
+     * and 'delta' values, and _optionally_ the payment direction.  At the
+     * moment, only the TWG uses the direction, to provide the /incoming
+     * and /outgoing endpoints.
+     */
+    // Helper type to collect the history parameters.
     private data class HistoryParams(
         val cmpOp: String, // < or >
         val orderBy: String // ASC or DESC
@@ -518,6 +529,7 @@ class Database(private val dbConfig: String) {
         start: Long,
         delta: Long,
         bankAccountId: Long,
+        withDirection: TransactionDirection? = null
     ): List<BankAccountTransaction> {
         reconnect()
         val ops = if (delta < 0)
@@ -536,22 +548,45 @@ class Database(private val dbConfig: String) {
               ,account_servicer_reference
               ,payment_information_id
               ,end_to_end_id
-              ,direction
+              ${if (withDirection != null) "" else ",direction"}
               ,bank_account_id
               ,bank_transaction_id
             FROM bank_account_transactions
-               WHERE bank_transaction_id ${ops.cmpOp} ? AND bank_account_id=?
+               WHERE bank_transaction_id ${ops.cmpOp} ? 
+              AND bank_account_id=?
+              ${if (withDirection != null) "AND direction=?::direction_enum" 
else ""}
             ORDER BY bank_transaction_id ${ops.orderBy}
             LIMIT ?
         """)
         stmt.setLong(1, start)
         stmt.setLong(2, bankAccountId)
-        stmt.setLong(3, abs(delta))
+        /**
+         * The LIMIT parameter index might change, according to
+         * the presence of the direction filter.
+         */
+        val limitParamIndex = if (withDirection != null) {
+            stmt.setString(3, withDirection.name)
+            4
+        }
+        else
+            3
+        stmt.setLong(limitParamIndex, abs(delta))
         val rs = stmt.executeQuery()
         rs.use {
             val ret = mutableListOf<BankAccountTransaction>()
             if (!it.next()) return ret
             do {
+                val direction = if (withDirection == null) {
+                    it.getString("direction").run {
+                        when (this) {
+                            "credit" -> TransactionDirection.credit
+                            "debit" -> TransactionDirection.debit
+                            else -> throw internalServerError("Wrong direction 
in transaction: $this")
+                        }
+                    }
+                }
+                else
+                    withDirection
                 ret.add(
                     BankAccountTransaction(
                         creditorPaytoUri = it.getString("creditor_payto_uri"),
@@ -564,13 +599,7 @@ class Database(private val dbConfig: String) {
                         ),
                         accountServicerReference = 
it.getString("account_servicer_reference"),
                         endToEndId = it.getString("end_to_end_id"),
-                        direction = it.getString("direction").run {
-                            when(this) {
-                                "credit" -> TransactionDirection.credit
-                                "debit" -> TransactionDirection.debit
-                                else -> throw internalServerError("Wrong 
direction in transaction: $this")
-                            }
-                        },
+                        direction = direction,
                         bankAccountId = it.getLong("bank_account_id"),
                         paymentInformationId = 
it.getString("payment_information_id"),
                         subject = it.getString("subject"),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index a71cf992..240b454d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -20,7 +20,10 @@
 package tech.libeufin.bank
 
 import io.ktor.http.*
+import io.ktor.http.cio.*
 import io.ktor.server.application.*
+import io.ktor.server.plugins.*
+import io.ktor.server.request.*
 import io.ktor.server.util.*
 import net.taler.common.errorcodes.TalerErrorCode
 import net.taler.wallet.crypto.Base32Crockford
@@ -51,7 +54,6 @@ fun ApplicationCall.getAuthToken(): String? {
     return null // Not a Bearer token case.
 }
 
-
 /**
  * Performs the HTTP basic authentication.  Returns the
  * authenticated customer on success, or null otherwise.
@@ -77,7 +79,10 @@ fun doBasicAuth(encodedCredentials: String): Customer? {
         )
     val login = userAndPassSplit[0]
     val plainPassword = userAndPassSplit[1]
-    val maybeCustomer = db.customerGetFromLogin(login) ?: return null
+    val maybeCustomer = db.customerGetFromLogin(login) ?: throw notFound(
+        "User not found",
+        TalerErrorCode.TALER_EC_END // FIXME: define EC.
+    )
     if (!CryptoUtil.checkpw(plainPassword, maybeCustomer.passwordHash)) return 
null
     return maybeCustomer
 }
@@ -372,6 +377,7 @@ fun getTalerWithdrawUri(baseUrl: String, woId: String) =
         this.appendPathSegments(pathSegments)
     }
 
+// Builds a withdrawal confirm URL.
 fun getWithdrawalConfirmUrl(
     baseUrl: String,
     wopId: String,
@@ -411,3 +417,32 @@ fun getWithdrawal(opIdParam: String): 
TalerWithdrawalOperation {
         )
     return op
 }
+
+data class HistoryParams(
+    val delta: Long,
+    val start: Long
+)
+/**
+ * Extracts the query parameters from "history-like" endpoints,
+ * providing the defaults according to the API specification.
+ */
+fun getHistoryParams(req: ApplicationRequest): HistoryParams {
+    val deltaParam: String = req.queryParameters["delta"] ?: throw 
MissingRequestParameterException(parameterName = "delta")
+    val delta: Long = try {
+        deltaParam.toLong()
+    } catch (e: Exception) {
+        logger.error(e.message)
+        throw badRequest("Param 'delta' not a number")
+    }
+    // Note: minimum 'start' is zero, as database IDs start from 1.
+    val start: Long = when (val param = req.queryParameters["start"]) {
+        null -> if (delta >= 0) 0L else Long.MAX_VALUE
+        else -> try {
+            param.toLong()
+        } catch (e: Exception) {
+            logger.error(e.message)
+            throw badRequest("Param 'start' not a number")
+        }
+    }
+    return HistoryParams(delta = delta, start = start)
+}
diff --git 
a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
index 672d9bdb..dc460928 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
@@ -21,6 +21,7 @@
 
 package tech.libeufin.bank
 
+import io.ktor.http.*
 import io.ktor.server.application.*
 import io.ktor.server.request.*
 import io.ktor.server.response.*
@@ -36,6 +37,35 @@ fun Routing.talerWireGatewayHandlers() {
         return@get
     }
     get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
+        val c = call.myAuth(TokenScope.readonly) ?: throw unauthorized()
+        if (!call.getResourceName("USERNAME").canI(c, withAdmin = true)) throw 
forbidden()
+        val params = getHistoryParams(call.request)
+        val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
+            ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
+        if (!bankAccount.isTalerExchange) throw forbidden("History is not 
related to a Taler exchange.")
+        val bankAccountId = bankAccount.expectRowId()
+
+        val history: List<BankAccountTransaction> = 
db.bankTransactionGetHistory(
+            start = params.start,
+            delta = params.delta,
+            bankAccountId = bankAccountId,
+            withDirection = TransactionDirection.credit
+        )
+        if (history.isEmpty()) {
+            call.respond(HttpStatusCode.NoContent)
+            return@get
+        }
+        val resp = IncomingHistory(credit_account = 
bankAccount.internalPaytoUri)
+        history.forEach {
+            resp.incoming_transactions.add(IncomingReserveTransaction(
+                row_id = it.expectRowId(),
+                amount = it.amount.toString(),
+                date = it.transactionDate,
+                debit_account = it.debtorPaytoUri,
+                reserve_pub = it.subject
+            ))
+        }
+        call.respond(resp)
         return@get
     }
     post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
index aafd6682..8b2f1a7d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/transactionsHandlers.kt
@@ -17,32 +17,14 @@ fun Routing.transactionsHandlers() {
         val resourceName = call.expectUriComponent("USERNAME")
         if (c.login != resourceName && c.login != "admin") throw forbidden()
         // Collecting params.
-        val deltaParam: String = call.request.queryParameters["delta"] ?: 
throw MissingRequestParameterException(parameterName = "delta")
-        val delta: Long = try {
-            deltaParam.toLong()
-        } catch (e: Exception) {
-            logger.error(e.message)
-            throw badRequest("Param 'delta' not a number")
-        }
-        // Note: minimum 'start' is zero, as database IDs start from 1.
-        val start: Long = when (val param = 
call.request.queryParameters["start"]) {
-            null -> if (delta >= 0) 0L else Long.MAX_VALUE
-            else -> try {
-                param.toLong()
-            } catch (e: Exception) {
-                logger.error(e.message)
-                throw badRequest("Param 'start' not a number")
-            }
-        }
-        logger.info("Param long_poll_ms not supported")
+        val historyParams = getHistoryParams(call.request)
         // Making the query.
         val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
             ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
-        val bankAccountId = bankAccount.bankAccountId
-            ?: throw internalServerError("Bank account lacks row ID.")
+        val bankAccountId = bankAccount.expectRowId()
         val history: List<BankAccountTransaction> = 
db.bankTransactionGetHistory(
-            start = start,
-            delta = delta,
+            start = historyParams.start,
+            delta = historyParams.delta,
             bankAccountId = bankAccountId
         )
         val res = BankAccountTransactionsResponse(transactions = 
mutableListOf())
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index 6311e3e9..bd64def0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -410,8 +410,11 @@ data class BankAccountGetWithdrawalResponse(
 
 typealias ResourceName = String
 
-
-// Checks if the input Customer has the rights over ResourceName
+/**
+ * Checks if the input Customer has the rights over ResourceName.
+ * FIXME: myAuth() gives null on failures, but this gives false.
+ * Should they return the same, for consistency?
+ */
 fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
     if (c.login == this) return true
     if (c.login == "admin" && withAdmin) return true
@@ -525,4 +528,21 @@ data class TWGConfigResponse(
     val name: String = "taler-wire-gateway",
     val version: String = "0:0:0:",
     val currency: String
+)
+
+// Response of a TWG /history/incoming call.
+@Serializable
+data class IncomingHistory(
+    val incoming_transactions: MutableList<IncomingReserveTransaction> = 
mutableListOf(),
+    val credit_account: String // Receiver's Payto URI.
+)
+// TWG's incoming payment record.
+@Serializable
+data class IncomingReserveTransaction(
+    val type: String = "RESERVE",
+    val row_id: Long, // DB row ID of the payment.
+    val date: Long, // microseconds timestamp.
+    val amount: String,
+    val debit_account: String, // Payto of the sender.
+    val reserve_pub: String
 )
\ No newline at end of file
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index 35b0b9d5..f38e9559 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -25,10 +25,14 @@ import java.util.Random
 import java.util.UUID
 
 // Foo pays Bar with custom subject.
-fun genTx(subject: String = "test"): BankInternalTransaction =
+fun genTx(
+    subject: String = "test",
+    creditorId: Long = 2,
+    debtorId: Long = 1
+): BankInternalTransaction =
     BankInternalTransaction(
-        creditorAccountId = 2,
-        debtorAccountId = 1,
+        creditorAccountId = creditorId,
+        debtorAccountId = debtorId,
         subject = subject,
         amount = TalerAmount( 10, 0, "KUDOS"),
         accountServicerReference = "acct-svcr-ref",
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index cdbb2c0c..048947b6 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -197,7 +197,7 @@ class LibeuFinApiTest {
                 basicAuth("not", "not")
                 expectSuccess = false
             }
-            assert(shouldNot.status == HttpStatusCode.Unauthorized)
+            assert(shouldNot.status == HttpStatusCode.NotFound)
         }
     }
     /**
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index 7f5c407b..7617467e 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -32,17 +32,52 @@ class TalerApiTest {
         lastNexusFetchRowId = 1L,
         owningCustomerId = 2L,
         hasDebt = false,
-        maxDebt = TalerAmount(10, 1, "KUDOS")
+        maxDebt = TalerAmount(10, 1, "KUDOS"),
+        isTalerExchange = true
     )
     val customerBar = Customer(
         login = "bar",
-        passwordHash = "hash",
+        passwordHash = CryptoUtil.hashpw("secret"),
         name = "Bar",
         phone = "+00",
         email = "foo@b.ar",
         cashoutPayto = "payto://external-IBAN",
         cashoutCurrency = "KUDOS"
     )
+    // Testing the /history/incoming call from the TWG API.
+    @Test
+    fun historyIncoming() {
+        val db = initDb()
+        assert(db.customerCreate(customerFoo) != null)
+        assert(db.bankAccountCreate(bankAccountFoo))
+        assert(db.customerCreate(customerBar) != null)
+        assert(db.bankAccountCreate(bankAccountBar))
+        // Give Foo reasonable debt allowance:
+        assert(db.bankAccountSetMaxDebt(
+            1L,
+            TalerAmount(1000, 0)
+        ))
+        // Foo pays Bar (the exchange) twice.
+        assert(db.bankTransactionCreate(genTx("withdrawal 1")) == 
Database.BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx("withdrawal 2")) == 
Database.BankTransactionResult.SUCCESS)
+        // Bar pays Foo once, but that should not appear in the result.
+        assert(
+            db.bankTransactionCreate(genTx("payout", creditorId = 1, debtorId 
= 2)) ==
+                    Database.BankTransactionResult.SUCCESS
+            )
+        // Bar expects two entries in the incoming history
+        testApplication {
+            application(webApp)
+            val resp = 
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=5") {
+                basicAuth("bar", "secret")
+                expectSuccess = true
+            }
+            val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText())
+            assert(j.incoming_transactions.size == 2)
+        }
+    }
+
+    // Testing the /admin/add-incoming call from the TWG API.
     @Test
     fun addIncoming() {
         val db = initDb()
@@ -50,6 +85,7 @@ class TalerApiTest {
         assert(db.bankAccountCreate(bankAccountFoo))
         assert(db.customerCreate(customerBar) != null)
         assert(db.bankAccountCreate(bankAccountBar))
+        // Give Bar reasonable debt allowance:
         assert(db.bankAccountSetMaxDebt(
             2L,
             TalerAmount(1000, 0)
@@ -68,7 +104,6 @@ class TalerApiTest {
                 """.trimIndent())
             }
         }
-
     }
     // Selecting withdrawal details from the Integrtion API endpoint.
     @Test

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]