gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (8f015037 -> 5c671b88)


From: gnunet
Subject: [libeufin] branch master updated (8f015037 -> 5c671b88)
Date: Tue, 17 Oct 2023 14:52:44 +0200

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

antoine pushed a change to branch master
in repository libeufin.

    from 8f015037 Fix procedures.sql
     new 22f1484c Improve transactions endpoint
     new 5086e251 Improve and test accounts management endpoints
     new 5c671b88 Cleanup

The 3 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:
 .../kotlin/tech/libeufin/bank/Authentication.kt    | 140 +++-
 .../tech/libeufin/bank/BankIntegrationApi.kt       |   8 +-
 .../main/kotlin/tech/libeufin/bank/CoreBankApi.kt  | 487 +++++++-------
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 362 +++++------
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    |   2 +-
 .../main/kotlin/tech/libeufin/bank/TalerMessage.kt |  24 -
 .../kotlin/tech/libeufin/bank/WireGatewayApi.kt    |  28 +-
 bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 131 +---
 bank/src/test/kotlin/CoreBankApiTest.kt            | 719 ++++++++-------------
 bank/src/test/kotlin/DatabaseTest.kt               |  47 +-
 bank/src/test/kotlin/helpers.kt                    |  19 +-
 database-versioning/procedures.sql                 |  76 ++-
 util/src/main/kotlin/CryptoUtil.kt                 |  32 +-
 13 files changed, 940 insertions(+), 1135 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
index 36031e97..2b63d523 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
@@ -3,8 +3,36 @@ package tech.libeufin.bank
 import io.ktor.http.*
 import io.ktor.server.application.*
 import net.taler.common.errorcodes.TalerErrorCode
-import tech.libeufin.util.getAuthorizationDetails
-import tech.libeufin.util.getAuthorizationRawHeader
+import tech.libeufin.util.*
+import net.taler.wallet.crypto.Base32Crockford
+import java.time.Instant
+
+/** Authenticate admin */
+suspend fun ApplicationCall.authAdmin(db: Database, scope: TokenScope) {
+    // TODO when all endpoints use this function we can use an optimized 
database request that only query the customer login
+    val login = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad 
login")
+    if (login != "admin") {
+        throw unauthorized("Only administrator allowed")
+    }
+}
+
+/** Authenticate and check access rights */
+suspend fun ApplicationCall.authCheck(db: Database, scope: TokenScope, 
withAdmin: Boolean = true, requireAdmin: Boolean = false): Pair<String, 
Boolean> {
+    // TODO when all endpoints use this function we can use an optimized 
database request that only query the customer login
+    val authLogin = authenticateBankRequest(db, scope) ?: throw 
unauthorized("Bad login")
+    val login = accountLogin()
+    if (requireAdmin && authLogin != "admin") {
+        if (authLogin != "admin") {
+            throw unauthorized("Only administrator allowed")
+        }
+    } else {
+        val hasRight = authLogin == login || (withAdmin && authLogin == 
"admin");
+        if (!hasRight) {
+            throw unauthorized("Customer $authLogin have no right on $login 
account")
+        }
+    }
+    return Pair(login, authLogin == "admin")
+}
 
 /**
  * This function tries to authenticate the call according
@@ -13,9 +41,9 @@ import tech.libeufin.util.getAuthorizationRawHeader
  *
  * requiredScope can be either "readonly" or "readwrite".
  *
- * Returns the authenticated customer, or null if they failed.
+ * Returns the authenticated customer login, or null if they failed.
  */
-suspend fun ApplicationCall.authenticateBankRequest(db: Database, 
requiredScope: TokenScope): Customer? {
+private suspend fun ApplicationCall.authenticateBankRequest(db: Database, 
requiredScope: TokenScope): String? {
     // Extracting the Authorization header.
     val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
         "Authorization header not found.",
@@ -28,12 +56,104 @@ suspend fun ApplicationCall.authenticateBankRequest(db: 
Database, requiredScope:
     return when (authDetails.scheme) {
         "Basic" -> doBasicAuth(db, authDetails.content)
         "Bearer" -> doTokenAuth(db, authDetails.content, requiredScope)
-        else -> throw LibeufinBankException(
-            httpStatus = HttpStatusCode.Unauthorized,
-            talerError = TalerError(
-                code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
-                hint = "Authorization method wrong or not supported."
-            )
+        else -> throw unauthorized("Authorization method wrong or not 
supported.")
+    }
+}
+
+// Get the auth token (stripped of the bearer-token:-prefix)
+// IF the call was authenticated with it.
+fun ApplicationCall.getAuthToken(): String? {
+    val h = getAuthorizationRawHeader(this.request) ?: return null
+    val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
+        "Authorization header is malformed.", 
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+    )
+    if (authDetails.scheme == "Bearer") return 
splitBearerToken(authDetails.content) ?: throw throw badRequest(
+        "Authorization header is malformed (could not strip the prefix from 
Bearer token).",
+        TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+    )
+    return null // Not a Bearer token case.
+}
+
+
+/**
+ * Performs the HTTP basic authentication.  Returns the
+ * authenticated customer login on success, or null otherwise.
+ */
+private suspend fun doBasicAuth(db: Database, encodedCredentials: String): 
String? {
+    val plainUserAndPass = String(base64ToBytes(encodedCredentials), 
Charsets.UTF_8) // :-separated
+    val userAndPassSplit = plainUserAndPass.split(
+        ":",
+        /**
+         * this parameter allows colons to occur in passwords.
+         * Without this, passwords that have colons would be split
+         * and become meaningless.
+         */
+        limit = 2
+    )
+    if (userAndPassSplit.size != 2) throw LibeufinBankException(
+        httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
+            code = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
+            "Malformed Basic auth credentials found in the Authorization 
header."
+        )
+    )
+    val (login, plainPassword) = userAndPassSplit
+    val passwordHash = db.customerPasswordHashFromLogin(login) ?: throw 
unauthorized()
+    if (!CryptoUtil.checkpw(plainPassword, passwordHash)) return null
+    return login
+}
+
+/**
+ * This function takes a prefixed Bearer token, removes the
+ * secret-token:-prefix and returns it.  Returns null, if the
+ * input is invalid.
+ */
+private fun splitBearerToken(tok: String): String? {
+    val tokenSplit = tok.split(":", limit = 2)
+    if (tokenSplit.size != 2) return null
+    if (tokenSplit[0] != "secret-token") return null
+    return tokenSplit[1]
+}
+
+/* Performs the secret-token authentication.  Returns the
+ * authenticated customer login on success, null otherwise. */
+private suspend fun doTokenAuth(
+    db: Database,
+    token: String,
+    requiredScope: TokenScope,
+): String? {
+    val bareToken = splitBearerToken(token) ?: throw badRequest(
+        "Bearer token malformed",
+        talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
+    )
+    val tokenBytes = try {
+        Base32Crockford.decode(bareToken)
+    } catch (e: Exception) {
+        throw badRequest(
+            e.message, TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
         )
     }
+    val maybeToken: BearerToken? = db.bearerTokenGet(tokenBytes)
+    if (maybeToken == null) {
+        logger.error("Auth token not found")
+        return null
+    }
+    if (maybeToken.expirationTime.isBefore(Instant.now())) {
+        logger.error("Auth token is expired")
+        return null
+    }
+    if (maybeToken.scope == TokenScope.readonly && requiredScope == 
TokenScope.readwrite) {
+        logger.error("Auth token has insufficient scope")
+        return null
+    }
+    if (!maybeToken.isRefreshable && requiredScope == TokenScope.refreshable) {
+        logger.error("Could not refresh unrefreshable token")
+        return null
+    }
+    // Getting the related username.
+    return db.customerLoginFromId(maybeToken.bankCustomer) ?: throw 
LibeufinBankException(
+        httpStatus = HttpStatusCode.InternalServerError, talerError = 
TalerError(
+            code = 
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
+            hint = "Customer not found, despite token mentions it.",
+        )
+    )
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
index f607317e..56cfbccd 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -29,12 +29,10 @@ import net.taler.common.errorcodes.TalerErrorCode
 
 fun Routing.bankIntegrationApi(db: Database, ctx: BankApplicationContext) {
     get("/taler-integration/config") {
-        val internalCurrency: String = ctx.currency
         call.respond(TalerIntegrationConfigResponse(
-            currency = internalCurrency,
+            currency = ctx.currency,
             currency_specification = ctx.currencySpecification
         ))
-        return@get
     }
 
     // Note: wopid acts as an authentication token.
@@ -60,7 +58,6 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
                 confirm_transfer_url = confirmUrl
             )
         )
-        return@get
     }
     post("/taler-integration/withdrawal-operation/{wopid}") {
         val wopid = call.expectUriComponent("wopid")
@@ -73,7 +70,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
             )
         }
         val dbSuccess: Boolean = if (!op.selectionDone) { // Check if reserve 
pub. was used in _another_ withdrawal.
-            if (db.bankTransactionCheckExists(req.reserve_pub) != null) throw 
conflict(
+            if (db.bankTransactionCheckExists(req.reserve_pub)) throw conflict(
                 "Reserve pub. already used", 
TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
             )
             db.talerWithdrawalSetDetails(
@@ -96,6 +93,5 @@ fun Routing.bankIntegrationApi(db: Database, ctx: 
BankApplicationContext) {
             transfer_done = op.confirmationDone, confirm_transfer_url = 
confirmUrl
         )
         call.respond(resp)
-        return@post
     }
 }
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
index 459c662e..fb039f86 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -24,17 +24,10 @@ private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.account
  * and wire transfers should belong here.
  */
 fun Routing.accountsMgmtApi(db: Database, ctx: BankApplicationContext) {
-
     // TOKEN ENDPOINTS
     delete("/accounts/{USERNAME}/token") {
-        val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized()
+        call.authCheck(db, TokenScope.readonly)
         val token = call.getAuthToken() ?: throw badRequest("Basic auth not 
supported here.")
-        val resourceName = call.getResourceName("USERNAME")
-        /**
-         * The following check makes sure that the token belongs
-         * to the username contained in {USERNAME}.
-         */
-        if (!resourceName.canI(c, withAdmin = true)) throw forbidden()
 
         /**
          * Not sanity-checking the token, as it was used by the authentication 
already.
@@ -50,13 +43,7 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
         call.respond(HttpStatusCode.NoContent)
     }
     post("/accounts/{USERNAME}/token") {
-        val customer =
-            call.authenticateBankRequest(db, TokenScope.refreshable) ?: throw 
unauthorized("Authentication failed")
-        val endpointOwner = call.maybeUriComponent("USERNAME")
-        if (customer.login != endpointOwner) throw forbidden(
-            "User has no rights on this enpoint",
-            TalerErrorCode.TALER_EC_GENERIC_FORBIDDEN
-        )
+        val (login, _) = call.authCheck(db, TokenScope.refreshable)
         val maybeAuthToken = call.getAuthToken()
         val req = call.receive<TokenRequest>()
         /**
@@ -90,8 +77,8 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
                 throw badRequest("Bad token duration: ${e.message}")
             }
         }
-        val customerDbRow = customer.dbRowId ?: throw internalServerError(
-            "Could not get customer '${customer.login}' database row ID"
+        val customerDbRow = db.customerGetFromLogin(login)?.dbRowId ?: throw 
internalServerError(
+            "Could not get customer '$login' database row ID"
         )
         val token = BearerToken(
             bankCustomer = customerDbRow,
@@ -112,51 +99,119 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
         )
         return@post
     }
-    // ACCOUNT ENDPOINTS
-    get("/public-accounts") {
-        // no authentication here.
-        val publicAccounts = db.accountsGetPublic(ctx.currency)
-        if (publicAccounts.isEmpty()) {
-            call.respond(HttpStatusCode.NoContent)
-        } else {
-            call.respond(PublicAccountsResponse(publicAccounts))
-        }
+    // WITHDRAWAL ENDPOINTS
+    post("/accounts/{USERNAME}/withdrawals") {
+        call.authCheck(db, TokenScope.readwrite)
+        val req = call.receive<BankAccountCreateWithdrawalRequest>() // 
Checking that the user has enough funds.
+        if (req.amount.currency != ctx.currency)
+            throw badRequest("Wrong currency: ${req.amount.currency}")
+        val b = call.bankAccount(db)
+
+        // TODO balance check only in database
+        if (!isBalanceEnough(
+                balance = b.expectBalance(), due = req.amount, maxDebt = 
b.maxDebt, hasBalanceDebt = b.hasDebt
+            )
+        ) throw forbidden(
+            hint = "Insufficient funds to withdraw with Taler",
+            talerErrorCode = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
+        ) // Auth and funds passed, create the operation now!
+        val opId = UUID.randomUUID()
+        if (!db.talerWithdrawalCreate(
+                opId, b.expectRowId(), req.amount
+            )
+        ) throw internalServerError("Bank failed at creating the withdraw 
operation.")
+
+        val bankBaseUrl = call.request.getBaseUrl() ?: throw 
internalServerError("Bank could not find its own base URL")
+        call.respond(
+            BankAccountCreateWithdrawalResponse(
+                withdrawal_id = opId.toString(), taler_withdraw_uri = 
getTalerWithdrawUri(bankBaseUrl, opId.toString())
+            )
+        )
     }
-    get("/accounts") {
-        val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized()
-        if (c.login != "admin") throw forbidden("Only admin allowed.")
-        // Get optional param.
-        val maybeFilter: String? = call.request.queryParameters["filter_name"]
-        logger.debug("Filtering on '${maybeFilter}'")
-        val queryParam = if (maybeFilter != null) {
-            "%${maybeFilter}%"
-        } else "%"
-        val accounts = db.accountsGetForAdmin(queryParam)
-        if (accounts.isEmpty()) {
-            call.respond(HttpStatusCode.NoContent)
-        } else {
-            call.respond(ListBankAccountsResponse(accounts))
-        }
+    get("/withdrawals/{withdrawal_id}") {
+        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
+        call.respond(
+            BankAccountGetWithdrawalResponse(
+                amount = op.amount,
+                aborted = op.aborted,
+                confirmation_done = op.confirmationDone,
+                selection_done = op.selectionDone,
+                selected_exchange_account = op.selectedExchangePayto,
+                selected_reserve_pub = op.reservePub
+            )
+        )
     }
-    post("/accounts") { // check if only admin is allowed to create new 
accounts
-        if (ctx.restrictRegistration) {
-            val customer: Customer? = call.authenticateBankRequest(db, 
TokenScope.readwrite)
-            if (customer == null || customer.login != "admin") throw 
LibeufinBankException(
-                httpStatus = HttpStatusCode.Unauthorized,
-                talerError = TalerError(
-                    code = TalerErrorCode.TALER_EC_GENERIC_UNAUTHORIZED.code,
-                    hint = "Either 'admin' not authenticated or an ordinary 
user tried this operation."
+    post("/withdrawals/{withdrawal_id}/abort") {
+        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Idempotency:
+        if (op.aborted) {
+            call.respondText("{}", ContentType.Application.Json)
+            return@post
+        } // Op is found, it'll now fail only if previously confirmed (DB 
checks).
+        if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
+            hint = "Cannot abort confirmed withdrawal", talerEc = 
TalerErrorCode.TALER_EC_END
+        )
+        call.respondText("{}", ContentType.Application.Json)
+    }
+    post("/withdrawals/{withdrawal_id}/confirm") {
+        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Checking idempotency:
+        if (op.confirmationDone) {
+            call.respondText("{}", ContentType.Application.Json)
+            return@post
+        }
+        if (op.aborted) throw conflict(
+            hint = "Cannot confirm an aborted withdrawal", talerEc = 
TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
+        ) // Checking that reserve GOT indeed selected.
+        if (!op.selectionDone) throw LibeufinBankException(
+            httpStatus = HttpStatusCode.UnprocessableEntity, talerError = 
TalerError(
+                hint = "Cannot confirm an unselected withdrawal", code = 
TalerErrorCode.TALER_EC_END.code
+            )
+        ) // Confirmation conditions are all met, now put the operation
+        // to the selected state _and_ wire the funds to the exchange.
+        // Note: 'when' helps not to omit more result codes, should more
+        // be added.
+        when (db.talerWithdrawalConfirm(op.withdrawalUuid, Instant.now())) {
+            WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
+                throw conflict(
+                "Insufficient funds",
+                    TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
                 )
+            WithdrawalConfirmationResult.OP_NOT_FOUND ->
+                /**
+                 * Despite previous checks, the database _still_ did not
+                 * find the withdrawal operation, that's on the bank.
+                 */
+                throw internalServerError("Withdrawal operation 
(${op.withdrawalUuid}) not found")
+            WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
+                /**
+                 * That can happen because the bank did not check the exchange
+                 * exists when POST /withdrawals happened, or because the 
exchange
+                 * bank account got removed before this confirmation.
+                 */
+                throw conflict(
+                    hint = "Exchange to withdraw from not found",
+                    talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+                )
+            WithdrawalConfirmationResult.CONFLICT -> throw 
internalServerError("Bank didn't check for idempotency")
+            WithdrawalConfirmationResult.SUCCESS -> call.respondText(
+                "{}", ContentType.Application.Json
             )
+        }
+    }
+}
+
+fun Routing.coreBankAccountsMgmtApi(db: Database, ctx: BankApplicationContext) 
{
+    post("/accounts") { 
+        // check if only admin is allowed to create new accounts
+        if (ctx.restrictRegistration) {
+            call.authAdmin(db, TokenScope.readwrite)
         } // auth passed, proceed with activity.
-        val req = call.receive<RegisterAccountRequest>() // Prohibit reserved 
usernames:
-        if (req.username == "admin" || req.username == "bank") throw 
LibeufinBankException(
-            httpStatus = HttpStatusCode.Conflict,
-            talerError = TalerError(
-                code = 
TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT.code,
-                hint = "Username '${req.username}' is reserved."
-            )
-        ) // Checking idempotency.
+        val req = call.receive<RegisterAccountRequest>()
+        // Prohibit reserved usernames:
+        if (reservedAccounts.contains(req.username)) throw conflict( 
+            "Username '${req.username}' is reserved.",
+            TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
+        )  // TODO conflict or forbidden ?
+        // Checking idempotency.
         val maybeCustomerExists =
             db.customerGetFromLogin(req.username) // Can be null if previous 
call crashed before completion.
         val maybeHasBankAccount = maybeCustomerExists.run {
@@ -179,11 +234,9 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
                 call.respond(HttpStatusCode.Created)
                 return@post
             }
-            throw LibeufinBankException(
-                httpStatus = HttpStatusCode.Conflict, talerError = TalerError(
-                    code = GENERIC_UNDEFINED, // FIXME: provide appropriate EC.
-                    hint = "Idempotency check failed."
-                )
+            throw conflict(
+                "Idempotency check failed.",
+                TalerErrorCode.TALER_EC_END // FIXME: provide appropriate EC.
             )
         }
 
@@ -231,98 +284,38 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
                 BankTransactionResult.NO_DEBTOR -> throw 
internalServerError("Bonus impossible: admin not found.")
                 BankTransactionResult.BALANCE_INSUFFICIENT -> throw 
internalServerError("Bonus impossible: admin has insufficient balance.")
                 BankTransactionResult.SAME_ACCOUNT -> throw 
internalServerError("Bonus impossible: admin should not be creditor.")
-                BankTransactionResult.SUCCESS -> {/* continue the execution */
-                }
+                BankTransactionResult.SUCCESS -> { /* continue the execution 
*/ }
             }
         }
         call.respond(HttpStatusCode.Created)
-        return@post
-    }
-    get("/accounts/{USERNAME}") {
-        val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized("Login failed")
-        val resourceName = call.expectUriComponent("USERNAME")
-        if (!resourceName.canI(c, withAdmin = true)) throw forbidden()
-        val customerData = db.customerGetFromLogin(resourceName) ?: throw 
notFound(
-                "Customer '$resourceName' not found in the database.",
-                talerEc = TalerErrorCode.TALER_EC_END
-        )
-        val bankAccountData = 
db.bankAccountGetFromOwnerId(customerData.expectRowId())
-            ?: throw internalServerError("Customer '$resourceName' had no bank 
account despite they are customer.'")
-        val balance = Balance(
-            amount = bankAccountData.balance ?: throw 
internalServerError("Account '${customerData.login}' lacks balance!"),
-            credit_debit_indicator = if (bankAccountData.hasDebt) {
-                CorebankCreditDebitInfo.debit
-            } else {
-                CorebankCreditDebitInfo.credit
-            }
-        )
-        call.respond(
-            AccountData(
-                name = customerData.name,
-                balance = balance,
-                debit_threshold = bankAccountData.maxDebt,
-                payto_uri = bankAccountData.internalPaytoUri,
-                contact_data = ChallengeContactData(
-                    email = customerData.email, phone = customerData.phone
-                ),
-                cashout_payto_uri = customerData.cashoutPayto,
-            )
-        )
-        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.")
+        val (login, _) = call.authCheck(db, TokenScope.readwrite, requireAdmin 
= ctx.restrictAccountDeletion)
         // 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.
-                    )
+        if (reservedAccounts.contains(login)) throw conflict( 
+            "Cannot delete reserved accounts",
+            TalerErrorCode.TALER_EC_BANK_RESERVED_USERNAME_CONFLICT
+        ) // TODO conflict or forbidden ?
+
+        when (db.customerDeleteIfBalanceIsZero(login)) {
+            CustomerDeletionResult.CUSTOMER_NOT_FOUND -> throw notFound(
+                "Customer '$login' not found",
+                talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
+            CustomerDeletionResult.BALANCE_NOT_ZERO -> throw 
LibeufinBankException(
+                httpStatus = HttpStatusCode.PreconditionFailed, // 
PreconditionFailed or conflict ?
+                talerError = TalerError(
+                    hint = "Balance is not zero.",
+                    code = TalerErrorCode.TALER_EC_NONE.code // FIXME: need EC.
                 )
+            )
             CustomerDeletionResult.SUCCESS -> 
call.respond(HttpStatusCode.NoContent)
         }
-        return@delete
-    }
-    patch("/accounts/{USERNAME}/auth") {
-        val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
-        val accountName = call.getResourceName("USERNAME")
-        if (!accountName.canI(c, withAdmin = true)) throw forbidden()
-        val req = call.receive<AccountPasswordChange>()
-        val hashedPassword = CryptoUtil.hashpw(req.new_password)
-        if (!db.customerChangePassword(
-                accountName,
-                hashedPassword
-        )) throw notFound(
-            "Account '$accountName' not found (despite it being authenticated 
by this call)",
-            talerEc = TalerErrorCode.TALER_EC_END // FIXME: need at least 
GENERIC_NOT_FOUND.
-        )
-        call.respond(HttpStatusCode.NoContent)
-        return@patch
     }
     patch("/accounts/{USERNAME}") {
-        val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
-        val accountName = call.getResourceName("USERNAME")
-        // preventing user non-admin X trying on resource Y.
-        if (!accountName.canI(c, withAdmin = true)) throw forbidden()
+        val (login, isAdmin) = call.authCheck(db, TokenScope.readwrite)
         // admin is not allowed itself to change its own details.
-        if (accountName == "admin") throw forbidden("admin account not 
patchable")
+        if (login == "admin") throw forbidden("admin account not patchable")
         // authentication OK, go on.
         val req = call.receive<AccountReconfiguration>()
         /**
@@ -330,12 +323,12 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
          * by this operation, as it MAY differ from the one being 
authenticated.
          * This typically happens when admin did the request.
          */
-        val accountCustomer = db.customerGetFromLogin(accountName) ?: throw 
notFound(
-            "Account $accountName not found",
+        val accountCustomer = db.customerGetFromLogin(login) ?: throw notFound(
+            "Account $login not found",
             talerEc = TalerErrorCode.TALER_EC_END // FIXME, define EC.
         )
         // Check if a non-admin user tried to change their legal name
-        if ((c.login != "admin") && (req.name != null) && (req.name != 
accountCustomer.name))
+        if (!isAdmin && (req.name != null) && (req.name != 
accountCustomer.name))
             throw forbidden("non-admin user cannot change their legal name")
         // Preventing identical data to be overridden.
         val bankAccount = 
db.bankAccountGetFromOwnerId(accountCustomer.expectRowId())
@@ -343,7 +336,7 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
         if (
             (req.is_exchange == bankAccount.isTalerExchange) &&
             (req.cashout_address == accountCustomer.cashoutPayto) &&
-            (req.name == c.name) &&
+            (req.name == accountCustomer.name) &&
             (req.challenge_contact_data?.phone == accountCustomer.phone) &&
             (req.challenge_contact_data?.email == accountCustomer.email)
             ) {
@@ -370,128 +363,86 @@ fun Routing.accountsMgmtApi(db: Database, ctx: 
BankApplicationContext) {
                 throw internalServerError("Customer '${accountCustomer.login}' 
lacks bank account")
             }
         }
-        return@patch
     }
-    // WITHDRAWAL ENDPOINTS
-    post("/accounts/{USERNAME}/withdrawals") {
-        val c = call.authenticateBankRequest(db, 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.
-        if(req.amount.currency != ctx.currency)
-            throw badRequest("Wrong currency: ${req.amount.currency}")
-        val b = db.bankAccountGetFromOwnerId(c.expectRowId())
-            ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
-        if (!isBalanceEnough(
-                balance = b.expectBalance(), due = req.amount, maxDebt = 
b.maxDebt, hasBalanceDebt = b.hasDebt
-            )
-        ) throw forbidden(
-            hint = "Insufficient funds to withdraw with Taler",
-            talerErrorCode = TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
-        ) // Auth and funds passed, create the operation now!
-        val opId = UUID.randomUUID()
-        if (!db.talerWithdrawalCreate(
-                opId, b.expectRowId(), req.amount
-            )
-        ) throw internalServerError("Bank failed at creating the withdraw 
operation.")
-
-        val bankBaseUrl = call.request.getBaseUrl() ?: throw 
internalServerError("Bank could not find its own base URL")
-        call.respond(
-            BankAccountCreateWithdrawalResponse(
-                withdrawal_id = opId.toString(), taler_withdraw_uri = 
getTalerWithdrawUri(bankBaseUrl, opId.toString())
-            )
-        )
-        return@post
-    }
-    get("/withdrawals/{withdrawal_id}") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
-        call.respond(
-            BankAccountGetWithdrawalResponse(
-                amount = op.amount,
-                aborted = op.aborted,
-                confirmation_done = op.confirmationDone,
-                selection_done = op.selectionDone,
-                selected_exchange_account = op.selectedExchangePayto,
-                selected_reserve_pub = op.reservePub
-            )
+    patch("/accounts/{USERNAME}/auth") {
+        val (login, _) = call.authCheck(db, TokenScope.readwrite)
+        val req = call.receive<AccountPasswordChange>()
+        val hashedPassword = CryptoUtil.hashpw(req.new_password)
+        if (!db.customerChangePassword(
+            login,
+            hashedPassword
+        )) throw notFound(
+            "Account '$login' not found (despite it being authenticated by 
this call)",
+            talerEc = TalerErrorCode.TALER_EC_END // FIXME: need at least 
GENERIC_NOT_FOUND.
         )
-        return@get
+        call.respond(HttpStatusCode.NoContent)
     }
-    post("/withdrawals/{withdrawal_id}/abort") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Idempotency:
-        if (op.aborted) {
-            call.respondText("{}", ContentType.Application.Json)
-            return@post
-        } // Op is found, it'll now fail only if previously confirmed (DB 
checks).
-        if (!db.talerWithdrawalAbort(op.withdrawalUuid)) throw conflict(
-            hint = "Cannot abort confirmed withdrawal", talerEc = 
TalerErrorCode.TALER_EC_END
-        )
-        call.respondText("{}", ContentType.Application.Json)
-        return@post
+    get("/public-accounts") {
+        // no authentication here.
+        val publicAccounts = db.accountsGetPublic(ctx.currency)
+        if (publicAccounts.isEmpty()) {
+            call.respond(HttpStatusCode.NoContent)
+        } else {
+            call.respond(PublicAccountsResponse(publicAccounts))
+        }
     }
-    post("/withdrawals/{withdrawal_id}/confirm") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id")) 
// Checking idempotency:
-        if (op.confirmationDone) {
-            call.respondText("{}", ContentType.Application.Json)
-            return@post
+    get("/accounts") {
+        call.authAdmin(db, TokenScope.readonly)
+        // Get optional param.
+        val maybeFilter: String? = call.request.queryParameters["filter_name"]
+        logger.debug("Filtering on '${maybeFilter}'")
+        val queryParam = if (maybeFilter != null) {
+            "%${maybeFilter}%"
+        } else "%"
+        val accounts = db.accountsGetForAdmin(queryParam)
+        if (accounts.isEmpty()) {
+            call.respond(HttpStatusCode.NoContent)
+        } else {
+            call.respond(ListBankAccountsResponse(accounts))
         }
-        if (op.aborted) throw conflict(
-            hint = "Cannot confirm an aborted withdrawal", talerEc = 
TalerErrorCode.TALER_EC_BANK_CONFIRM_ABORT_CONFLICT
-        ) // Checking that reserve GOT indeed selected.
-        if (!op.selectionDone) throw LibeufinBankException(
-            httpStatus = HttpStatusCode.UnprocessableEntity, talerError = 
TalerError(
-                hint = "Cannot confirm an unselected withdrawal", code = 
TalerErrorCode.TALER_EC_END.code
-            )
-        ) // Confirmation conditions are all met, now put the operation
-        // to the selected state _and_ wire the funds to the exchange.
-        // Note: 'when' helps not to omit more result codes, should more
-        // be added.
-        when (db.talerWithdrawalConfirm(op.withdrawalUuid, Instant.now())) {
-            WithdrawalConfirmationResult.BALANCE_INSUFFICIENT ->
-                throw conflict(
-                "Insufficient funds",
-                    TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
-                )
-            WithdrawalConfirmationResult.OP_NOT_FOUND ->
-                /**
-                 * Despite previous checks, the database _still_ did not
-                 * find the withdrawal operation, that's on the bank.
-                 */
-                throw internalServerError("Withdrawal operation 
(${op.withdrawalUuid}) not found")
-
-            WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND ->
-                /**
-                 * That can happen because the bank did not check the exchange
-                 * exists when POST /withdrawals happened, or because the 
exchange
-                 * bank account got removed before this confirmation.
-                 */
-                throw conflict(
-                    hint = "Exchange to withdraw from not found",
-                    talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
-                )
-
-            WithdrawalConfirmationResult.CONFLICT -> throw 
internalServerError("Bank didn't check for idempotency")
-
-            WithdrawalConfirmationResult.SUCCESS -> call.respondText(
-                "{}", ContentType.Application.Json
+    }
+    get("/accounts/{USERNAME}") {
+        val (login, _) = call.authCheck(db, TokenScope.readonly)
+        val customerData = db.customerGetFromLogin(login) ?: throw notFound(
+                "Customer '$login' not found in the database.",
+                talerEc = TalerErrorCode.TALER_EC_END
+        )
+        val bankAccountData = 
db.bankAccountGetFromOwnerId(customerData.expectRowId())
+            ?: throw internalServerError("Customer '$login' had no bank 
account despite they are customer.'")
+        val balance = Balance(
+            amount = bankAccountData.balance ?: throw 
internalServerError("Account '${customerData.login}' lacks balance!"),
+            credit_debit_indicator = if (bankAccountData.hasDebt) {
+                CorebankCreditDebitInfo.debit
+            } else {
+                CorebankCreditDebitInfo.credit
+            }
+        )
+        call.respond(
+            AccountData(
+                name = customerData.name,
+                balance = balance,
+                debit_threshold = bankAccountData.maxDebt,
+                payto_uri = bankAccountData.internalPaytoUri,
+                contact_data = ChallengeContactData(
+                    email = customerData.email, phone = customerData.phone
+                ),
+                cashout_payto_uri = customerData.cashoutPayto,
             )
-        }
-        return@post
+        )
     }
 }
 
 fun Routing.coreBankTransactionsApi(db: Database, ctx: BankApplicationContext) 
{
     get("/accounts/{USERNAME}/transactions") {
-        val username = call.authCheck(db, TokenScope.readonly, true)
+        call.authCheck(db, TokenScope.readonly)
         val params = getHistoryParams(call.request.queryParameters)
         val bankAccount = call.bankAccount(db)
 
-        val history: List<BankAccountTransactionInfo> = 
db.bankPoolHistory(params, bankAccount.id)
+        val history: List<BankAccountTransactionInfo> = 
db.bankPoolHistory(params, bankAccount.bankAccountId!!)
         call.respond(BankAccountTransactionsResponse(history))
     }
     get("/accounts/{USERNAME}/transactions/{T_ID}") {
-        val username = call.authCheck(db, TokenScope.readonly, true)
+        call.authCheck(db, TokenScope.readonly)
         val tId = call.expectUriComponent("T_ID")
         val txRowId = try {
             tId.toLong()
@@ -505,7 +456,7 @@ fun Routing.coreBankTransactionsApi(db: Database, ctx: 
BankApplicationContext) {
             "Bank transaction '$tId' not found",
             TalerErrorCode.TALER_EC_BANK_TRANSACTION_NOT_FOUND
         )
-        if (tx.bankAccountId != bankAccount.id) // TODO not found ?
+        if (tx.bankAccountId != bankAccount.bankAccountId) // TODO not found ?
             throw unauthorized("Client has no rights over the bank 
transaction: $tId") 
 
         call.respond(
@@ -521,31 +472,23 @@ fun Routing.coreBankTransactionsApi(db: Database, ctx: 
BankApplicationContext) {
         )
     }
     post("/accounts/{USERNAME}/transactions") {
-        val username = call.authCheck(db, TokenScope.readonly, false)
+        val (login, _ ) = call.authCheck(db, TokenScope.readwrite, withAdmin = 
false)
         val tx = call.receive<BankAccountTransactionCreate>()
 
         val subject = tx.payto_uri.message ?: throw badRequest("Wire transfer 
lacks subject")
-        val amount = tx.payto_uri.amount ?: tx.amount
-        if (amount == null) throw badRequest("Wire transfer lacks amount")
+        val amount = tx.payto_uri.amount ?: tx.amount ?: throw 
badRequest("Wire transfer lacks amount")
         if (amount.currency != ctx.currency) throw badRequest(
             "Wrong currency: ${amount.currency}",
             talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
         )
-        // TODO rewrite all thos database query in a single database function
-        val debtorBankAccount = call.bankAccount(db)
-        val creditorBankAccount = 
db.bankAccountGetFromInternalPayto(tx.payto_uri)
-            ?: throw notFound(
-                "Creditor account not found",
-                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
-            )
-        val dbInstructions = BankInternalTransaction(
-            debtorAccountId = debtorBankAccount.id,
-            creditorAccountId = creditorBankAccount.expectRowId(),
+        val result = db.bankTransaction(
+            creditAccountPayto = tx.payto_uri,
+            debitAccountUsername = login,
             subject = subject,
             amount = amount,
-            transactionDate = Instant.now()
+            timestamp = Instant.now(),
         )
-        when (db.bankTransactionCreate(dbInstructions)) {
+        when (result) {
             BankTransactionResult.BALANCE_INSUFFICIENT -> throw conflict(
                 "Insufficient funds",
                 TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
@@ -554,8 +497,14 @@ fun Routing.coreBankTransactionsApi(db: Database, ctx: 
BankApplicationContext) {
                 "Wire transfer attempted with credit and debit party being the 
same bank account",
                 TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
             )
-            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.NO_DEBTOR -> throw notFound(
+                "Customer $login not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
+            BankTransactionResult.NO_CREDITOR -> throw notFound(
+                "Creditor account was not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+            )
             BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
         }
     }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index a7fbbb42..77c34b75 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -99,33 +99,35 @@ private fun <R> PgConnection.transaction(lambda: 
(PgConnection) -> R): R {
 fun initializeDatabaseTables(cfg: DatabaseConfig) {
     logger.info("doing DB initialization, sqldir ${cfg.sqlDir}, dbConnStr 
${cfg.dbConnStr}")
     pgDataSource(cfg.dbConnStr).pgConnection().use { conn ->
-        val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText()
-        conn.execSQLUpdate(sqlVersioning)
-
-        val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM 
_v.patches where patch_name = ?")
-
-        for (n in 1..9999) {
-            val numStr = n.toString().padStart(4, '0')
-            val patchName = "libeufin-bank-$numStr"
-
-            checkStmt.setString(1, patchName)
-            val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw 
Error("unable to query patches");
-            if (patchCount >= 1) {
-                logger.info("patch $patchName already applied")
-                continue
-            }
-
-            val path = File("${cfg.sqlDir}/libeufin-bank-$numStr.sql")
-            if (!path.exists()) {
-                logger.info("path $path doesn't exist anymore, stopping")
-                break
+        conn.transaction {
+            val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText()
+            conn.execSQLUpdate(sqlVersioning)
+    
+            val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM 
_v.patches where patch_name = ?")
+    
+            for (n in 1..9999) {
+                val numStr = n.toString().padStart(4, '0')
+                val patchName = "libeufin-bank-$numStr"
+    
+                checkStmt.setString(1, patchName)
+                val patchCount = checkStmt.oneOrNull { it.getInt(1) } ?: throw 
Error("unable to query patches");
+                if (patchCount >= 1) {
+                    logger.info("patch $patchName already applied")
+                    continue
+                }
+    
+                val path = File("${cfg.sqlDir}/libeufin-bank-$numStr.sql")
+                if (!path.exists()) {
+                    logger.info("path $path doesn't exist anymore, stopping")
+                    break
+                }
+                logger.info("applying patch $path")
+                val sqlPatchText = path.readText()
+                conn.execSQLUpdate(sqlPatchText)
             }
-            logger.info("applying patch $path")
-            val sqlPatchText = path.readText()
-            conn.execSQLUpdate(sqlPatchText)
-        }
-        val sqlProcedures = File("${cfg.sqlDir}/procedures.sql").readText()
-        conn.execSQLUpdate(sqlProcedures)
+            val sqlProcedures = File("${cfg.sqlDir}/procedures.sql").readText()
+            conn.execSQLUpdate(sqlProcedures)
+        } 
     }
 }
 
@@ -284,48 +286,39 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         }
     }
 
-    // Mostly used to get customers out of bearer tokens.
-    suspend fun customerGetFromRowId(customer_id: Long): Customer? = conn { 
conn ->
+    suspend fun customerChangePassword(customerName: String, passwordHash: 
String): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
-            SELECT
-              login,
-              password_hash,
-              name,
-              email,
-              phone,
-              cashout_payto,
-              cashout_currency
-            FROM customers
-            WHERE customer_id=?
+            UPDATE customers SET password_hash=? where login=?
         """)
-        stmt.setLong(1, customer_id)
+        stmt.setString(1, passwordHash)
+        stmt.setString(2, customerName)
+        stmt.executeUpdateCheck()
+    }
+
+    suspend fun customerPasswordHashFromLogin(login: String): String? = conn { 
conn ->
+        val stmt = conn.prepareStatement("""
+            SELECT password_hash FROM customers WHERE login=?
+        """)
+        stmt.setString(1, login)
         stmt.oneOrNull { 
-            Customer(
-                login = it.getString("login"),
-                passwordHash = it.getString("password_hash"),
-                name = it.getString("name"),
-                phone = it.getString("phone"),
-                email = it.getString("email"),
-                cashoutCurrency = it.getString("cashout_currency"),
-                cashoutPayto = it.getString("cashout_payto"),
-                dbRowId = customer_id
-            )
+            it.getString(1)
         }
     }
 
-    suspend fun customerChangePassword(customerName: String, passwordHash: 
String): Boolean = conn { conn ->
+    suspend fun customerLoginFromId(id: Long): String? = conn { conn ->
         val stmt = conn.prepareStatement("""
-            UPDATE customers SET password_hash=? where login=?
+            SELECT login FROM customers WHERE customer_id=?
         """)
-        stmt.setString(1, passwordHash)
-        stmt.setString(2, customerName)
-        stmt.executeUpdateCheck()
+        stmt.setLong(1, id)
+        stmt.oneOrNull { 
+            it.getString(1)
+        }
     }
 
     suspend fun customerGetFromLogin(login: String): Customer? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
-              customer_id,
+            customer_id,
               password_hash,
               name,
               email,
@@ -654,38 +647,12 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         }
     }
 
-    data class BankInfo(
-        val id: Long,
-        val isTalerExchange: Boolean,
-        val internalPaytoUri: String
-    )
-    
-    suspend fun bankAccountInfoFromCustomerLogin(login: String): BankInfo? = 
conn { conn ->
-        val stmt = conn.prepareStatement("""
-            SELECT
-                bank_account_id
-                ,is_taler_exchange
-                ,internal_payto_uri
-            FROM bank_accounts
-                JOIN customers 
-                ON customer_id=owning_customer_id
-            WHERE login=?
-        """)
-        stmt.setString(1, login)
-        stmt.oneOrNull { 
-            BankInfo(
-                id = it.getLong(1),
-                isTalerExchange = it.getBoolean(2),
-                internalPaytoUri = it.getString(3),
-            )
-        }
-    }
-
-    suspend fun bankAccountGetFromInternalPayto(internalPayto: IbanPayTo): 
BankAccount? = conn { conn ->
+    suspend fun bankAccountGetFromCustomerLogin(login: String): BankAccount? = 
conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
              bank_account_id
              ,owning_customer_id
+             ,internal_payto_uri
              ,is_public
              ,is_taler_exchange
              ,last_nexus_fetch_row_id
@@ -695,13 +662,15 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
              ,(max_debt).val AS max_debt_val
              ,(max_debt).frac AS max_debt_frac
             FROM bank_accounts
-            WHERE internal_payto_uri=?
+                JOIN customers 
+                ON customer_id=owning_customer_id
+                WHERE login=?
         """)
-        stmt.setString(1, internalPayto.canonical)
+        stmt.setString(1, login)
 
         stmt.oneOrNull {
             BankAccount(
-                internalPaytoUri = internalPayto,
+                internalPaytoUri = 
IbanPayTo(it.getString("internal_payto_uri")),
                 balance = TalerAmount(
                     it.getLong("balance_val"),
                     it.getInt("balance_frac"),
@@ -723,6 +692,110 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
 
     // BANK ACCOUNT TRANSACTIONS
 
+    private fun handleExchangeTx(
+        conn: PgConnection,
+        subject: String,
+        creditorAccountId: Long,
+        debtorAccountId: Long,
+        it: ResultSet
+    ) {
+        val metadata = TxMetadata.parse(subject)
+        if (it.getBoolean("out_creditor_is_exchange")) {
+            val rowId = it.getLong("out_credit_row_id")
+            if (metadata is IncomingTxMetadata) {
+                val stmt = conn.prepareStatement("""
+                    INSERT INTO taler_exchange_incoming 
+                        (reserve_pub, bank_transaction) 
+                    VALUES (?, ?)
+                """)
+                stmt.setBytes(1, metadata.reservePub.raw)
+                stmt.setLong(2, rowId)
+                stmt.executeUpdate()
+                conn.execSQLUpdate("NOTIFY incoming_tx, '$creditorAccountId 
$rowId'")
+            } else {
+                // TODO bounce
+                logger.warn("exchange account $creditorAccountId received a 
transaction $rowId with malformed metadata, will bounce in future version")
+            }
+        }
+        if (it.getBoolean("out_debtor_is_exchange")) {
+            val rowId = it.getLong("out_debit_row_id")
+            if (metadata is OutgoingTxMetadata) {
+                val stmt = conn.prepareStatement("""
+                    INSERT INTO taler_exchange_outgoing 
+                        (wtid, exchange_base_url, bank_transaction) 
+                    VALUES (?, ?, ?)
+                """)
+                stmt.setBytes(1, metadata.wtid.raw)
+                stmt.setString(2, metadata.exchangeBaseUrl.url)
+                stmt.setLong(3, rowId)
+                stmt.executeUpdate()
+                conn.execSQLUpdate("NOTIFY outgoing_tx, '$debtorAccountId 
$rowId'")
+            } else {
+                logger.warn("exchange account $debtorAccountId sent a 
transaction $rowId with malformed metadata")
+            }
+        }
+    }
+
+    suspend fun bankTransaction(
+        creditAccountPayto: IbanPayTo,
+        debitAccountUsername: String,
+        subject: String,
+        amount: TalerAmount,
+        timestamp: Instant,
+        accountServicerReference: String = "not used", // ISO20022
+        endToEndId: String = "not used", // ISO20022
+        paymentInformationId: String = "not used" // ISO20022
+    ): BankTransactionResult = conn { conn ->
+        conn.transaction {
+            val stmt = conn.prepareStatement("""
+                SELECT 
+                    out_creditor_not_found 
+                    ,out_debtor_not_found
+                    ,out_same_account
+                    ,out_balance_insufficient
+                    ,out_credit_bank_account_id
+                    ,out_debit_bank_account_id
+                    ,out_credit_row_id
+                    ,out_debit_row_id
+                    ,out_creditor_is_exchange 
+                    ,out_debtor_is_exchange
+                FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?,?,?)
+            """
+            )
+            stmt.setString(1, creditAccountPayto.canonical)
+            stmt.setString(2, debitAccountUsername)
+            stmt.setString(3, subject)
+            stmt.setLong(4, amount.value)
+            stmt.setInt(5, amount.frac)
+            stmt.setLong(6, timestamp.toDbMicros() ?: throw 
faultyTimestampByBank())
+            stmt.setString(7, accountServicerReference)
+            stmt.setString(8, paymentInformationId)
+            stmt.setString(9, endToEndId)
+            stmt.executeQuery().use {
+                when {
+                    !it.next() -> throw internalServerError("Bank transaction 
didn't properly return")
+                    it.getBoolean("out_creditor_not_found") -> {
+                        logger.error("No creditor account found")
+                        BankTransactionResult.NO_CREDITOR
+                    }
+                    it.getBoolean("out_debtor_not_found") -> {
+                        logger.error("No debtor account found")
+                        BankTransactionResult.NO_DEBTOR
+                    }
+                    it.getBoolean("out_same_account") -> 
BankTransactionResult.SAME_ACCOUNT
+                    it.getBoolean("out_balance_insufficient") -> {
+                        logger.error("Balance insufficient")
+                        BankTransactionResult.BALANCE_INSUFFICIENT
+                    }
+                    else -> {
+                        handleExchangeTx(conn, subject, 
it.getLong("out_credit_bank_account_id"), 
it.getLong("out_debit_bank_account_id"), it)
+                        BankTransactionResult.SUCCESS
+                    }
+                }
+            }
+        }
+    }
+
     suspend fun bankTransactionCreate(
         tx: BankInternalTransaction
     ): BankTransactionResult = conn { conn ->
@@ -766,41 +839,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
                         BankTransactionResult.BALANCE_INSUFFICIENT
                     }
                     else -> {
-                        val metadata = TxMetadata.parse(tx.subject)
-                        if (it.getBoolean("out_creditor_is_exchange")) {
-                            val rowId = it.getLong("out_credit_row_id")
-                            if (metadata is IncomingTxMetadata) {
-                                val stmt = conn.prepareStatement("""
-                                    INSERT INTO taler_exchange_incoming 
-                                        (reserve_pub, bank_transaction) 
-                                    VALUES (?, ?)
-                                """)
-                                stmt.setBytes(1, metadata.reservePub.raw)
-                                stmt.setLong(2, rowId)
-                                stmt.executeUpdate()
-                                conn.execSQLUpdate("NOTIFY incoming_tx, 
'${"${tx.creditorAccountId} $rowId"}'")
-                            } else {
-                                // TODO bounce
-                                logger.warn("exchange account 
${tx.creditorAccountId} received a transaction $rowId with malformed metadata, 
will bounce in future version")
-                            }
-                        }
-                        if (it.getBoolean("out_debtor_is_exchange")) {
-                            val rowId = it.getLong("out_debit_row_id")
-                            if (metadata is OutgoingTxMetadata) {
-                                val stmt = conn.prepareStatement("""
-                                    INSERT INTO taler_exchange_outgoing 
-                                        (wtid, exchange_base_url, 
bank_transaction) 
-                                    VALUES (?, ?, ?)
-                                """)
-                                stmt.setBytes(1, metadata.wtid.raw)
-                                stmt.setString(2, metadata.exchangeBaseUrl.url)
-                                stmt.setLong(3, rowId)
-                                stmt.executeUpdate()
-                                conn.execSQLUpdate("NOTIFY outgoing_tx, 
'${"${tx.debtorAccountId} $rowId"}'")
-                            } else {
-                                logger.warn("exchange account 
${tx.debtorAccountId} sent a transaction $rowId with malformed metadata")
-                            }
-                        }
+                        handleExchangeTx(conn, tx.subject, 
tx.creditorAccountId, tx.debtorAccountId, it)
                         BankTransactionResult.SUCCESS
                     }
                 }
@@ -815,14 +854,14 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      *
      * Returns the row ID if found, null otherwise.
      */
-    suspend fun bankTransactionCheckExists(subject: String): Long? = conn { 
conn ->
+    suspend fun bankTransactionCheckExists(subject: String): Boolean = conn { 
conn ->
         val stmt = conn.prepareStatement("""
-            SELECT bank_transaction_id
+            SELECT 1
             FROM bank_account_transactions
             WHERE subject = ?;           
         """)
         stmt.setString(1, subject)
-        stmt.oneOrNull { it.getLong("bank_transaction_id") }
+        stmt.oneOrNull {  } != null
     }
 
     // Get the bank transaction whose row ID is rowId
@@ -1050,67 +1089,6 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         }
     }
 
-    /**
-     * 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.
-     */
-    suspend fun bankTransactionGetHistory(
-        start: Long,
-        delta: Int,
-        bankAccountId: Long
-    ): List<BankAccountTransaction> = conn { conn ->
-        val (cmpOp, orderBy) = if (delta < 0) Pair("<", "DESC") else Pair(">", 
"ASC")
-        val stmt = conn.prepareStatement("""
-            SELECT 
-              creditor_payto_uri
-              ,creditor_name
-              ,debtor_payto_uri
-              ,debtor_name
-              ,subject
-              ,(amount).val AS amount_val
-              ,(amount).frac AS amount_frac
-              ,transaction_date
-              ,account_servicer_reference
-              ,payment_information_id
-              ,end_to_end_id
-              ,direction
-              ,bank_account_id
-              ,bank_transaction_id
-            FROM bank_account_transactions
-               WHERE bank_transaction_id ${cmpOp} ? 
-              AND bank_account_id=?
-            ORDER BY bank_transaction_id ${orderBy}
-            LIMIT ?
-        """)
-        stmt.setLong(1, start)
-        stmt.setLong(2, bankAccountId)
-        stmt.setInt(3, abs(delta))
-        stmt.all {
-            BankAccountTransaction(
-                creditorPaytoUri = it.getString("creditor_payto_uri"),
-                creditorName = it.getString("creditor_name"),
-                debtorPaytoUri = it.getString("debtor_payto_uri"),
-                debtorName = it.getString("debtor_name"),
-                amount = TalerAmount(
-                    it.getLong("amount_val"),
-                    it.getInt("amount_frac"),
-                    getCurrency()
-                ),
-                accountServicerReference = 
it.getString("account_servicer_reference"),
-                endToEndId = it.getString("end_to_end_id"),
-                direction = 
TransactionDirection.valueOf(it.getString("direction")),
-                bankAccountId = it.getLong("bank_account_id"),
-                paymentInformationId = it.getString("payment_information_id"),
-                subject = it.getString("subject"),
-                transactionDate = 
it.getLong("transaction_date").microsToJavaInstant() ?: throw 
faultyTimestampByBank(),
-                dbRowId = it.getLong("bank_transaction_id")
-            )
-        }
-    }
-
     // WITHDRAWALS
     suspend fun talerWithdrawalCreate(
         opUUID: UUID,
@@ -1418,20 +1396,6 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         }
     }
 
-    /**
-     * Represents the database row related to one payment
-     * that was requested by the Taler exchange.
-     */
-    data class TalerTransferFromDb(
-        val timestamp: Long,
-        val debitTxRowId: Long,
-        val requestUid: HashCode,
-        val amount: TalerAmount,
-        val exchangeBaseUrl: String,
-        val wtid: ShortHashCode,
-        val creditAccount: String
-    )
-
     /**
      * Holds the result of inserting a Taler transfer request
      * into the database.
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index c09a15b4..73835833 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -66,7 +66,6 @@ import kotlin.system.exitProcess
 
 // GLOBALS
 private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Main")
-const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
 val TOKEN_DEFAULT_DURATION: java.time.Duration = Duration.ofDays(1L)
 
 /**
@@ -260,6 +259,7 @@ fun Application.corebankWebApp(db: Database, ctx: 
BankApplicationContext) {
             return@get
         }
         this.accountsMgmtApi(db, ctx)
+        this.coreBankAccountsMgmtApi(db, ctx)
         this.coreBankTransactionsApi(db, ctx)
         this.accountsMgmtApi(db, ctx)
         this.bankIntegrationApi(db, ctx)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index 0222c78b..414d50d9 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -403,30 +403,6 @@ data class BankAccountGetWithdrawalResponse(
     val selected_exchange_account: IbanPayTo? = null
 )
 
-typealias ResourceName = String
-
-/**
- * Checks if the input Customer has the rights over ResourceName.
- */
-fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
-    if (c.login == this) return true
-    if (c.login == "admin" && withAdmin) return true
-    return false
-}
-
-/**
- * Factors out the retrieval of the resource name from
- * the URI.  The resource looked for defaults to "USERNAME"
- * as this is frequently mentioned resource along the endpoints.
- *
- * This helper is recommended because it returns a ResourceName
- * type that then offers the ".canI()" helper to check if the user
- * has the rights on the resource.
- */
-fun ApplicationCall.getResourceName(param: String): ResourceName =
-    this.expectUriComponent(param)
-
-
 // GET /config response from the Taler Integration API.
 @Serializable
 data class TalerIntegrationConfigResponse(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
index 657ce0f1..a8262a3a 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -43,7 +43,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
-        val username = call.authCheck(db, TokenScope.readwrite, true)
+        val (login, _) = call.authCheck(db, TokenScope.readwrite, withAdmin = 
false)
         val req = call.receive<TransferRequest>()
         if (req.amount.currency != ctx.currency)
             throw badRequest(
@@ -52,16 +52,16 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
             )
         val dbRes = db.talerTransferCreate(
             req = req,
-            username = username,
+            username = login,
             timestamp = Instant.now()
         )
         when (dbRes.txResult) {
             TalerTransferResult.NO_DEBITOR -> throw notFound(
-                "Customer $username not found",
-                TalerErrorCode.TALER_EC_END // FIXME: need EC.
+                "Customer $login not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
             TalerTransferResult.NOT_EXCHANGE -> throw conflict(
-                "$username is not an exchange account.",
+                "$login is not an exchange account.",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
             TalerTransferResult.NO_CREDITOR -> throw notFound(
@@ -98,22 +98,22 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
         reduce: (List<T>, String) -> Any, 
         dbLambda: suspend Database.(HistoryParams, Long) -> List<T>
     ) {
-        val username = call.authCheck(db, TokenScope.readonly, true)
+        val (login, _) = call.authCheck(db, TokenScope.readonly)
         val params = getHistoryParams(call.request.queryParameters)
         val bankAccount = call.bankAccount(db)
         
         if (!bankAccount.isTalerExchange)
             throw conflict(
-                "$username is not an exchange account.",
+                "$login is not an exchange account.",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
 
-        val items = db.dbLambda(params, bankAccount.id);
+        val items = db.dbLambda(params, bankAccount.bankAccountId!!);
     
         if (items.isEmpty()) {
             call.respond(HttpStatusCode.NoContent)
         } else {
-            call.respond(reduce(items, bankAccount.internalPaytoUri))
+            call.respond(reduce(items, bankAccount.internalPaytoUri.canonical))
         }
     }
 
@@ -126,7 +126,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
-        val username = call.authCheck(db, TokenScope.readwrite, false)
+        val (login, _) = call.authCheck(db, TokenScope.readwrite, withAdmin = 
false)
         val req = call.receive<AddIncomingRequest>()
         if (req.amount.currency != ctx.currency)
             throw badRequest(
@@ -136,16 +136,16 @@ fun Routing.wireGatewayApi(db: Database, ctx: 
BankApplicationContext) {
         val timestamp = Instant.now()
         val dbRes = db.talerAddIncomingCreate(
             req = req,
-            username = username,
+            username = login,
             timestamp = timestamp
         )
         when (dbRes.txResult) {
             TalerAddIncomingResult.NO_CREDITOR -> throw notFound(
-                "Customer $username not found",
-                TalerErrorCode.TALER_EC_END // FIXME: need EC.
+                "Customer $login not found",
+                TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
             TalerAddIncomingResult.NOT_EXCHANGE -> throw conflict(
-                "$username is not an exchange account.",
+                "$login is not an exchange account.",
                 TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
             )
             TalerAddIncomingResult.NO_DEBITOR -> throw notFound(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 8f2c56c0..47bec1c1 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -34,124 +34,46 @@ import java.time.Instant
 import java.util.*
 
 private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.helpers")
+val reservedAccounts = setOf("admin", "bank")
 
 fun ApplicationCall.expectUriComponent(componentName: String) =
     this.maybeUriComponent(componentName) ?: throw badRequest(
         hint = "No username found in the URI", talerErrorCode = 
TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
     )
 
-// Get the auth token (stripped of the bearer-token:-prefix)
-// IF the call was authenticated with it.
-fun ApplicationCall.getAuthToken(): String? {
-    val h = getAuthorizationRawHeader(this.request) ?: return null
-    val authDetails = getAuthorizationDetails(h) ?: throw badRequest(
-        "Authorization header is malformed.", 
TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
-    )
-    if (authDetails.scheme == "Bearer") return 
splitBearerToken(authDetails.content) ?: throw throw badRequest(
-        "Authorization header is malformed (could not strip the prefix from 
Bearer token).",
-        TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
-    )
-    return null // Not a Bearer token case.
-}
-
-/** Authenticate and check access rights */
-suspend fun ApplicationCall.authCheck(db: Database, scope: TokenScope, 
withAdmin: Boolean): String {
-    val authCustomer = authenticateBankRequest(db, scope) ?: throw 
unauthorized("Bad login")
-    val username = getResourceName("USERNAME")
-    if (!username.canI(authCustomer, withAdmin)) throw unauthorized("No right 
on $username account")
-    return username
-}
-
-/** Retrieve the bank account info for the selected username*/
-suspend fun ApplicationCall.bankAccount(db: Database): Database.BankInfo {
-    val username = getResourceName("USERNAME")
-    return db.bankAccountInfoFromCustomerLogin(username) ?: throw notFound(
-        hint = "Customer $username not found",
-        talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC.
-    )
-}
+typealias ResourceName = String
 
 /**
- * Performs the HTTP basic authentication.  Returns the
- * authenticated customer on success, or null otherwise.
+ * Checks if the input Customer has the rights over ResourceName.
  */
-suspend fun doBasicAuth(db: Database, encodedCredentials: String): Customer? {
-    val plainUserAndPass = String(base64ToBytes(encodedCredentials), 
Charsets.UTF_8) // :-separated
-    val userAndPassSplit = plainUserAndPass.split(
-        ":",
-        /**
-         * this parameter allows colons to occur in passwords.
-         * Without this, passwords that have colons would be split
-         * and become meaningless.
-         */
-        limit = 2
-    )
-    if (userAndPassSplit.size != 2) throw LibeufinBankException(
-        httpStatus = HttpStatusCode.BadRequest, talerError = TalerError(
-            code = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED.code,
-            "Malformed Basic auth credentials found in the Authorization 
header."
-        )
-    )
-    val login = userAndPassSplit[0]
-    val plainPassword = userAndPassSplit[1]
-    val maybeCustomer = db.customerGetFromLogin(login) ?: throw unauthorized()
-    if (!CryptoUtil.checkpw(plainPassword, maybeCustomer.passwordHash)) return 
null
-    return maybeCustomer
+fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
+    if (c.login == this) return true
+    if (c.login == "admin" && withAdmin) return true
+    return false
 }
 
 /**
- * This function takes a prefixed Bearer token, removes the
- * secret-token:-prefix and returns it.  Returns null, if the
- * input is invalid.
+ * Factors out the retrieval of the resource name from
+ * the URI.  The resource looked for defaults to "USERNAME"
+ * as this is frequently mentioned resource along the endpoints.
+ *
+ * This helper is recommended because it returns a ResourceName
+ * type that then offers the ".canI()" helper to check if the user
+ * has the rights on the resource.
  */
-private fun splitBearerToken(tok: String): String? {
-    val tokenSplit = tok.split(":", limit = 2)
-    if (tokenSplit.size != 2) return null
-    if (tokenSplit[0] != "secret-token") return null
-    return tokenSplit[1]
-}
+fun ApplicationCall.getResourceName(param: String): ResourceName =
+    this.expectUriComponent(param)
+    
 
-/* Performs the secret-token authentication.  Returns the
- * authenticated customer on success, null otherwise. */
-suspend fun doTokenAuth(
-    db: Database,
-    token: String,
-    requiredScope: TokenScope,
-): Customer? {
-    val bareToken = splitBearerToken(token) ?: throw badRequest(
-        "Bearer token malformed",
-        talerErrorCode = TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
-    )
-    val tokenBytes = try {
-        Base32Crockford.decode(bareToken)
-    } catch (e: Exception) {
-        throw badRequest(
-            e.message, TalerErrorCode.TALER_EC_GENERIC_HTTP_HEADERS_MALFORMED
-        )
-    }
-    val maybeToken: BearerToken? = db.bearerTokenGet(tokenBytes)
-    if (maybeToken == null) {
-        logger.error("Auth token not found")
-        return null
-    }
-    if (maybeToken.expirationTime.isBefore(Instant.now())) {
-        logger.error("Auth token is expired")
-        return null
-    }
-    if (maybeToken.scope == TokenScope.readonly && requiredScope == 
TokenScope.readwrite) {
-        logger.error("Auth token has insufficient scope")
-        return null
-    }
-    if (!maybeToken.isRefreshable && requiredScope == TokenScope.refreshable) {
-        logger.error("Could not refresh unrefreshable token")
-        return null
-    }
-    // Getting the related username.
-    return db.customerGetFromRowId(maybeToken.bankCustomer) ?: throw 
LibeufinBankException(
-        httpStatus = HttpStatusCode.InternalServerError, talerError = 
TalerError(
-            code = 
TalerErrorCode.TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE.code,
-            hint = "Customer not found, despite token mentions it.",
-        )
+/** Get account login from path */
+suspend fun ApplicationCall.accountLogin(): String = 
getResourceName("USERNAME")
+
+/** Retrieve the bank account info for the selected username*/
+suspend fun ApplicationCall.bankAccount(db: Database): BankAccount {
+    val login = accountLogin()
+    return db.bankAccountGetFromCustomerLogin(login) ?: throw notFound(
+        hint = "Bank account for customer $login not found",
+        talerEc = TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
     )
 }
 
@@ -176,7 +98,6 @@ fun internalServerError(hint: String?): 
LibeufinBankException = LibeufinBankExce
     )
 )
 
-
 fun notFound(
     hint: String?,
     talerEc: TalerErrorCode
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt 
b/bank/src/test/kotlin/CoreBankApiTest.kt
index 2af6ecbd..d144fe2e 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -21,6 +21,270 @@ import kotlin.random.Random
 import kotlin.test.*
 import kotlinx.coroutines.*
 
+class CoreBankAccountsMgmtApiTest {
+    // Testing the account creation and its idempotency
+    @Test
+    fun createAccountTest() = bankSetup { _ -> 
+        val ibanPayto = genIbanPaytoUri()
+        val req = json {
+            "username" to "foo"
+            "password" to "password"
+            "name" to "Jane"
+            "is_public" to true
+            "internal_payto_uri" to ibanPayto
+        }
+        // Check Ok
+        client.post("/accounts") {
+            jsonBody(req)
+        }.assertCreated()
+        // Testing idempotency.
+        client.post("/accounts") {
+            jsonBody(req)
+        }.assertCreated()
+
+        // Test generate payto_uri
+        client.post("/accounts") {
+            jsonBody(json {
+                "username" to "jor"
+                "password" to "password"
+                "name" to "Joe"
+            })
+        }.assertCreated()
+
+        // Reserved account
+        reservedAccounts.forEach {
+            client.post("/accounts") {
+                jsonBody(json {
+                    "username" to it
+                    "password" to "password"
+                    "name" to "John Smith"
+                })
+            }.assertStatus(HttpStatusCode.Conflict)
+        }
+    }
+
+    // Test admin-only account creation
+    @Test
+    fun createAccountRestrictedTest() = bankSetup(conf = "test_restrict.conf") 
{ _ -> 
+        val req = json {
+            "username" to "baz"
+            "password" to "xyz"
+            "name" to "Mallory"
+        }
+
+        // Ordinary user
+        client.post("/accounts") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(req)
+        }.assertUnauthorized()
+        // Administrator
+        client.post("/accounts") {
+            basicAuth("admin", "admin-password")
+            jsonBody(req)
+        }.assertCreated()
+    }
+
+    // DELETE /accounts/USERNAME
+    @Test
+    fun deleteAccount() = bankSetup { _ -> 
+        // Unknown account
+        client.delete("/accounts/unknown") {
+            basicAuth("admin", "admin-password")
+        }.assertStatus(HttpStatusCode.NotFound)
+
+        // Reserved account
+        reservedAccounts.forEach {
+            client.delete("/accounts/$it") {
+                basicAuth("admin", "admin-password")
+                expectSuccess = false
+            }.assertStatus(HttpStatusCode.Conflict)
+        }
+       
+        // successful deletion
+        client.post("/accounts") {
+            jsonBody(json {
+                "username" to "john"
+                "password" to "password"
+                "name" to "John Smith"
+            })
+        }.assertCreated()
+        client.delete("/accounts/john") {
+            basicAuth("admin", "admin-password")
+        }.assertNoContent()
+        // Trying again must yield 404
+        client.delete("/accounts/john") {
+            basicAuth("admin", "admin-password")
+        }.assertStatus(HttpStatusCode.NotFound)
+
+        
+        // fail to delete, due to a non-zero balance.
+        client.post("/accounts/exchange/transactions") {
+            basicAuth("exchange", "exchange-password")
+            jsonBody(json {
+                "payto_uri" to 
"payto://iban/MERCHANT-IBAN-XYZ?message=payout&amount=KUDOS:1"
+            })
+        }.assertOk()
+        client.delete("/accounts/merchant") {
+            basicAuth("admin", "admin-password")
+        }.assertStatus(HttpStatusCode.PreconditionFailed)
+        client.post("/accounts/merchant/transactions") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(json {
+                "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout&amount=KUDOS:1"
+            })
+        }.assertOk()
+        client.delete("/accounts/merchant") {
+            basicAuth("admin", "admin-password")
+        }.assertNoContent()
+    }
+
+    // PATCH /accounts/USERNAME
+    @Test
+    fun accountReconfig() = bankSetup { db -> 
+        // Successful attempt now.
+        val req = json {
+            "cashout_address" to "payto://new-cashout-address"
+            "challenge_contact_data" to json {
+                "email" to "new@example.com"
+                "phone" to "+987"
+            }
+            "is_exchange" to true
+        }
+        client.patch("/accounts/merchant") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(req)
+        }.assertNoContent()
+        // Checking idempotence.
+        client.patch("/accounts/merchant") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(req)
+        }.assertNoContent()
+
+        val nameReq = json {
+            "name" to "Another Foo"
+        }
+        // Checking ordinary user doesn't get to patch their name.
+        client.patch("/accounts/merchant") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(nameReq)
+        }.assertStatus(HttpStatusCode.Forbidden)
+        // Finally checking that admin does get to patch foo's name.
+        client.patch("/accounts/merchant") {
+            basicAuth("admin", "admin-password")
+            jsonBody(nameReq)
+        }.assertNoContent()
+
+        val fooFromDb = db.customerGetFromLogin("merchant")
+        assertEquals("Another Foo", fooFromDb?.name)
+    }
+
+    // PATCH /accounts/USERNAME/auth
+    @Test
+    fun passwordChangeTest() = bankSetup { _ -> 
+        // Changing the password.
+        client.patch("/accounts/merchant/auth") {
+            basicAuth("merchant", "merchant-password")
+            jsonBody(json {
+                "new_password" to "new-password"
+            })
+        }.assertNoContent()
+        // Previous password should fail.
+        client.patch("/accounts/merchant/auth") {
+            basicAuth("merchant", "merchant-password")
+        }.assertUnauthorized()
+        // New password should succeed.
+        client.patch("/accounts/merchant/auth") {
+            basicAuth("merchant", "new-password")
+            jsonBody(json {
+                "new_password" to "merchant-password"
+            })
+        }.assertNoContent()
+    }
+
+    // GET /public-accounts and GET /accounts
+    @Test
+    fun accountsListTest() = bankSetup { _ -> 
+        // Remove default accounts
+        listOf("merchant", "exchange").forEach {
+            client.delete("/accounts/$it") {
+                basicAuth("admin", "admin-password")
+            }.assertNoContent()
+        }
+        // Check error when no public accounts
+        client.get("/public-accounts").assertNoContent()
+        client.get("/accounts") {
+            basicAuth("admin", "admin-password")
+        }.assertOk()
+        
+        // Gen some public and private accounts
+        repeat(5) {
+            client.post("/accounts") {
+                jsonBody(json {
+                    "username" to "$it"
+                    "password" to "password"
+                    "name" to "Mr $it"
+                    "is_public" to (it%2 == 0)
+                })
+            }.assertCreated()
+        }
+        // All public
+        client.get("/public-accounts").run {
+            assertOk()
+            val obj = 
Json.decodeFromString<PublicAccountsResponse>(bodyAsText())
+            assertEquals(3, obj.public_accounts.size)
+            obj.public_accounts.forEach {
+                assertEquals(0, it.account_name.toInt() % 2)
+            }
+        }
+        // All accounts
+        client.get("/accounts"){
+            basicAuth("admin", "admin-password")
+        }.run {
+            assertOk()
+            val obj = 
Json.decodeFromString<ListBankAccountsResponse>(bodyAsText())
+            assertEquals(6, obj.accounts.size)
+            obj.accounts.forEachIndexed { idx, it ->
+                if (idx == 0) {
+                    assertEquals("admin", it.username)
+                } else {
+                    assertEquals(idx - 1, it.username.toInt())
+                }
+            }
+        }
+        // Filtering
+         client.get("/accounts?filter_name=3"){
+            basicAuth("admin", "admin-password")
+        }.run {
+            assertOk()
+            val obj = 
Json.decodeFromString<ListBankAccountsResponse>(bodyAsText())
+            assertEquals(1, obj.accounts.size)
+            assertEquals("3", obj.accounts[0].username)
+        }
+    }
+
+    // GET /accounts/USERNAME
+    @Test
+    fun getAccountTest() = bankSetup { _ -> 
+        // Check ok
+        client.get("/accounts/merchant") {
+            basicAuth("merchant", "merchant-password")
+        }.assertOk().run {
+            val obj: AccountData = Json.decodeFromString(bodyAsText())
+            assertEquals("Merchant", obj.name)
+        }
+
+        // Check admin ok
+        client.get("/accounts/merchant") {
+            basicAuth("admin", "admin-password")
+        }.assertOk()
+
+        // Check wrong user
+        client.get("/accounts/exchange") {
+            basicAuth("merchanr", "merchanr-password")
+        }.assertStatus(HttpStatusCode.Unauthorized)
+    }
+}
+
 class CoreBankTransactionsApiTest {
     // Test endpoint is correctly authenticated 
     suspend fun ApplicationTestBuilder.authRoutine(path: String, withAdmin: 
Boolean = true, method: HttpMethod = HttpMethod.Post) {
@@ -299,7 +563,6 @@ class CoreBankTransactionsApiTest {
             })
         }.assertStatus(HttpStatusCode.Conflict)
     }
-    
 }
 
 class LibeuFinApiTest {
@@ -337,38 +600,7 @@ class LibeuFinApiTest {
         println(r.bodyAsText())
     }
 
-    @Test
-    fun passwordChangeTest() = setup { db, ctx -> 
-        assert(db.customerCreate(customerFoo) != null)
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            // Changing the password.
-            client.patch("/accounts/foo/auth") {
-                expectSuccess = true
-                contentType(ContentType.Application.Json)
-                basicAuth("foo", "pw")
-                setBody("""{"new_password": "bar"}""")
-            }
-            // Previous password should fail.
-            client.patch("/accounts/foo/auth") {
-                expectSuccess = false
-                contentType(ContentType.Application.Json)
-                basicAuth("foo", "pw")
-                setBody("""{"new_password": "not-even-parsed"}""")
-            }.apply {
-                assert(this.status == HttpStatusCode.Unauthorized)
-            }
-            // New password should succeed.
-            client.patch("/accounts/foo/auth") {
-                expectSuccess = true
-                contentType(ContentType.Application.Json)
-                basicAuth("foo", "bar")
-                setBody("""{"new_password": "not-used"}""")
-            }
-        }
-    }
+   
     @Test
     fun tokenDeletionTest() = setup { db, ctx -> 
         assert(db.customerCreate(customerFoo) != null)
@@ -417,39 +649,6 @@ class LibeuFinApiTest {
         }
     }
 
-    @Test
-    fun publicAccountsTest() = setup { db, ctx -> 
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            client.get("/public-accounts").apply {
-                assert(this.status == HttpStatusCode.NoContent)
-            }
-            // Make one public account.
-            db.customerCreate(customerBar).apply {
-                assert(this != null)
-                assert(
-                    db.bankAccountCreate(
-                        BankAccount(
-                            isPublic = true,
-                            internalPaytoUri = 
IbanPayTo("payto://iban/non-used"),
-                            lastNexusFetchRowId = 1L,
-                            owningCustomerId = this!!,
-                            hasDebt = false,
-                            maxDebt = TalerAmount(10, 1, "KUDOS")
-                        )
-                    ) != null
-                )
-            }
-            client.get("/public-accounts").apply {
-                assert(this.status == HttpStatusCode.OK)
-                val obj = 
Json.decodeFromString<PublicAccountsResponse>(this.bodyAsText())
-                assert(obj.public_accounts.size == 1)
-                assert(obj.public_accounts[0].account_name == "bar")
-            }
-        }
-    }
     // Creating token with "forever" duration.
     @Test
     fun tokenForeverTest() = setup { db, ctx -> 
@@ -539,7 +738,7 @@ class LibeuFinApiTest {
                 expectSuccess = false
                 basicAuth("foo", "pw")
             }
-            assert(r.status == HttpStatusCode.Forbidden)
+            assert(r.status == HttpStatusCode.Unauthorized)
             // Make ad-hoc token for foo.
             val fooTok = ByteArray(32).apply { Random.nextBytes(this) }
             assert(
@@ -575,390 +774,4 @@ class LibeuFinApiTest {
             assert(never.expiration.t_s == Instant.MAX)
         }
     }
-
-    /**
-     * Testing the retrieval of account information.
-     * The tested logic is the one usually needed by SPAs
-     * to show customers their status.
-     */
-    @Test
-    fun getAccountTest() = setup { db, ctx -> 
-        // Artificially insert a customer and bank account in the database.
-        val customerRowId = db.customerCreate(
-            Customer(
-                "foo",
-                CryptoUtil.hashpw("pw"),
-                "Foo"
-            )
-        )
-        assert(customerRowId != null)
-        assert(
-            db.bankAccountCreate(
-                BankAccount(
-                    hasDebt = false,
-                    internalPaytoUri = IbanPayTo("payto://iban/DE1234"),
-                    maxDebt = TalerAmount(100, 0, "KUDOS"),
-                    owningCustomerId = customerRowId!!
-                )
-            ) != null
-        )
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            val r = client.get("/accounts/foo") {
-                expectSuccess = true
-                basicAuth("foo", "pw")
-            }
-            val obj: AccountData = Json.decodeFromString(r.bodyAsText())
-            assert(obj.name == "Foo")
-            // Checking admin can.
-            val adminRowId = db.customerCreate(
-                Customer(
-                    "admin",
-                    CryptoUtil.hashpw("admin"),
-                    "Admin"
-                )
-            )
-            assert(adminRowId != null)
-            assert(
-                db.bankAccountCreate(
-                    BankAccount(
-                        hasDebt = false,
-                        internalPaytoUri = 
IbanPayTo("payto://iban/SANDBOXX/ADMIN-IBAN"),
-                        maxDebt = TalerAmount(100, 0, "KUDOS"),
-                        owningCustomerId = adminRowId!!
-                    )
-                ) != null
-            )
-            client.get("/accounts/foo") {
-                expectSuccess = true
-                basicAuth("admin", "admin")
-            }
-            val shouldNot = client.get("/accounts/foo") {
-                basicAuth("not", "not")
-                expectSuccess = false
-            }
-            assert(shouldNot.status == HttpStatusCode.Unauthorized)
-        }
-    }
-
-    /**
-     * Testing the account creation and its idempotency
-     */
-    @Test
-    fun createAccountTest() = setup { db, ctx -> 
-        testApplication {
-            val ibanPayto = genIbanPaytoUri()
-            application {
-                corebankWebApp(db, ctx)
-            }
-            var resp = client.post("/accounts") {
-                expectSuccess = false
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "foo",
-                    "password": "bar",
-                    "name": "Jane",
-                    "is_public": true,
-                    "internal_payto_uri": "$ibanPayto"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Created)
-            // Testing idempotency.
-            resp = client.post("/accounts") {
-                expectSuccess = false
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "foo",
-                    "password": "bar",
-                    "name": "Jane",
-                    "is_public": true,
-                    "internal_payto_uri": "$ibanPayto"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Created)
-        }
-    }
-
-    /**
-     * Testing the account creation and its idempotency
-     */
-    @Test
-    fun createTwoAccountsTest() = setup { db, ctx -> 
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            var resp = client.post("/accounts") {
-                expectSuccess = false
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "foo",
-                    "password": "bar",
-                    "name": "Jane"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Created)
-            // Test creating another account.
-            resp = client.post("/accounts") {
-                expectSuccess = false
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "joe",
-                    "password": "bar",
-                    "name": "Joe"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Created)
-        }
-    }
-
-    /**
-     * Test admin-only account creation
-     */
-    @Test
-    fun createAccountRestrictedTest() = setup(conf = "test_restrict.conf") { 
db, ctx -> 
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-
-            // Ordinary user tries, should fail.
-            var resp = client.post("/accounts") {
-                expectSuccess = false
-                basicAuth("foo", "bar")
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "baz",
-                    "password": "xyz",
-                    "name": "Mallory"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Unauthorized)
-            // Creating the administrator.
-            assert(
-                db.customerCreate(
-                    Customer(
-                        "admin",
-                        CryptoUtil.hashpw("pass"),
-                        "CFO"
-                    )
-                ) != null
-            )
-            // customer exists, this makes only the bank account:
-            assert(maybeCreateAdminAccount(db, ctx))
-            resp = client.post("/accounts") {
-                expectSuccess = false
-                basicAuth("admin", "pass")
-                contentType(ContentType.Application.Json)
-                setBody(
-                    """{
-                    "username": "baz",
-                    "password": "xyz",
-                    "name": "Mallory"
-                }""".trimIndent()
-                )
-            }
-            assert(resp.status == HttpStatusCode.Created)
-        }
-    }
-
-    /**
-     * Tests DELETE /accounts/foo
-     */
-    @Test
-    fun deleteAccount() = setup { db, ctx -> 
-        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 = true
-            }.apply {
-                assert(this.status == HttpStatusCode.NoContent)
-            }
-            // Trying again must yield 404
-            client.delete("/accounts/foo") {
-                basicAuth("admin", "pass")
-                expectSuccess = false
-            }.apply {
-                assert(this.status == HttpStatusCode.NotFound)
-            }
-            // 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 reconfiguration of account data.
-     */
-    @Test
-    fun accountReconfig() = setup { db, ctx -> 
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            assertNotNull(db.customerCreate(customerFoo))
-            // First call expects 500, because foo lacks a bank account
-            client.patch("/accounts/foo") {
-                basicAuth("foo", "pw")
-                jsonBody(json {
-                    "is_exchange" to true
-                })
-            }.assertStatus(HttpStatusCode.InternalServerError)
-            // Creating foo's bank account.
-            assertNotNull(db.bankAccountCreate(genBankAccount(1L)))
-            // Successful attempt now.
-            val validReq = AccountReconfiguration(
-                cashout_address = "payto://new-cashout-address",
-                challenge_contact_data = ChallengeContactData(
-                    email = "new@example.com",
-                    phone = "+987"
-                ),
-                is_exchange = true,
-                name = null
-            )
-            client.patch("/accounts/foo") {
-                basicAuth("foo", "pw")
-                jsonBody(validReq)
-            }.assertStatus(HttpStatusCode.NoContent)
-            // Checking idempotence.
-            client.patch("/accounts/foo") {
-                basicAuth("foo", "pw")
-                jsonBody(validReq)
-            }.assertStatus(HttpStatusCode.NoContent)
-            // Checking ordinary user doesn't get to patch their name.
-            client.patch("/accounts/foo") {
-                basicAuth("foo", "pw")
-                jsonBody(json {
-                    "name" to "Another Foo"
-                })
-            }.assertStatus(HttpStatusCode.Forbidden)
-            // Finally checking that admin does get to patch foo's name.
-            assertNotNull(db.customerCreate(Customer(
-                login = "admin",
-                passwordHash = CryptoUtil.hashpw("secret"),
-                name = "CFO"
-            )))
-            client.patch("/accounts/foo") {
-                basicAuth("admin", "secret")
-                jsonBody(json {
-                    "name" to "Another Foo"
-                })
-            }.assertStatus(HttpStatusCode.NoContent)
-            val fooFromDb = db.customerGetFromLogin("foo")
-            assertNotNull(fooFromDb)
-            assertEquals("Another Foo", fooFromDb.name)
-        }
-    }
-
-    /**
-     * Tests the GET /accounts endpoint.
-     */
-    @Test
-    fun getAccountsList() = setup { db, ctx -> 
-        val adminCustomer = Customer(
-            "admin",
-            CryptoUtil.hashpw("pass"),
-            "CFO"
-        )
-        assert(db.customerCreate(adminCustomer) != null)
-        testApplication {
-            application {
-                corebankWebApp(db, ctx)
-            }
-            // No users registered, expect no data.
-            client.get("/accounts") {
-                basicAuth("admin", "pass")
-                expectSuccess = true
-            }.apply {
-                assert(this.status == HttpStatusCode.NoContent)
-            }
-            // foo account
-            db.customerCreate(customerFoo).apply {
-                assert(this != null)
-                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
-            }
-            // bar account
-            db.customerCreate(customerBar).apply {
-                assert(this != null)
-                assert(db.bankAccountCreate(genBankAccount(this!!)) != null)
-            }
-            // Two users registered, requesting all of them.
-            client.get("/accounts") {
-                basicAuth("admin", "pass")
-                expectSuccess = true
-            }.apply {
-                println(this.bodyAsText())
-                assert(this.status == HttpStatusCode.OK)
-                val obj = 
Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText())
-                assert(obj.accounts.size == 2)
-                // Order unreliable, just checking they're different.
-                assert(obj.accounts[0].username != obj.accounts[1].username)
-            }
-            // Filtering on bar.
-            client.get("/accounts?filter_name=ar") {
-                basicAuth("admin", "pass")
-                expectSuccess = true
-            }.apply {
-                assert(this.status == HttpStatusCode.OK)
-                val obj = 
Json.decodeFromString<ListBankAccountsResponse>(this.bodyAsText())
-                assert(obj.accounts.size == 1) {
-                    println("Wrong size of filtered query: 
${obj.accounts.size}")
-                }
-                assert(obj.accounts[0].username == "bar")
-            }
-        }
-    }
-
 }
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index 5d6d41c3..d8be7861 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -255,31 +255,6 @@ class DatabaseTest {
         assert(barAccount?.balance?.equals(TalerAmount(10, 0, "KUDOS")) == 
true)
     }
 
-    // Testing customer(+bank account) deletion logic.
-    @Test
-    fun customerDeletionTest() = setupDb { db ->
-        // 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)
-
-        // Creating foo again, artificially setting its balance != zero.
-        db.customerCreate(customerFoo).apply {
-            assert(this != null)
-            db.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() = setupDb { db ->
         assert(db.customerGetFromLogin("foo") == null)
@@ -338,18 +313,24 @@ class DatabaseTest {
         // Foo pays Bar 100 times:
         for (i in 1..100) { db.bankTransactionCreate(genTx("test-$i")) }
         // Testing positive delta:
-        val forward = db.bankTransactionGetHistory(
-            start = 50L,
-            delta = 2,
+        val forward = db.bankPoolHistory(
+            params = HistoryParams(
+                start = 50L,
+                delta = 2,
+                poll_ms = 0
+            ),
             bankAccountId = 1L // asking as Foo
         )
-        assert(forward[0].expectRowId() >= 50 && forward.size == 2 && 
forward[0].dbRowId!! < forward[1].dbRowId!!)
-        val backward = db.bankTransactionGetHistory(
-            start = 50L,
-            delta = -2,
+        assert(forward[0].row_id >= 50 && forward.size == 2 && 
forward[0].row_id < forward[1].row_id)
+        val backward = db.bankPoolHistory(
+            params = HistoryParams(
+                start = 50L,
+                delta = -2,
+                poll_ms = 0
+            ),
             bankAccountId = 1L // asking as Foo
         )
-        assert(backward[0].expectRowId() <= 50 && backward.size == 2 && 
backward[0].dbRowId!! > backward[1].dbRowId!!)
+        assert(backward[0].row_id <= 50 && backward.size == 2 && 
backward[0].row_id > backward[1].row_id)
     }
     @Test
     fun cashoutTest() = setupDb { db ->
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 12dc7434..0cfaf69e 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -47,13 +47,25 @@ val bankAccountExchange = BankAccount(
     isTalerExchange = true
 )
 
-fun bankSetup(lambda: suspend ApplicationTestBuilder.(Database) -> Unit) {
-    setup { db, ctx -> 
+fun bankSetup(
+    conf: String = "test.conf",    
+    lambda: suspend ApplicationTestBuilder.(Database) -> Unit
+) {
+    setup(conf) { db, ctx -> 
         // Creating the exchange and merchant accounts first.
         assertNotNull(db.customerCreate(customerMerchant))
         assertNotNull(db.bankAccountCreate(bankAccountMerchant))
         assertNotNull(db.customerCreate(customerExchange))
         assertNotNull(db.bankAccountCreate(bankAccountExchange))
+        // Create admin account
+        assertNotNull(db.customerCreate(
+            Customer(
+                "admin",
+                CryptoUtil.hashpw("admin-password"),
+                "CFO"
+            )
+        ))
+        assert(maybeCreateAdminAccount(db, ctx))
         testApplication {
             application {
                 corebankWebApp(db, ctx)
@@ -90,6 +102,9 @@ fun HttpResponse.assertStatus(status: HttpStatusCode): 
HttpResponse {
     return this
 }
 fun HttpResponse.assertOk(): HttpResponse = assertStatus(HttpStatusCode.OK)
+fun HttpResponse.assertCreated(): HttpResponse = 
assertStatus(HttpStatusCode.Created)
+fun HttpResponse.assertNoContent(): HttpResponse = 
assertStatus(HttpStatusCode.NoContent)
+fun HttpResponse.assertUnauthorized(): HttpResponse = 
assertStatus(HttpStatusCode.Unauthorized)
 fun HttpResponse.assertBadRequest(): HttpResponse = 
assertStatus(HttpStatusCode.BadRequest)
 
 fun BankTransactionResult.assertSuccess() {
diff --git a/database-versioning/procedures.sql 
b/database-versioning/procedures.sql
index ec26a242..c2d07a8d 100644
--- a/database-versioning/procedures.sql
+++ b/database-versioning/procedures.sql
@@ -432,7 +432,81 @@ COMMENT ON FUNCTION taler_add_incoming(
   IS 'function that (1) inserts the TWG requests'
      'details into the database and (2) performs '
      'the actual bank transaction to pay the merchant';
-  
+
+CREATE OR REPLACE FUNCTION bank_transaction(
+  IN in_credit_account_payto TEXT,
+  IN in_debit_account_username TEXT,
+  IN in_subject TEXT,
+  IN in_amount taler_amount,
+  IN in_timestamp BIGINT,
+  IN in_account_servicer_reference TEXT,
+  IN in_payment_information_id TEXT,
+  IN in_end_to_end_id TEXT,
+  -- Error status
+  OUT out_creditor_not_found BOOLEAN,
+  OUT out_debtor_not_found BOOLEAN,
+  OUT out_same_account BOOLEAN,
+  OUT out_balance_insufficient BOOLEAN,
+  -- Success return
+  OUT out_credit_bank_account_id BIGINT,
+  OUT out_debit_bank_account_id BIGINT,
+  OUT out_credit_row_id BIGINT,
+  OUT out_debit_row_id BIGINT,
+  OUT out_creditor_is_exchange BOOLEAN,
+  OUT out_debtor_is_exchange BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+-- Find credit bank account id
+SELECT bank_account_id
+  INTO out_credit_bank_account_id
+  FROM bank_accounts
+  WHERE internal_payto_uri = in_credit_account_payto;
+IF NOT FOUND THEN
+  out_creditor_not_found=TRUE;
+  RETURN;
+END IF;
+-- Find debit bank account id
+SELECT bank_account_id
+  INTO out_debit_bank_account_id
+  FROM bank_accounts 
+      JOIN customers 
+        ON customer_id=owning_customer_id
+  WHERE login = in_debit_account_username;
+IF NOT FOUND THEN
+  out_debtor_not_found=TRUE;
+  RETURN;
+END IF;
+-- Perform bank transfer
+SELECT
+  transfer.out_balance_insufficient,
+  transfer.out_credit_row_id,
+  transfer.out_debit_row_id,
+  transfer.out_same_account,
+  transfer.out_creditor_is_exchange,
+  transfer.out_debtor_is_exchange
+  INTO
+    out_balance_insufficient,
+    out_credit_row_id,
+    out_debit_row_id,
+    out_same_account,
+    out_creditor_is_exchange,
+    out_debtor_is_exchange
+  FROM bank_wire_transfer(
+    out_credit_bank_account_id,
+    out_debit_bank_account_id,
+    in_subject,
+    in_amount,
+    in_timestamp,
+    in_account_servicer_reference,
+    in_payment_information_id,
+    in_end_to_end_id
+  ) as transfer;
+IF out_balance_insufficient THEN
+  RETURN;
+END IF;
+END $$;
 
 CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
   IN in_withdrawal_uuid uuid,
diff --git a/util/src/main/kotlin/CryptoUtil.kt 
b/util/src/main/kotlin/CryptoUtil.kt
index 1528c623..a9699446 100644
--- a/util/src/main/kotlin/CryptoUtil.kt
+++ b/util/src/main/kotlin/CryptoUtil.kt
@@ -310,25 +310,21 @@ object CryptoUtil {
 
     fun checkpw(pw: String, storedPwHash: String): Boolean {
         val components = storedPwHash.split('$')
-        if (components.size < 2) {
-            throw Exception("bad password hash")
-        }
-        val algo = components[0]
-        // Support legacy unsalted passwords
-        if (algo == "sha256") {
-            val hash = components[1]
-            val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw))
-            return pwh == hash
-        }
-        if (algo == "sha256-salted") {
-            if (components.size != 3) {
-                throw Exception("bad password hash")
+        when (val algo = components[0]) {
+            "sha256" -> {  // Support legacy unsalted passwords
+                if (components.size != 2) throw Exception("bad password hash")
+                val hash = components[1]
+                val pwh = bytesToBase64(CryptoUtil.hashStringSHA256(pw))
+                return pwh == hash
+            }
+            "sha256-salted" -> {
+                if (components.size != 3) throw Exception("bad password hash")
+                val salt = components[1]
+                val hash = components[2]
+                val pwh = 
bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw"))
+                return pwh == hash
             }
-            val salt = components[1]
-            val hash = components[2]
-            val pwh = bytesToBase64(CryptoUtil.hashStringSHA256("$salt|$pw"))
-            return pwh == hash
+            else -> throw Exception("unsupported hash algo: '$algo'")
         }
-        throw Exception("unsupported hash algo: '$algo'")
     }
 }

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