gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (44f2a6c1 -> 3bfd7161)


From: gnunet
Subject: [libeufin] branch master updated (44f2a6c1 -> 3bfd7161)
Date: Mon, 02 Oct 2023 12:44:49 +0200

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

ms pushed a change to branch master
in repository libeufin.

    from 44f2a6c1 Implementing GET /accounts
     new 0c8f9082 Reserve pub. filter.
     new 3bfd7161 Implementing accounts deletion.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../main/kotlin/tech/libeufin/bank/BankMessages.kt | 21 ++++++-
 .../tech/libeufin/bank/CorebankApiHandlers.kt      | 49 ++++++++++++----
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 30 +++++++---
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    |  5 ++
 .../tech/libeufin/bank/WireGatewayApiHandlers.kt   |  6 +-
 bank/src/test/kotlin/Common.kt                     |  1 +
 bank/src/test/kotlin/DatabaseTest.kt               | 42 +++++++++++---
 bank/src/test/kotlin/LibeuFinApiTest.kt            | 67 +++++++++++++++++++++-
 bank/src/test/kotlin/TalerApiTest.kt               | 13 +++--
 contrib/wallet-core                                |  2 +-
 database-versioning/libeufin-bank-0001.sql         |  1 +
 database-versioning/procedures.sql                 | 51 ++++++++++++++++
 util/src/main/kotlin/Encoding.kt                   |  4 ++
 util/src/main/kotlin/strings.kt                    |  7 +++
 14 files changed, 261 insertions(+), 38 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
index 787a99a5..75869426 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -19,7 +19,6 @@
 
 package tech.libeufin.bank
 
-import CreditDebitIndicator
 import io.ktor.http.*
 import io.ktor.server.application.*
 import kotlinx.serialization.Serializable
@@ -475,6 +474,16 @@ fun ResourceName.canI(c: Customer, withAdmin: Boolean = 
true): Boolean {
 fun ApplicationCall.getResourceName(param: String): ResourceName =
     this.expectUriComponent(param)
 
+/**
+ * This type communicates the result of deleting an account
+ * from the database.
+ */
+enum class CustomerDeletionResult {
+    SUCCESS,
+    CUSTOMER_NOT_FOUND,
+    BALANCE_NOT_ZERO
+}
+
 /**
  * This type communicates the result of a database operation
  * to confirm one withdrawal operation.
@@ -496,6 +505,16 @@ enum class WithdrawalConfirmationResult {
     CONFLICT
 }
 
+/**
+ * Communicates the result of creating a bank transaction in the database.
+ */
+enum class BankTransactionResult {
+    NO_CREDITOR,
+    NO_DEBTOR,
+    SUCCESS,
+    CONFLICT // balance insufficient
+}
+
 // GET /config response from the Taler Integration API.
 @Serializable
 data class TalerIntegrationConfigResponse(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index b9a53fab..489b71e0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -185,13 +185,10 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
                 transactionDate = Instant.now()
             )
             when (db.bankTransactionCreate(adminPaysBonus)) {
-                Database.BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Bonus impossible: creditor not found, despite its recent 
creation.")
-
-                Database.BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
-
-                Database.BankTransactionResult.CONFLICT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
-
-                Database.BankTransactionResult.SUCCESS -> {/* continue the 
execution */
+                BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Bonus impossible: creditor not found, despite its recent 
creation.")
+                BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
+                BankTransactionResult.CONFLICT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
+                BankTransactionResult.SUCCESS -> {/* continue the execution */
                 }
             }
         }
@@ -257,6 +254,36 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         return@get
     }
+    delete("/accounts/{USERNAME}") {
+        val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
+        val resourceName = call.expectUriComponent("USERNAME")
+        // Checking rights.
+        if (c.login != "admin" && ctx.restrictAccountDeletion)
+            throw forbidden("Only admin allowed.")
+        if (!resourceName.canI(c, withAdmin = true))
+            throw forbidden("Insufficient rights on this account.")
+        // Not deleting reserved names.
+        if (resourceName == "bank" || resourceName == "admin")
+            throw forbidden("Cannot delete reserved accounts.")
+        val res = db.customerDeleteIfBalanceIsZero(resourceName)
+        when (res) {
+            CustomerDeletionResult.CUSTOMER_NOT_FOUND ->
+                throw notFound(
+                    "Customer '$resourceName' not found",
+                    talerEc = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
+                    )
+            CustomerDeletionResult.BALANCE_NOT_ZERO ->
+                throw LibeufinBankException(
+                    httpStatus = HttpStatusCode.PreconditionFailed,
+                    talerError = TalerError(
+                        hint = "Balance is not zero.",
+                        code = TalerErrorCode.TALER_EC_NONE.code // FIXME: 
need EC.
+                    )
+                )
+            CustomerDeletionResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
+        }
+        return@delete
+    }
 
     post("/accounts/{USERNAME}/withdrawals") {
         val c = call.authenticateBankRequest(db, TokenScope.readwrite)
@@ -435,13 +462,13 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         val res = db.bankTransactionCreate(dbInstructions)
         when (res) {
-            Database.BankTransactionResult.CONFLICT -> throw conflict(
+            BankTransactionResult.CONFLICT -> throw conflict(
                 "Insufficient funds",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
-            Database.BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Creditor not found despite previous checks.")
-            Database.BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Debtor not found despite the request was authenticated.")
-            Database.BankTransactionResult.SUCCESS -> 
call.respond(HttpStatusCode.OK)
+            BankTransactionResult.NO_CREDITOR -> throw 
internalServerError("Creditor not found despite previous checks.")
+            BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Debtor not found despite the request was authenticated.")
+            BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
         }
         return@post
     }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 717239de..a18f5bd2 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -21,7 +21,6 @@
 package tech.libeufin.bank
 
 import org.postgresql.jdbc.PgConnection
-import org.postgresql.util.PSQLException
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import tech.libeufin.util.getJdbcConnectionFromPg
@@ -231,6 +230,27 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
         }
     }
 
+    /**
+     * Deletes a customer (including its bank account row) from
+     * the database.  The bank account gets deleted by the cascade.
+     */
+    fun customerDeleteIfBalanceIsZero(login: String): CustomerDeletionResult {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              out_nx_customer,
+              out_balance_not_zero
+              FROM customer_delete(?);
+        """)
+        stmt.setString(1, login)
+        stmt.executeQuery().apply {
+            if (!this.next()) throw internalServerError("Deletion returned 
nothing.")
+            if (this.getBoolean("out_nx_customer")) return 
CustomerDeletionResult.CUSTOMER_NOT_FOUND
+            if (this.getBoolean("out_balance_not_zero")) return 
CustomerDeletionResult.BALANCE_NOT_ZERO
+            return CustomerDeletionResult.SUCCESS
+        }
+    }
+
     // Mostly used to get customers out of bearer tokens.
     fun customerGetFromRowId(customer_id: Long): Customer? {
         reconnect()
@@ -304,6 +324,7 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
             )
         }
     }
+
     // Possibly more "customerGetFrom*()" to come.
 
     // BEARER TOKEN
@@ -566,12 +587,7 @@ class Database(private val dbConfig: String, private val 
bankCurrency: String) {
     }
 
     // BANK ACCOUNT TRANSACTIONS
-    enum class BankTransactionResult {
-        NO_CREDITOR,
-        NO_DEBTOR,
-        SUCCESS,
-        CONFLICT // balance insufficient
-    }
+
     fun bankTransactionCreate(
         tx: BankInternalTransaction
     ): BankTransactionResult {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 29dd7e9d..2db84c3c 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -82,6 +82,10 @@ data class BankApplicationContext(
      * Restrict account registration to the administrator.
      */
     val restrictRegistration: Boolean,
+    /**
+     * Restrict account deletion to the administrator.
+     */
+    val restrictAccountDeletion: Boolean,
     /**
      * Cashout currency, if cashouts are supported.
      */
@@ -480,6 +484,7 @@ fun readBankApplicationContextFromConfig(cfg: TalerConfig): 
BankApplicationConte
         suggestedWithdrawalExchange = cfg.lookupValueString("libeufin-bank", 
"suggested_withdrawal_exchange"),
         defaultAdminDebtLimit = cfg.requireValueAmount("libeufin-bank", 
"default_admin_debt_limit", currency),
         spaCaptchaURL = cfg.lookupValueString("libeufin-bank", 
"spa_captcha_url"),
+        restrictAccountDeletion = 
cfg.lookupValueBooleanDefault("libeufin-bank", "restrict_account_deletion", 
true)
     )
 }
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index 95307b31..71aa2039 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -123,12 +123,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
             exchangeBankAccountId = exchangeBankAccount.expectRowId(),
             timestamp = transferTimestamp
         )
-        if (dbRes.txResult == Database.BankTransactionResult.CONFLICT)
+        if (dbRes.txResult == BankTransactionResult.CONFLICT)
             throw conflict(
                 "Insufficient balance for exchange",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
             )
-        if (dbRes.txResult == Database.BankTransactionResult.NO_CREDITOR)
+        if (dbRes.txResult == BankTransactionResult.NO_CREDITOR)
             throw notFound(
                 "Creditor account was not found",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
@@ -179,7 +179,7 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
          * Other possible errors are highly unlikely, because of the
          * previous checks on the existence of the involved bank accounts.
          */
-        if (res == Database.BankTransactionResult.CONFLICT)
+        if (res == BankTransactionResult.CONFLICT)
             throw conflict(
                 "Insufficient balance",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
diff --git a/bank/src/test/kotlin/Common.kt b/bank/src/test/kotlin/Common.kt
index 425cff3a..8d9721ab 100644
--- a/bank/src/test/kotlin/Common.kt
+++ b/bank/src/test/kotlin/Common.kt
@@ -47,6 +47,7 @@ fun getTestContext(
         registrationBonus = null,
         suggestedWithdrawalExchange = suggestedExchange,
         spaCaptchaURL = null,
+        restrictAccountDeletion = true
     )
 }
 
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index b0a9128a..89415b3a 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -18,8 +18,10 @@
  */
 
 import org.junit.Test
+import org.postgresql.jdbc.PgConnection
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
 import java.time.Instant
 import java.util.Random
 import java.util.UUID
@@ -125,7 +127,7 @@ class DatabaseTest {
             exchangeBankAccountId = 1L,
             timestamp = Instant.now()
         )
-        assert(res.txResult == Database.BankTransactionResult.SUCCESS)
+        assert(res.txResult == BankTransactionResult.SUCCESS)
     }
 
     @Test
@@ -167,13 +169,13 @@ class DatabaseTest {
             TalerAmount(50, 0, currency)
         )
         val firstSpending = db.bankTransactionCreate(fooPaysBar) // Foo pays 
Bar and goes debit.
-        assert(firstSpending == Database.BankTransactionResult.SUCCESS)
+        assert(firstSpending == BankTransactionResult.SUCCESS)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Foo: credit -> debit
         assert(fooAccount?.hasDebt == true) // Asserting Foo's debit.
         // Now checking that more spending doesn't get Foo out of debit.
         val secondSpending = db.bankTransactionCreate(fooPaysBar)
-        assert(secondSpending == Database.BankTransactionResult.SUCCESS)
+        assert(secondSpending == BankTransactionResult.SUCCESS)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Checking that Foo's debit is two times the paid amount
         // Foo: debit -> debit
@@ -200,14 +202,14 @@ class DatabaseTest {
             transactionDate = Instant.now()
         )
         val barPays = db.bankTransactionCreate(barPaysFoo)
-        assert(barPays == Database.BankTransactionResult.SUCCESS)
+        assert(barPays == BankTransactionResult.SUCCESS)
         barAccount = db.bankAccountGetFromOwnerId(barId)
         val barBalanceTen: TalerAmount? = barAccount?.balance
         // Bar: credit -> credit
         assert(barAccount?.hasDebt == false && barBalanceTen?.value == 10L && 
barBalanceTen.frac == 0)
         // Bar pays again to let Foo return in credit.
         val barPaysAgain = db.bankTransactionCreate(barPaysFoo)
-        assert(barPaysAgain == Database.BankTransactionResult.SUCCESS)
+        assert(barPaysAgain == BankTransactionResult.SUCCESS)
         // Refreshing the two accounts.
         barAccount = db.bankAccountGetFromOwnerId(barId)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
@@ -218,7 +220,7 @@ class DatabaseTest {
         assert(barAccount?.balance?.equals(TalerAmount(0, 0, "KUDOS")) == true)
         // Bringing Bar to debit.
         val barPaysMore = db.bankTransactionCreate(barPaysFoo)
-        assert(barPaysMore == Database.BankTransactionResult.SUCCESS)
+        assert(barPaysMore == BankTransactionResult.SUCCESS)
         barAccount = db.bankAccountGetFromOwnerId(barId)
         fooAccount = db.bankAccountGetFromOwnerId(fooId)
         // Bar: credit -> debit
@@ -226,6 +228,32 @@ class DatabaseTest {
         assert(fooAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == 
true)
         assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == 
true)
     }
+
+    // Testing customer(+bank account) deletion logic.
+    @Test
+    fun customerDeletionTest() {
+        val db = initDb()
+        // asserting false, as foo doesn't exist yet.
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.CUSTOMER_NOT_FOUND)
+        // Creating foo.
+        db.customerCreate(customerFoo).apply {
+            assert(this != null)
+            assert(db.bankAccountCreate(bankAccountFoo) != null)
+        }
+        // foo has zero balance, deletion should succeed.
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.SUCCESS)
+        val db2 = initDb()
+        // Creating foo again, artificially setting its balance != zero.
+        db2.customerCreate(customerFoo).apply {
+            assert(this != null)
+            db2.bankAccountCreate(bankAccountFoo).apply {
+                assert(this != null)
+                val conn = 
DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+                conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET 
balance.frac = 1 WHERE bank_account_id = $this")
+            }
+        }
+        assert(db.customerDeleteIfBalanceIsZero("foo") == 
CustomerDeletionResult.BALANCE_NOT_ZERO)
+    }
     @Test
     fun customerCreationTest() {
         val db = initDb()
@@ -346,7 +374,7 @@ class DatabaseTest {
             paymentInformationId = "pmtinfid",
             transactionDate = Instant.now()
         )
-        ) == Database.BankTransactionResult.SUCCESS)
+        ) == BankTransactionResult.SUCCESS)
         // Confirming the cash-out
         assert(db.cashoutConfirm(op.cashoutUuid, 1L, 1L))
         // Checking the confirmation took place.
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 107d277c..d9a5a3c9 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -2,13 +2,16 @@ import io.ktor.client.plugins.*
 import io.ktor.client.request.*
 import io.ktor.client.statement.*
 import io.ktor.http.*
+import io.ktor.server.engine.*
 import io.ktor.server.testing.*
 import kotlinx.serialization.decodeFromString
 import kotlinx.serialization.json.Json
 import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
+import org.postgresql.jdbc.PgConnection
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
+import java.sql.DriverManager
 import java.time.Duration
 import java.time.Instant
 import java.time.temporal.ChronoUnit
@@ -517,6 +520,66 @@ class LibeuFinApiTest {
         }
     }
 
+    /**
+     * Tests DELETE /accounts/foo
+     */
+    @Test
+    fun deleteAccount() {
+        val db = initDb()
+        val ctx = getTestContext()
+        val adminCustomer = Customer(
+            "admin",
+            CryptoUtil.hashpw("pass"),
+            "CFO"
+        )
+        db.customerCreate(adminCustomer)
+        testApplication {
+            application {
+                corebankWebApp(db, ctx)
+            }
+            // account to delete doesn't exist.
+            client.delete("/accounts/foo") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.NotFound)
+            }
+            // account to delete is reserved.
+            client.delete("/accounts/admin") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.Forbidden)
+            }
+            // successful deletion
+            db.customerCreate(customerFoo).apply {
+                assert(this != null)
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
+            }
+            client.delete("/accounts/foo") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.NoContent)
+            }
+            // fail to delete, due to a non-zero balance.
+            db.customerCreate(customerBar).apply {
+                assert(this != null)
+                db.bankAccountCreate(genBankAccount(this!!)).apply {
+                    assert(this != null)
+                    val conn = 
DriverManager.getConnection("jdbc:postgresql:///libeufincheck").unwrap(PgConnection::class.java)
+                    conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET 
balance.val = 1 WHERE bank_account_id = $this")
+                }
+            }
+            client.delete("/accounts/bar") {
+                basicAuth("admin", "pass")
+                expectSuccess = false
+            }.apply {
+                assert(this.status == HttpStatusCode.PreconditionFailed)
+            }
+        }
+    }
+
     /**
      * Tests the GET /accounts endpoint.
      */
@@ -544,12 +607,12 @@ class LibeuFinApiTest {
             // foo account
             db.customerCreate(customerFoo).apply {
                 assert(this != null)
-                db.bankAccountCreate(genBankAccount(this!!)) != null
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
             }
             // bar account
             db.customerCreate(customerBar).apply {
                 assert(this != null)
-                db.bankAccountCreate(genBankAccount(this!!)) != null
+                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
             }
             // Two users registered, requesting all of them.
             client.get("/accounts") {
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index 158b8734..9be3de8b 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,6 +4,7 @@ import io.ktor.client.statement.*
 import io.ktor.http.*
 import io.ktor.server.testing.*
 import kotlinx.serialization.json.Json
+import net.taler.wallet.crypto.Base32Crockford
 import org.junit.Test
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
@@ -151,14 +152,14 @@ class TalerApiTest {
         // Foo pays Bar (the exchange) twice.
         val reservePubOne = 
"5ZFS98S1K4Y083W95GVZK638TSRE44RABVASB3AFA3R95VCW17V0"
         val reservePubTwo = 
"TFBT5NEVT8D2GETZ4DRF7C69XZHKHJ15296HRGB1R5ARNK0SP8A0"
-        assert(db.bankTransactionCreate(genTx(reservePubOne)) == 
Database.BankTransactionResult.SUCCESS)
-        assert(db.bankTransactionCreate(genTx(reservePubTwo)) == 
Database.BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx(reservePubOne)) == 
BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx(reservePubTwo)) == 
BankTransactionResult.SUCCESS)
         // Should not show up in the taler wire gateway API history
-        assert(db.bankTransactionCreate(genTx("bogus foobar")) == 
Database.BankTransactionResult.SUCCESS)
+        assert(db.bankTransactionCreate(genTx("bogus foobar")) == 
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
+                    BankTransactionResult.SUCCESS
         )
         // Bar expects two entries in the incoming history
         testApplication {
@@ -172,9 +173,9 @@ class TalerApiTest {
             val j: IncomingHistory = Json.decodeFromString(resp.bodyAsText())
             assert(j.incoming_transactions.size == 2)
             // Testing ranges.
-            val mockReservePub = "X".repeat(52)
+            val mockReservePub = Base32Crockford.encode(ByteArray(32))
             for (i in 1..400)
-                assert(db.bankTransactionCreate(genTx(mockReservePub)) == 
Database.BankTransactionResult.SUCCESS)
+                assert(db.bankTransactionCreate(genTx(mockReservePub)) == 
BankTransactionResult.SUCCESS)
             // forward range:
             val range = 
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=10&start=30")
 {
                 basicAuth("bar", "secret")
diff --git a/contrib/wallet-core b/contrib/wallet-core
index 1b5df6f0..c5a3cd4c 160000
--- a/contrib/wallet-core
+++ b/contrib/wallet-core
@@ -1 +1 @@
-Subproject commit 1b5df6f037a8d9e93a96e4c83f7e628023e4cd90
+Subproject commit c5a3cd4c50676c49fa6c67cbdeb609101c38e764
diff --git a/database-versioning/libeufin-bank-0001.sql 
b/database-versioning/libeufin-bank-0001.sql
index acd4f174..94475c99 100644
--- a/database-versioning/libeufin-bank-0001.sql
+++ b/database-versioning/libeufin-bank-0001.sql
@@ -91,6 +91,7 @@ CREATE TABLE IF NOT EXISTS bank_accounts
   ,internal_payto_uri TEXT NOT NULL UNIQUE
   ,owning_customer_id BIGINT NOT NULL UNIQUE -- UNIQUE enforces 1-1 map with 
customers
     REFERENCES customers(customer_id)
+    ON DELETE CASCADE
   ,is_public BOOLEAN DEFAULT FALSE NOT NULL -- privacy by default
   ,is_taler_exchange BOOLEAN DEFAULT FALSE NOT NULL
   ,last_nexus_fetch_row_id BIGINT
diff --git a/database-versioning/procedures.sql 
b/database-versioning/procedures.sql
index 652219ba..ad36cc2a 100644
--- a/database-versioning/procedures.sql
+++ b/database-versioning/procedures.sql
@@ -86,6 +86,57 @@ END $$;
 COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT)
   IS 'Update or insert configuration values';
 
+CREATE OR REPLACE FUNCTION customer_delete(
+  IN in_login TEXT,
+  OUT out_nx_customer BOOLEAN,
+  OUT out_balance_not_zero BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+my_customer_id BIGINT;
+my_balance_val INT8;
+my_balance_frac INT4;
+BEGIN
+-- check if login exists
+SELECT customer_id
+  INTO my_customer_id
+  FROM customers
+  WHERE login = in_login;
+IF NOT FOUND
+THEN
+  out_nx_customer=TRUE;
+  RETURN;
+END IF;
+out_nx_customer=FALSE;
+
+-- get the balance
+SELECT
+  (balance).val as balance_val,
+  (balance).frac as balance_frac
+  INTO
+    my_balance_val,
+    my_balance_frac
+  FROM bank_accounts
+  WHERE owning_customer_id = my_customer_id;
+IF NOT FOUND
+THEN
+  RAISE EXCEPTION 'Invariant failed: customer lacks bank account';
+END IF;
+-- check that balance is zero.
+IF my_balance_val != 0 OR my_balance_frac != 0
+THEN
+  out_balance_not_zero=TRUE;
+  RETURN;
+END IF;
+out_balance_not_zero=FALSE;
+
+-- actual deletion
+DELETE FROM customers WHERE login = in_login;
+END $$;
+COMMENT ON FUNCTION customer_delete(TEXT)
+  IS 'Deletes a customer (and its bank account via cascade) if the balance is 
zero';
+
 CREATE OR REPLACE FUNCTION taler_transfer(
   IN in_request_uid TEXT,
   IN in_wtid TEXT,
diff --git a/util/src/main/kotlin/Encoding.kt b/util/src/main/kotlin/Encoding.kt
index db0c269e..4f3dcabf 100644
--- a/util/src/main/kotlin/Encoding.kt
+++ b/util/src/main/kotlin/Encoding.kt
@@ -53,6 +53,10 @@ object Base32Crockford {
         return sb.toString()
     }
 
+    /**
+     * Decodes the input to its binary representation, throws
+     * net.taler.wallet.crypto.EncodingException on invalid encodings.
+     */
     fun decode(encoded: String, out: ByteArrayOutputStream) {
         val size = encoded.length
         var bitpos = 0
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
index 0490b209..2fd858f9 100644
--- a/util/src/main/kotlin/strings.kt
+++ b/util/src/main/kotlin/strings.kt
@@ -20,6 +20,7 @@
 package tech.libeufin.util
 
 import logger
+import net.taler.wallet.crypto.Base32Crockford
 import java.math.BigInteger
 import java.util.*
 
@@ -159,6 +160,12 @@ fun hasWopidPlaceholder(captchaUrl: String): Boolean {
 fun extractReservePubFromSubject(rawSubject: String): String? {
     val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
     val result = re.find(rawSubject.replace("[\n]+".toRegex(), "")) ?: return 
null
+    try {
+        Base32Crockford.decode(result.value)
+    } catch (e: Exception) {
+        logger.debug("Not containing a reserve pub: $rawSubject")
+        return null
+    }
     return result.value.uppercase()
 }
 

-- 
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]