gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated: fix timestamp format, normalize intern


From: gnunet
Subject: [libeufin] branch master updated: fix timestamp format, normalize internal IBANs
Date: Sun, 24 Sep 2023 21:21:15 +0200

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

dold pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 1a641ba7 fix timestamp format, normalize internal IBANs
1a641ba7 is described below

commit 1a641ba73be83f379a2e0c9f876b3c6ff30d3ed7
Author: Florian Dold <florian@dold.me>
AuthorDate: Sun Sep 24 21:18:27 2023 +0200

    fix timestamp format, normalize internal IBANs
---
 .../main/kotlin/tech/libeufin/bank/BankMessages.kt |  21 ++--
 .../tech/libeufin/bank/CorebankApiHandlers.kt      | 133 +++++++++------------
 .../src/main/kotlin/tech/libeufin/bank/Database.kt |   1 -
 .../tech/libeufin/bank/IntegrationApiHandlers.kt   |  48 +++-----
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    |   7 +-
 .../tech/libeufin/bank/WireGatewayApiHandlers.kt   |  41 ++++---
 bank/src/test/kotlin/LibeuFinApiTest.kt            |  40 +++++++
 util/src/main/kotlin/{Payto.kt => IbanPayto.kt}    |  22 +++-
 util/src/test/kotlin/PaytoTest.kt                  |   4 +-
 9 files changed, 178 insertions(+), 139 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
index 429a89ef..86d52ccd 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -24,6 +24,7 @@ import io.ktor.server.application.*
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.Serializable
 import java.util.*
+import kotlin.reflect.jvm.internal.impl.types.AbstractStubType
 
 /**
  * Allowed lengths for fractional digits in amounts.
@@ -38,9 +39,15 @@ enum class FracDigits(howMany: Int) {
  * Timestamp containing the number of seconds since epoch.
  */
 @Serializable
-data class Timestamp(
-    val t_s: Long // FIXME (?): not supporting "never" at the moment.
-)
+data class TalerProtocolTimestamp(
+    val t_s: Long, // FIXME (?): not supporting "never" at the moment.
+) {
+    companion object {
+        fun fromMicroseconds(uSec: Long): TalerProtocolTimestamp {
+            return TalerProtocolTimestamp(uSec / 1000000)
+        }
+    }
+}
 
 /**
  * HTTP response type of successful token refresh.
@@ -51,7 +58,7 @@ data class Timestamp(
 @Serializable
 data class TokenSuccessResponse(
     val access_token: String,
-    val expiration: Timestamp
+    val expiration: TalerProtocolTimestamp
 )
 
 /**
@@ -545,7 +552,7 @@ data class AddIncomingRequest(
  */
 @Serializable
 data class AddIncomingResponse(
-    val timestamp: Long,
+    val timestamp: TalerProtocolTimestamp,
     val row_id: Long
 )
 
@@ -572,7 +579,7 @@ data class IncomingHistory(
 data class IncomingReserveTransaction(
     val type: String = "RESERVE",
     val row_id: Long, // DB row ID of the payment.
-    val date: Long, // microseconds timestamp.
+    val date: TalerProtocolTimestamp,
     val amount: String,
     val debit_account: String, // Payto of the sender.
     val reserve_pub: String
@@ -596,6 +603,6 @@ data class TransferRequest(
  */
 @Serializable
 data class TransferResponse(
-    val timestamp: Long,
+    val timestamp: TalerProtocolTimestamp,
     val row_id: Long
 )
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
index e1939073..eaaa33ad 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -26,7 +26,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/accounts/{USERNAME}/token") {
-        val customer = call.authenticateBankRequest(db, 
TokenScope.refreshable) ?: throw unauthorized("Authentication failed")
+        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_END 
// FIXME: need generic forbidden
@@ -51,8 +52,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         }
         val maxDurationTime: Long = ctx.maxAuthTokenDurationUs
         if (req.duration != null && req.duration.d_us > maxDurationTime) throw 
forbidden(
-            "Token duration bigger than bank's limit",
-            // FIXME: define new EC for this case.
+            "Token duration bigger than bank's limit", // FIXME: define new EC 
for this case.
             TalerErrorCode.TALER_EC_END
         )
         val tokenDurationUs = req.duration?.d_us ?: TOKEN_DEFAULT_DURATION_US
@@ -61,8 +61,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         val expirationTimestampUs: Long = getNowUs() + tokenDurationUs
         if (expirationTimestampUs < tokenDurationUs) throw badRequest(
-            "Token duration caused arithmetic overflow",
-            // FIXME: need dedicate EC (?)
+            "Token duration caused arithmetic overflow", // FIXME: need 
dedicate EC (?)
             talerErrorCode = TalerErrorCode.TALER_EC_END
         )
         val token = BearerToken(
@@ -76,7 +75,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         if (!db.bearerTokenCreate(token)) throw internalServerError("Failed at 
inserting new token in the database")
         call.respond(
             TokenSuccessResponse(
-                access_token = Base32Crockford.encode(tokenBytes), expiration 
= Timestamp(
+                access_token = Base32Crockford.encode(tokenBytes), expiration 
= TalerProtocolTimestamp(
                     t_s = expirationTimestampUs / 1000000L
                 )
             )
@@ -84,8 +83,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         return@post
     }
 
-    post("/accounts") {
-        // check if only admin is allowed to create new accounts
+    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(
@@ -94,30 +92,29 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
                     hint = "Either 'admin' not authenticated or an ordinary 
user tried this operation."
                 )
             )
-        }
-        // auth passed, proceed with activity.
-        val req = call.receive<RegisterAccountRequest>()
-        // Prohibit reserved usernames:
+        } // 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 = GENERIC_UNDEFINED, // FIXME: this waits GANA.
                 hint = "Username '${req.username}' is reserved."
             )
-        )
-        // Checking idempotency.
-        val maybeCustomerExists = db.customerGetFromLogin(req.username)
-        // Can be null if previous call crashed before completion.
+        ) // Checking idempotency.
+        val maybeCustomerExists =
+            db.customerGetFromLogin(req.username) // Can be null if previous 
call crashed before completion.
         val maybeHasBankAccount = maybeCustomerExists.run {
             if (this == null) return@run null
             db.bankAccountGetFromOwnerId(this.expectRowId())
         }
         if (maybeCustomerExists != null && maybeHasBankAccount != null) {
-            logger.debug("Registering username was found: 
${maybeCustomerExists.login}")
-            // Checking _all_ the details are the same.
+            logger.debug("Registering username was found: 
${maybeCustomerExists.login}") // Checking _all_ the details are the same.
             val isIdentic =
-                maybeCustomerExists.name == req.name && 
maybeCustomerExists.email == req.challenge_contact_data?.email && 
maybeCustomerExists.phone == req.challenge_contact_data?.phone && 
maybeCustomerExists.cashoutPayto == req.cashout_payto_uri && CryptoUtil.checkpw(
-                    req.password,
-                    maybeCustomerExists.passwordHash
+                maybeCustomerExists.name == req.name &&
+                        maybeCustomerExists.email == 
req.challenge_contact_data?.email &&
+                        maybeCustomerExists.phone == 
req.challenge_contact_data?.phone &&
+                        maybeCustomerExists.cashoutPayto ==
+                        req.cashout_payto_uri && CryptoUtil.checkpw(
+                    req.password, maybeCustomerExists.passwordHash
                 ) && maybeHasBankAccount.isPublic == req.is_public && 
maybeHasBankAccount.isTalerExchange == req.is_taler_exchange && 
maybeHasBankAccount.internalPaytoUri == req.internal_payto_uri
             if (isIdentic) {
                 call.respond(HttpStatusCode.Created)
@@ -129,26 +126,27 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
                     hint = "Idempotency check failed."
                 )
             )
-        }
-        // From here: fresh user being added.
+        } // From here: fresh user being added.
         val newCustomer = Customer(
             login = req.username,
             name = req.name,
             email = req.challenge_contact_data?.email,
             phone = req.challenge_contact_data?.phone,
-            cashoutPayto = req.cashout_payto_uri,
-            // Following could be gone, if included in cashout_payto_uri
+            cashoutPayto = req.cashout_payto_uri, // Following could be gone, 
if included in cashout_payto_uri
             cashoutCurrency = ctx.cashoutCurrency,
             passwordHash = CryptoUtil.hashpw(req.password),
         )
         val newCustomerRowId = db.customerCreate(newCustomer)
-            ?: throw internalServerError("New customer INSERT failed despite 
the previous checks")/* Crashing here won't break data consistency between 
customers
-         * and bank accounts, because of the idempotency.  Client will
-         * just have to retry.  */
+            ?: throw internalServerError("New customer INSERT failed despite 
the previous checks") // Crashing here won't break data consistency between 
customers // and bank accounts, because of the idempotency.  Client will // 
just have to retry.
         val maxDebt = ctx.defaultCustomerDebtLimit
+        val internalPayto: String = if (req.internal_payto_uri != null) {
+            stripIbanPayto(req.internal_payto_uri)
+        } else {
+            stripIbanPayto(genIbanPaytoUri())
+        }
         val newBankAccount = BankAccount(
             hasDebt = false,
-            internalPaytoUri = req.internal_payto_uri ?: genIbanPaytoUri(),
+            internalPaytoUri = internalPayto,
             owningCustomerId = newCustomerRowId,
             isPublic = req.is_public,
             isTalerExchange = req.is_taler_exchange,
@@ -157,12 +155,9 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         val newBankAccountId = db.bankAccountCreate(newBankAccount)
             ?: throw internalServerError("Could not INSERT bank account 
despite all the checks.")
 
-        /**
-         * The new account got created, now optionally award the registration
-         * bonus to it.  The configuration gets either a Taler amount (of the
-         * bonus), or null if no bonus is meant to be awarded.
-         */
-        val bonusAmount = if (ctx.registrationBonusEnabled) 
ctx.registrationBonus else null
+        // The new account got created, now optionally award the registration
+        // bonus to it.
+        val bonusAmount = if (ctx.registrationBonusEnabled && 
!req.is_taler_exchange) ctx.registrationBonus else null
         if (bonusAmount != null) {
             val adminCustomer =
                 db.customerGetFromLogin("admin") ?: throw 
internalServerError("Admin customer not found")
@@ -194,9 +189,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized("Login failed")
         val resourceName = call.maybeUriComponent("USERNAME") ?: throw 
badRequest(
             hint = "No username found in the URI", talerErrorCode = 
TalerErrorCode.TALER_EC_GENERIC_PARAMETER_MISSING
-        )
-        // Checking resource name only if Basic auth was used.
-        // Successful tokens do not need this check, they just pass.
+        ) // Checking resource name only if Basic auth was used. // Successful 
tokens do not need this check, they just pass.
         if (((c.login != resourceName) && (c.login != "admin")) && 
(call.getAuthToken() == null)) throw forbidden("No rights on the resource.")
         val customerData = db.customerGetFromLogin(c.login)
             ?: throw internalServerError("Customer '${c.login} despite being 
authenticated.'")
@@ -205,8 +198,11 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         val bankAccountData = db.bankAccountGetFromOwnerId(customerInternalId)
             ?: throw internalServerError("Customer '${c.login} had no bank 
account despite they are customer.'")
         val balance = Balance(
-            amount = bankAccountData.balance.toString(),
-            credit_debit_indicator = if (bankAccountData.hasDebt) { "debit" } 
else { "credit" }
+            amount = bankAccountData.balance.toString(), 
credit_debit_indicator = if (bankAccountData.hasDebt) {
+                "debit"
+            } else {
+                "credit"
+            }
         )
         call.respond(
             AccountData(
@@ -224,12 +220,11 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/accounts/{USERNAME}/withdrawals") {
-        val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
-        // Admin not allowed to withdraw in the name of customers:
+        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.
+        val req = call.receive<BankAccountCreateWithdrawalRequest>() // 
Checking that the user has enough funds.
         val b = db.bankAccountGetFromOwnerId(c.expectRowId())
             ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
         val withdrawalAmount = parseTalerAmount(req.amount)
@@ -239,8 +234,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         ) throw forbidden(
             hint = "Insufficient funds to withdraw with Taler",
             talerErrorCode = TalerErrorCode.TALER_EC_NONE // FIXME: need EC.
-        )
-        // Auth and funds passed, create the operation now!
+        ) // Auth and funds passed, create the operation now!
         val opId = UUID.randomUUID()
         if (!db.talerWithdrawalCreate(
                 opId, b.expectRowId(), withdrawalAmount
@@ -272,13 +266,11 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/withdrawals/{withdrawal_id}/abort") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
-        // Idempotency:
+        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).
+        } // 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
         )
@@ -287,25 +279,22 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     }
 
     post("/withdrawals/{withdrawal_id}/confirm") {
-        val op = getWithdrawal(db, call.expectUriComponent("withdrawal_id"))
-        // Checking idempotency:
+        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.
+        ) // 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.
-         */
+        ) // 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, getNowUs())) {
             WithdrawalConfirmationResult.BALANCE_INSUFFICIENT -> throw 
conflict(
                 "Insufficient funds", TalerErrorCode.TALER_EC_END // FIXME: 
define EC for this.
@@ -340,10 +329,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     get("/accounts/{USERNAME}/transactions") {
         val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized()
         val resourceName = call.expectUriComponent("USERNAME")
-        if (c.login != resourceName && c.login != "admin") throw forbidden()
-        // Collecting params.
-        val historyParams = getHistoryParams(call.request)
-        // Making the query.
+        if (c.login != resourceName && c.login != "admin") throw forbidden() 
// Collecting params.
+        val historyParams = getHistoryParams(call.request) // Making the query.
         val bankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
             ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
         val bankAccountId = bankAccount.expectRowId()
@@ -373,17 +360,14 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
     // Creates a bank transaction.
     post("/accounts/{USERNAME}/transactions") {
         val c = call.authenticateBankRequest(db, TokenScope.readwrite) ?: 
throw unauthorized()
-        val resourceName = call.expectUriComponent("USERNAME")
-        // admin has no rights here.
+        val resourceName = call.expectUriComponent("USERNAME") // admin has no 
rights here.
         if ((c.login != resourceName) && (call.getAuthToken() == null)) throw 
forbidden()
         val txData = call.receive<BankAccountTransactionCreate>()
-        // FIXME: make payto parser IBAN-agnostic?
         val payto = parsePayto(txData.payto_uri) ?: throw badRequest("Invalid 
creditor Payto")
-        val paytoWithoutParams = "payto://iban/${payto.bic}/${payto.iban}"
+        val paytoWithoutParams = stripIbanPayto(txData.payto_uri)
         val subject = payto.message ?: throw badRequest("Wire transfer lacks 
subject")
-        val debtorId = c.dbRowId ?: throw internalServerError("Debtor database 
ID not found")
-        // This performs already a SELECT on the bank account,
-        // like the wire transfer will do as well later!
+        val debtorId = c.dbRowId
+            ?: throw internalServerError("Debtor database ID not found") // 
This performs already a SELECT on the bank account, // like the wire transfer 
will do as well later!
         val creditorCustomerData = 
db.bankAccountGetFromInternalPayto(paytoWithoutParams) ?: throw notFound(
             "Creditor account not found", TalerErrorCode.TALER_EC_END // 
FIXME: define this EC.
         )
@@ -415,10 +399,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
 
     get("/accounts/{USERNAME}/transactions/{T_ID}") {
         val c = call.authenticateBankRequest(db, TokenScope.readonly) ?: throw 
unauthorized()
-        val accountOwner = call.expectUriComponent("USERNAME")
-        // auth ok, check rights.
-        if (c.login != "admin" && c.login != accountOwner) throw forbidden()
-        // rights ok, check tx exists.
+        val accountOwner = call.expectUriComponent("USERNAME") // auth ok, 
check rights.
+        if (c.login != "admin" && c.login != accountOwner) throw forbidden() 
// rights ok, check tx exists.
         val tId = call.expectUriComponent("T_ID")
         val txRowId = try {
             tId.toLong()
@@ -432,8 +414,7 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: 
BankApplicationContext) {
         )
         val customerBankAccount = db.bankAccountGetFromOwnerId(customerRowId)
             ?: throw internalServerError("Customer '${c.login}' lacks bank 
account.")
-        if (tx.bankAccountId != customerBankAccount.bankAccountId) throw 
forbidden("Client has no rights over the bank transaction: $tId")
-        // auth and rights, respond.
+        if (tx.bankAccountId != customerBankAccount.bankAccountId) throw 
forbidden("Client has no rights over the bank transaction: $tId") // auth and 
rights, respond.
         call.respond(
             BankAccountTransactionInfo(
                 amount = 
"${tx.amount.currency}:${tx.amount.value}.${tx.amount.frac}",
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 44d0b61a..68ef6038 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -34,7 +34,6 @@ import kotlin.math.abs
 
 private const val DB_CTR_LIMIT = 1000000
 
-
 fun Customer.expectRowId(): Long = this.dbRowId ?: throw 
internalServerError("Cutsomer '$login' had no DB row ID.")
 fun BankAccount.expectBalance(): TalerAmount = this.balance ?: throw 
internalServerError("Bank account '${this.internalPaytoUri}' lacks balance.")
 fun BankAccount.expectRowId(): Long = this.bankAccountId ?: throw 
internalServerError("Bank account '${this.internalPaytoUri}' lacks database row 
ID.")
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
index 51db01f2..499bf07e 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/IntegrationApiHandlers.kt
@@ -27,20 +27,19 @@ import io.ktor.server.response.*
 import io.ktor.server.routing.*
 import net.taler.common.errorcodes.TalerErrorCode
 import tech.libeufin.util.getBaseUrl
+import tech.libeufin.util.stripIbanPayto
 
 fun Routing.talerIntegrationHandlers(db: Database, ctx: 
BankApplicationContext) {
     get("/taler-integration/config") {
         val internalCurrency: String = ctx.currency
         call.respond(TalerIntegrationConfigResponse(currency = 
internalCurrency))
         return@get
-    }
-    // Note: wopid acts as an authentication token.
+    } // Note: wopid acts as an authentication token.
     get("/taler-integration/withdrawal-operation/{wopid}") {
         val wopid = call.expectUriComponent("wopid")
         val op = getWithdrawal(db, wopid) // throws 404 if not found.
         val relatedBankAccount = 
db.bankAccountGetFromOwnerId(op.walletBankAccount)
-        if (relatedBankAccount == null)
-            throw internalServerError("Bank has a withdrawal not related to 
any bank account.")
+        if (relatedBankAccount == null) throw internalServerError("Bank has a 
withdrawal not related to any bank account.")
         val suggestedExchange = ctx.suggestedWithdrawalExchange
         val walletCustomer = 
db.customerGetFromRowId(relatedBankAccount.owningCustomerId)
         if (walletCustomer == null)
@@ -65,31 +64,23 @@ fun Routing.talerIntegrationHandlers(db: Database, ctx: 
BankApplicationContext)
         val wopid = call.expectUriComponent("wopid")
         val req = call.receive<BankWithdrawalOperationPostRequest>()
         val op = getWithdrawal(db, wopid) // throws 404 if not found.
-        if (op.selectionDone) {
-            // idempotency
-            if (op.selectedExchangePayto != req.selected_exchange &&
-                op.reservePub != req.reserve_pub)
-                throw conflict(
-                    hint = "Cannot select different exchange and reserve pub. 
under the same withdrawal operation",
-                    talerEc = 
TalerErrorCode.TALER_EC_BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT
-                )
+        if (op.selectionDone) { // idempotency
+            if (op.selectedExchangePayto != req.selected_exchange && 
op.reservePub != req.reserve_pub) throw conflict(
+                hint = "Cannot select different exchange and reserve pub. 
under the same withdrawal operation",
+                talerEc = 
TalerErrorCode.TALER_EC_BANK_WITHDRAWAL_OPERATION_RESERVE_SELECTION_CONFLICT
+            )
         }
-        val dbSuccess: Boolean = if (!op.selectionDone) {
-            // Check if reserve pub. was used in _another_ withdrawal.
-            if (db.bankTransactionCheckExists(req.reserve_pub) != null)
-                throw conflict(
-                    "Reserve pub. already used",
-                    TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
-                )
-            val exchangePayto = req.selected_exchange
+        val dbSuccess: Boolean = if (!op.selectionDone) { // Check if reserve 
pub. was used in _another_ withdrawal.
+            if (db.bankTransactionCheckExists(req.reserve_pub) != null) throw 
conflict(
+                "Reserve pub. already used", 
TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+            )
+            val exchangePayto = stripIbanPayto(req.selected_exchange)
             db.talerWithdrawalSetDetails(
-                op.withdrawalUuid,
-                exchangePayto,
-                req.reserve_pub
+                op.withdrawalUuid, exchangePayto, req.reserve_pub
             )
-        }
-        else // DB succeeded in the past.
+        } else { // Nothing to do in the database, i.e. we were successful
             true
+        }
         if (!dbSuccess)
             // Whatever the problem, the bank missed it: respond 500.
             throw internalServerError("Bank failed at selecting the 
withdrawal.")
@@ -99,12 +90,9 @@ fun Routing.talerIntegrationHandlers(db: Database, ctx: 
BankApplicationContext)
                 baseUrl = ctx.spaCaptchaURL,
                 wopId = wopid
             )
-        }
-        else
-            null
+        } else null
         val resp = BankWithdrawalOperationPostResponse(
-            transfer_done = op.confirmationDone,
-            confirm_transfer_url = confirmUrl
+            transfer_done = op.confirmationDone, confirm_transfer_url = 
confirmUrl
         )
         call.respond(resp)
         return@post
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 8d4c4a2f..53a2df43 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -435,7 +435,12 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP 
server", name = "serve")
         val db = Database(dbConnStr, ctx.currency)
         if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper
             exitProcess(1)
-        embeddedServer(Netty, port = servePort) {
+        embeddedServer(Netty, port = servePort, configure = {
+            // Disable threads for now, the DB isn't thread safe yet.
+            connectionGroupSize = 1
+            workerGroupSize = 1
+            callGroupSize = 1
+        }) {
             corebankWebApp(db, ctx)
         }.start(wait = true)
     }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
index dda207b5..9c5cd9af 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -56,13 +56,15 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
         }
         val resp = IncomingHistory(credit_account = 
bankAccount.internalPaytoUri)
         history.forEach {
-            resp.incoming_transactions.add(IncomingReserveTransaction(
-                row_id = it.expectRowId(),
-                amount = it.amount.toString(),
-                date = it.transactionDate,
-                debit_account = it.debtorPaytoUri,
-                reserve_pub = it.subject
-            ))
+            resp.incoming_transactions.add(
+                IncomingReserveTransaction(
+                    row_id = it.expectRowId(),
+                    amount = it.amount.toString(),
+                    date = 
TalerProtocolTimestamp.fromMicroseconds(it.transactionDate),
+                    debit_account = it.debtorPaytoUri,
+                    reserve_pub = it.subject
+                )
+            )
         }
         call.respond(resp)
         return@get
@@ -81,10 +83,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
                         && maybeDoneAlready.exchangeBaseUrl == 
req.exchange_base_url
                         && maybeDoneAlready.wtid == req.wtid
             if (isIdempotent) {
-                call.respond(TransferResponse(
-                    timestamp = maybeDoneAlready.timestamp,
-                    row_id = maybeDoneAlready.debitTxRowId
-                ))
+                call.respond(
+                    TransferResponse(
+                        timestamp = 
TalerProtocolTimestamp.fromMicroseconds(maybeDoneAlready.timestamp),
+                        row_id = maybeDoneAlready.debitTxRowId
+                    )
+                )
                 return@post
             }
             throw conflict(
@@ -116,10 +120,12 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
             )
         val debitRowId = dbRes.txRowId
             ?: throw internalServerError("Database did not return the debit tx 
row ID")
-        call.respond(TransferResponse(
-            timestamp = transferTimestamp,
-            row_id = debitRowId
-        ))
+        call.respond(
+            TransferResponse(
+                timestamp = 
TalerProtocolTimestamp.fromMicroseconds(transferTimestamp),
+                row_id = debitRowId
+            )
+        )
         return@post
     }
 
@@ -169,8 +175,9 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: 
BankApplicationContext)
         call.respond(
             AddIncomingResponse(
                 row_id = rowId,
-                timestamp = txTimestamp
-        ))
+                timestamp = 
TalerProtocolTimestamp.fromMicroseconds(txTimestamp)
+            )
+        )
         return@post
     }
 }
diff --git a/bank/src/test/kotlin/LibeuFinApiTest.kt 
b/bank/src/test/kotlin/LibeuFinApiTest.kt
index 87d73985..176ec63b 100644
--- a/bank/src/test/kotlin/LibeuFinApiTest.kt
+++ b/bank/src/test/kotlin/LibeuFinApiTest.kt
@@ -290,6 +290,46 @@ class LibeuFinApiTest {
         }
     }
 
+    /**
+     * Testing the account creation and its idempotency
+     */
+    @Test
+    fun createTwoAccountsTest() {
+        testApplication {
+            val db = initDb()
+            val ctx = getTestContext()
+            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"
+                }""".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
      */
diff --git a/util/src/main/kotlin/Payto.kt b/util/src/main/kotlin/IbanPayto.kt
similarity index 87%
rename from util/src/main/kotlin/Payto.kt
rename to util/src/main/kotlin/IbanPayto.kt
index 026aecc3..daa6872c 100644
--- a/util/src/main/kotlin/Payto.kt
+++ b/util/src/main/kotlin/IbanPayto.kt
@@ -1,13 +1,12 @@
 package tech.libeufin.util
 
-import io.ktor.http.*
 import logger
 import java.net.URI
 import java.net.URLDecoder
 import java.net.URLEncoder
 
 // Payto information.
-data class Payto(
+data class IbanPayto(
     // represent query param "sender-name" or "receiver-name".
     val receiverName: String?,
     val iban: String,
@@ -27,7 +26,7 @@ private fun getQueryParamOrNull(name: String, params: 
List<Pair<String, String>>
 }
 
 // Parses a Payto URI, returning null if the input is invalid.
-fun parsePayto(payto: String): Payto? {
+fun parsePayto(payto: String): IbanPayto? {
     /**
      * This check is due because URIs having a "payto:" prefix without
      * slashes are correctly parsed by the Java 'URI' class.  'mailto'
@@ -74,7 +73,7 @@ fun parsePayto(payto: String): Payto? {
         }
     } else null
 
-    return Payto(
+    return IbanPayto(
         iban = iban,
         bic = bic,
         amount = getQueryParamOrNull("amount", params),
@@ -96,4 +95,17 @@ fun buildIbanPaytoUri(
         return "$ret&message=$messageUrlEnc"
     }
     return ret
-}
\ No newline at end of file
+}
+
+/**
+ * Strip a payto://iban URI of everything
+ * except the IBAN.
+ */
+fun stripIbanPayto(paytoUri: String): String {
+    val parsedPayto = parsePayto(paytoUri)
+    if (parsedPayto == null) {
+        throw Error("invalid payto://iban URI")
+    }
+    val canonIban = parsedPayto.iban.lowercase()
+    return "payto://iban/${canonIban}"
+}
diff --git a/util/src/test/kotlin/PaytoTest.kt 
b/util/src/test/kotlin/PaytoTest.kt
index c7174883..18fcb41b 100644
--- a/util/src/test/kotlin/PaytoTest.kt
+++ b/util/src/test/kotlin/PaytoTest.kt
@@ -1,5 +1,5 @@
 import org.junit.Test
-import tech.libeufin.util.Payto
+import tech.libeufin.util.IbanPayto
 import tech.libeufin.util.parsePayto
 
 class PaytoTest {
@@ -13,7 +13,7 @@ class PaytoTest {
 
     @Test
     fun parsePaytoTest() {
-        val withBic: Payto = 
parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")!!
+        val withBic: IbanPayto = 
parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")!!
         assert(withBic.iban == "IBAN123")
         assert(withBic.bic == "BIC123")
         assert(withBic.receiverName == "The Name")

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