[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: Starting the Taler API for the SPA.
From: |
gnunet |
Subject: |
[libeufin] branch master updated: Starting the Taler API for the SPA. |
Date: |
Tue, 19 Sep 2023 13:57:39 +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 26097f20 Starting the Taler API for the SPA.
26097f20 is described below
commit 26097f20f8003b981a6d231fcd91592e818323d0
Author: MS <ms@taler.net>
AuthorDate: Tue Sep 19 13:56:46 2023 +0200
Starting the Taler API for the SPA.
Introducing helpers to check if a balance is enough
before initiating the withdrawal.
---
.../src/main/kotlin/tech/libeufin/bank/Database.kt | 3 +-
bank/src/main/kotlin/tech/libeufin/bank/Main.kt | 3 +-
bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 80 +++++++++++++++++++++-
.../kotlin/tech/libeufin/bank/talerWebHandlers.kt | 55 +++++++++++++++
.../kotlin/tech/libeufin/bank/tokenHandlers.kt | 19 +++++
bank/src/main/kotlin/tech/libeufin/bank/types.kt | 12 +++-
bank/src/test/kotlin/AmountTest.kt | 49 +++++++++++++
7 files changed, 216 insertions(+), 5 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 34689a47..8b45e146 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -30,7 +30,8 @@ import kotlin.math.abs
private const val DB_CTR_LIMIT = 1000000
-fun Customer.expectRowId(): Long = this.dbRowId ?: throw
internalServerError("Cutsomer '$login' had no DB row ID")
+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.")
class Database(private val dbConfig: String) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 33333597..cf5b48a9 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -50,6 +50,7 @@ val logger: Logger =
LoggerFactory.getLogger("tech.libeufin.bank")
val db = Database(System.getProperty("BANK_DB_CONNECTION_STRING"))
const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
val TOKEN_DEFAULT_DURATION_US = Duration.ofDays(1L).seconds * 1000000
+const val FRACTION_BASE = 100000000
/**
@@ -224,7 +225,7 @@ val webApp: Application.() -> Unit = {
this.accountsMgmtHandlers()
this.tokenHandlers()
this.transactionsHandlers()
- // this.talerHandlers()
+ this.talerWebHandlers()
// this.walletIntegrationHandlers()
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 258c1f96..1431ff4f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -230,7 +230,7 @@ fun parseTalerAmount(
// Fraction is at most 8 digits, so it's always < than MAX_INT.
val fraction: Int = match.destructured.component3().run {
var frac = 0
- var power = 100000000
+ var power = FRACTION_BASE
if (this.isNotEmpty())
// Skips the dot and processes the fractional chars.
this.substring(1).forEach { chr ->
@@ -257,4 +257,82 @@ fun parseTalerAmount(
)
}
+private fun normalizeAmount(amt: TalerAmount): TalerAmount {
+ if (amt.frac > FRACTION_BASE) {
+ val normalValue = amt.value + (amt.frac / FRACTION_BASE)
+ val normalFrac = amt.frac % FRACTION_BASE
+ return TalerAmount(
+ value = normalValue,
+ frac = normalFrac,
+ maybeCurrency = amt.currency
+ )
+ }
+ return amt
+}
+
+
+// Adds two amounts and returns the normalized version.
+private fun amountAdd(first: TalerAmount, second: TalerAmount): TalerAmount {
+ if (first.currency != second.currency)
+ throw badRequest(
+ "Currency mismatch, balance '${first.currency}', price
'${second.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
+ val valueAdd = first.value + second.value
+ if (valueAdd < first.value)
+ throw badRequest("Amount value overflowed")
+ val fracAdd = first.frac + second.frac
+ if (fracAdd < first.frac)
+ throw badRequest("Amount fraction overflowed")
+ return normalizeAmount(TalerAmount(
+ value = valueAdd,
+ frac = fracAdd,
+ maybeCurrency = first.currency
+ ))
+}
+
+/**
+ * Checks whether the balance could cover the due amount. Returns true
+ * when it does, false otherwise. Note: this function is only a checker,
+ * meaning that no actual business state should change after it runs.
+ * The place where business states change is in the SQL that's loaded in
+ * the database.
+ */
+fun isBalanceEnough(
+ balance: TalerAmount,
+ due: TalerAmount,
+ maxDebt: TalerAmount,
+ hasBalanceDebt: Boolean
+): Boolean {
+ val normalMaxDebt = normalizeAmount(maxDebt) // Very unlikely to be needed.
+ if (hasBalanceDebt) {
+ val chargedBalance = amountAdd(balance, due)
+ if (chargedBalance.value > normalMaxDebt.value) return false // max
debt surpassed
+ if (
+ (chargedBalance.value == normalMaxDebt.value) &&
+ (chargedBalance.frac > maxDebt.frac)
+ )
+ return false
+ return true
+ }
+ /**
+ * Balance doesn't have debt, but it MIGHT get one. The following
+ * block calculates how much debt the balance would get, should a
+ * subtraction of 'due' occur.
+ */
+ if (balance.currency != due.currency)
+ throw badRequest(
+ "Currency mismatch, balance '${balance.currency}', due
'${due.currency}'",
+ TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
+ )
+ val valueDiff = if (balance.value < due.value) due.value - balance.value
else 0L
+ val fracDiff = if (balance.frac < due.frac) due.frac - balance.frac else 0
+ // Getting the normalized version of such diff.
+ val normalDiff = normalizeAmount(TalerAmount(valueDiff, fracDiff,
balance.currency))
+ // Failing if the normalized diff surpasses the max debt.
+ if (normalDiff.value > normalMaxDebt.value) return false
+ if ((normalDiff.value == normalMaxDebt.value) &&
+ (normalDiff.frac > normalMaxDebt.frac)) return false
+ return true
+}
fun getBankCurrency(): String = db.configGet("internal_currency") ?: throw
internalServerError("Bank lacks currency")
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
new file mode 100644
index 00000000..6bb27374
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 all the Taler handlers that do NOT
+ * communicate with wallets, therefore any handler that serves
+ * to SPAs or CLI HTTP clients.
+ */
+
+package tech.libeufin.bank
+
+import io.ktor.server.application.*
+import io.ktor.server.request.*
+import io.ktor.server.routing.*
+
+fun Routing.talerWebHandlers() {
+ post("/accounts/{USERNAME}/withdrawals") {
+ val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+ // Admin not allowed to withdraw in the name of customers:
+ val accountName = call.expectUriComponent("USERNAME")
+ if (c.login != accountName)
+ throw unauthorized("User ${c.login} not allowed to withdraw for
account '${accountName}'")
+ val req = call.receive<BankAccountCreateWithdrawalRequest>()
+ // Checking that the user has enough funds.
+ val b = db.bankAccountGetFromOwnerId(c.expectRowId())
+ ?: throw internalServerError("Customer '${c.login}' lacks bank
account.")
+
+ throw NotImplementedError()
+ }
+ get("/accounts/{USERNAME}/withdrawals/{W_ID}") {
+ throw NotImplementedError()
+ }
+ post("/accounts/{USERNAME}/withdrawals/abort") {
+ throw NotImplementedError()
+ }
+ post("/accounts/{USERNAME}/withdrawals/confirm") {
+ throw NotImplementedError()
+ }
+}
+
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
index 0b665069..803c0f36 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/tokenHandlers.kt
@@ -1,3 +1,22 @@
+/*
+ * 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/>
+ */
+
package tech.libeufin.bank
import io.ktor.server.application.*
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index fd65d244..92567634 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -364,7 +364,8 @@ data class BankAccountTransactionCreate(
val amount: String
)
-// GET /transactions/T_ID
+/* History element, either from GET /transactions/T_ID
+ or from GET /transactions */
@Serializable
data class BankAccountTransactionInfo(
val creditor_payto_uri: String,
@@ -376,7 +377,14 @@ data class BankAccountTransactionInfo(
val date: Long
)
+// Response type for histories, namely GET /transactions
@Serializable
data class BankAccountTransactionsResponse(
val transactions: MutableList<BankAccountTransactionInfo>
-)
\ No newline at end of file
+)
+
+// Taler withdrawal request.
+@Serializable
+data class BankAccountCreateWithdrawalRequest(
+ val amount: String
+)
diff --git a/bank/src/test/kotlin/AmountTest.kt
b/bank/src/test/kotlin/AmountTest.kt
index efdede5d..4368d9c1 100644
--- a/bank/src/test/kotlin/AmountTest.kt
+++ b/bank/src/test/kotlin/AmountTest.kt
@@ -21,9 +21,58 @@
import org.junit.Test
import tech.libeufin.bank.FracDigits
import tech.libeufin.bank.TalerAmount
+import tech.libeufin.bank.isBalanceEnough
import tech.libeufin.bank.parseTalerAmount
class AmountTest {
+ @Test
+ fun amountAdditionTest() {
+ // Balance enough, assert for true
+ assert(isBalanceEnough(
+ balance = TalerAmount(10, 0, "KUDOS"),
+ due = TalerAmount(8, 0, "KUDOS"),
+ hasBalanceDebt = false,
+ maxDebt = TalerAmount(100, 0, "KUDOS")
+ ))
+ // Balance still sufficient, thanks for big enough debt permission.
Assert true.
+ assert(isBalanceEnough(
+ balance = TalerAmount(10, 0, "KUDOS"),
+ due = TalerAmount(80, 0, "KUDOS"),
+ hasBalanceDebt = false,
+ maxDebt = TalerAmount(100, 0, "KUDOS")
+ ))
+ // Balance not enough, max debt cannot cover, asserting for false.
+ assert(!isBalanceEnough(
+ balance = TalerAmount(10, 0, "KUDOS"),
+ due = TalerAmount(80, 0, "KUDOS"),
+ hasBalanceDebt = true,
+ maxDebt = TalerAmount(50, 0, "KUDOS")
+ ))
+ // Balance becomes enough, due to a larger max debt, asserting for
true.
+ assert(isBalanceEnough(
+ balance = TalerAmount(10, 0, "KUDOS"),
+ due = TalerAmount(80, 0, "KUDOS"),
+ hasBalanceDebt = false,
+ maxDebt = TalerAmount(70, 0, "KUDOS")
+ ))
+ // Max debt not enough for the smallest fraction, asserting for false
+ assert(!isBalanceEnough(
+ balance = TalerAmount(0, 0, "KUDOS"),
+ due = TalerAmount(0, 2, "KUDOS"),
+ hasBalanceDebt = false,
+ maxDebt = TalerAmount(0, 1, "KUDOS")
+ ))
+ // Same as above, but already in debt.
+ assert(!isBalanceEnough(
+ balance = TalerAmount(0, 1, "KUDOS"),
+ due = TalerAmount(0, 1, "KUDOS"),
+ hasBalanceDebt = true,
+ maxDebt = TalerAmount(0, 1, "KUDOS")
+ ))
+
+
+ }
+
/* Testing that currency is fetched from the config
and set in the TalerAmount dedicated field. */
@Test
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: Starting the Taler API for the SPA.,
gnunet <=