gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (cc35923a -> 50bf46ce)


From: gnunet
Subject: [libeufin] branch master updated (cc35923a -> 50bf46ce)
Date: Tue, 14 Nov 2023 18:43:14 +0100

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

antoine pushed a change to branch master
in repository libeufin.

    from cc35923a nexus fetch, adding:
     new 7ffa7a9f Track used iban and improve iban verification
     new 50bf46ce Enforce transaction serialization

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


Summary of changes:
 .../main/kotlin/tech/libeufin/bank/TalerCommon.kt  | 81 ++++++++++++++--------
 .../kotlin/tech/libeufin/bank/WireGatewayApi.kt    |  1 -
 .../kotlin/tech/libeufin/bank/db/CashoutDAO.kt     | 10 +--
 .../kotlin/tech/libeufin/bank/db/ConversionDAO.kt  |  2 +-
 .../main/kotlin/tech/libeufin/bank/db/Database.kt  | 54 ++++++++++++---
 .../kotlin/tech/libeufin/bank/db/ExchangeDAO.kt    |  4 +-
 .../kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt  |  8 +--
 bank/src/test/kotlin/AmountTest.kt                 |  6 +-
 bank/src/test/kotlin/BankIntegrationApiTest.kt     |  7 +-
 bank/src/test/kotlin/CoreBankApiTest.kt            | 35 ++++------
 bank/src/test/kotlin/DatabaseTest.kt               | 33 +++++++++
 bank/src/test/kotlin/SecurityTest.kt               |  6 +-
 bank/src/test/kotlin/StatsTest.kt                  |  2 +-
 bank/src/test/kotlin/WireGatewayApiTest.kt         | 46 +++++-------
 bank/src/test/kotlin/helpers.kt                    | 24 +++++--
 database-versioning/libeufin-bank-0001.sql         | 20 +++---
 16 files changed, 215 insertions(+), 124 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
index 24fa3f02..93c254a6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -29,6 +29,7 @@ import java.time.Instant
 import java.time.temporal.ChronoUnit
 import java.util.concurrent.TimeUnit
 import java.util.*
+import java.math.BigInteger
 import java.net.*
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.descriptors.*
@@ -43,9 +44,7 @@ import org.slf4j.event.Level
 private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.TalerCommon")
 const val MAX_SAFE_INTEGER = 9007199254740991L; // 2^53 - 1
 
-/**
- * 32-byte Crockford's Base32 encoded data.
- */
+/** 32-byte Crockford's Base32 encoded data */
 @Serializable(with = Base32Crockford32B.Serializer::class)
 class Base32Crockford32B {
     private var encoded: String? = null
@@ -98,9 +97,7 @@ class Base32Crockford32B {
     }
 }
 
-/**
- * 64-byte Crockford's Base32 encoded data.
- */
+/** 64-byte Crockford's Base32 encoded data */
 @Serializable(with = Base32Crockford64B.Serializer::class)
 class Base32Crockford64B {
     private var encoded: String? = null
@@ -153,9 +150,9 @@ class Base32Crockford64B {
     }
 }
 
-/** 32-byte hash code. */
+/** 32-byte hash code */
 typealias ShortHashCode = Base32Crockford32B;
-/** 64-byte hash code. */
+/** 64-byte hash code */
 typealias HashCode = Base32Crockford64B;
 /**
  * EdDSA and ECDHE public keys always point on Curve25519
@@ -164,9 +161,7 @@ typealias HashCode = Base32Crockford64B;
  */
 typealias EddsaPublicKey = Base32Crockford32B;
 
-/**
- * Timestamp containing the number of seconds since epoch.
- */
+/** Timestamp containing the number of seconds since epoch */
 @Serializable
 data class TalerProtocolTimestamp(
     @Serializable(with = TalerProtocolTimestamp.Serializer::class)
@@ -234,18 +229,31 @@ class TalerAmount {
         this.currency = currency
     }
     constructor(encoded: String) {
-        fun badAmount(hint: String): Exception = 
-            badRequest(hint, TalerErrorCode.BANK_BAD_FORMAT_AMOUNT)
-        
-        val match = PATTERN.matchEntire(encoded) ?: throw badAmount("Invalid 
amount format");
+        val match = PATTERN.matchEntire(encoded) ?: throw badRequest(
+            "Invalid amount format",
+            TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+        );
         val (currency, value, frac) = match.destructured
         this.currency = currency
-        this.value = value.toLongOrNull() ?: throw badAmount("Invalid value")
-        if (this.value > MAX_VALUE) throw badAmount("Value specified in amount 
is too large")
+        this.value = value.toLongOrNull() ?: throw badRequest(
+            "Invalid value",
+            TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+        )
+        if (this.value > MAX_VALUE) throw badRequest(
+            "Value specified in amount is too large",
+            TalerErrorCode.BANK_NUMBER_TOO_BIG
+        )
         this.frac = if (frac.isEmpty()) {
             0
         } else {
-            var tmp = frac.toIntOrNull() ?: throw badAmount("Invalid 
fractional value")
+            var tmp = frac.toIntOrNull() ?: throw badRequest(
+                "Invalid fractional value",
+                TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+            )
+            if (tmp > FRACTION_BASE) throw badRequest(
+                "Fractional calue specified in amount is too large",
+                TalerErrorCode.BANK_NUMBER_TOO_BIG
+            )
             repeat(8 - frac.length) {
                 tmp *= 10
             }
@@ -295,17 +303,14 @@ class DecimalNumber {
     val frac: Int
 
     constructor(encoded: String) {
-        fun badAmount(hint: String): Exception = 
-            badRequest(hint, TalerErrorCode.BANK_BAD_FORMAT_AMOUNT)
-        
-        val match = PATTERN.matchEntire(encoded) ?: throw badAmount("Invalid 
decimal number format");
+        val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid 
decimal number format");
         val (value, frac) = match.destructured
-        this.value = value.toLongOrNull() ?: throw badAmount("Invalid value")
-        if (this.value > TalerAmount.MAX_VALUE) throw badAmount("Value 
specified in decimal number is too large")
+        this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
+        if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value 
specified in decimal number is too large")
         this.frac = if (frac.isEmpty()) {
             0
         } else {
-            var tmp = frac.toIntOrNull() ?: throw badAmount("Invalid 
fractional value")
+            var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid 
fractional value")
             repeat(8 - frac.length) {
                 tmp *= 10
             }
@@ -422,6 +427,7 @@ sealed class PaytoUri {
 class IbanPayTo: PaytoUri {
     val parsed: URI
     val canonical: String
+    val iban: String
     override val amount: TalerAmount?
     override val message: String?
     override val receiverName: String?
@@ -433,8 +439,9 @@ class IbanPayTo: PaytoUri {
 
         val splitPath = parsed.path.split("/").filter { it.isNotEmpty() }
         require(splitPath.size < 3 && splitPath.isNotEmpty()) { "too many path 
segments" }
-        val iban = (if (splitPath.size == 1) splitPath[0] else 
splitPath[1]).replace("-", "").uppercase()
-        // TODO normalize && check IBAN ?
+        val rawIban = if (splitPath.size == 1) splitPath[0] else splitPath[1]
+        iban = rawIban.uppercase().replace(SEPARATOR, "")
+        checkIban(iban)
         canonical = "payto://iban/$iban"
     
         val params = (parsed.query ?: "").parseUrlEncodedParameters();
@@ -443,6 +450,8 @@ class IbanPayTo: PaytoUri {
         receiverName = params["receiver-name"]
     }
 
+    override fun toString(): String = canonical
+
     internal object Serializer : KSerializer<IbanPayTo> {
         override val descriptor: SerialDescriptor =
                 PrimitiveSerialDescriptor("IbanPayTo", PrimitiveKind.STRING)
@@ -455,4 +464,22 @@ class IbanPayTo: PaytoUri {
             return IbanPayTo(decoder.decodeString())
         }
     }
+
+    companion object {
+        private val SEPARATOR = Regex("[\\ \\-]");
+
+        fun checkIban(iban: String) {
+            val builder = StringBuilder(iban.length + iban.asSequence().map { 
if (it.isDigit()) 1 else 2 }.sum())
+            (iban.subSequence(4, iban.length).asSequence() + 
iban.subSequence(0, 4).asSequence()).forEach {
+                if (it.isDigit()) {
+                    builder.append(it)
+                } else {
+                    builder.append((it.code - 'A'.code) + 10)
+                }
+            }
+            val str = builder.toString()
+            val mod = str.toBigInteger().mod(97.toBigInteger()).toInt();
+            if (mod != 1) throw badRequest("Iban malformed, modulo is $mod 
expected 1")
+        }
+    }
 }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
index 6046d701..ea7ab082 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -31,7 +31,6 @@ import net.taler.common.errorcodes.TalerErrorCode
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import tech.libeufin.util.extractReservePubFromSubject
-import tech.libeufin.util.stripIbanPayto
 import java.time.Instant
 import kotlin.math.abs
 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
index f9c0fb22..0bc0bffd 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -67,7 +67,7 @@ class CashoutDAO(private val db: Database) {
         now: Instant,
         retryCounter: Int,
         validityPeriod: Duration
-    ): CashoutCreation = db.conn { conn ->
+    ): CashoutCreation = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 out_bad_conversion,
@@ -121,7 +121,7 @@ class CashoutDAO(private val db: Database) {
         id: Long,
         now: Instant,
         retransmissionPeriod: Duration
-    ) = db.conn { conn ->
+    ) = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT challenge_mark_sent(challenge, ?, ?)
             FROM cashout_operations
@@ -133,7 +133,7 @@ class CashoutDAO(private val db: Database) {
         stmt.executeQueryCheck()
     }
 
-    suspend fun abort(id: Long): AbortResult = db.conn { conn ->
+    suspend fun abort(id: Long): AbortResult = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             UPDATE cashout_operations
             SET aborted = local_transaction IS NULL
@@ -152,7 +152,7 @@ class CashoutDAO(private val db: Database) {
         id: Long,
         tanCode: String,
         timestamp: Instant
-    ): CashoutConfirmationResult = db.conn { conn ->
+    ): CashoutConfirmationResult = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 out_no_op,
@@ -188,7 +188,7 @@ class CashoutDAO(private val db: Database) {
         CONFLICT_ALREADY_CONFIRMED
     }
 
-    suspend fun delete(id: Long): CashoutDeleteResult = db.conn { conn ->
+    suspend fun delete(id: Long): CashoutDeleteResult = db.serializable { conn 
->
         val stmt = conn.prepareStatement("""
            SELECT out_already_confirmed
              FROM cashout_delete(?)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
index 0139576f..e4b8d1f3 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -26,7 +26,7 @@ import java.util.concurrent.TimeUnit
 import tech.libeufin.util.*
 
 class ConversionDAO(private val db: Database) {
-    suspend fun updateConfig(cfg: ConversionInfo) = db.conn {
+    suspend fun updateConfig(cfg: ConversionInfo) = db.serializable {
         it.transaction { conn -> 
             var stmt = conn.prepareStatement("CALL config_set_amount(?, (?, 
?)::taler_amount)")
             for ((name, amount) in listOf(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
index cb00f7e9..47f86080 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -33,8 +33,11 @@ import kotlinx.coroutines.flow.*
 import kotlinx.coroutines.*
 import com.zaxxer.hikari.*
 import tech.libeufin.util.*
+import io.ktor.http.HttpStatusCode
+import net.taler.common.errorcodes.TalerErrorCode
 
 private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.Database")
+private val SERIALIZATION_RETRY: Int = 10;
 
 /**
  * This error occurs in case the timestamp took by the bank for some
@@ -63,7 +66,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         val pgSource = pgDataSource(dbConfig)
         val config = HikariConfig();
         config.dataSource = pgSource
-        config.connectionInitSql = "SET search_path TO libeufin_bank;"
+        config.connectionInitSql = "SET search_path TO libeufin_bank;SET 
SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL SERIALIZABLE;"
         config.validate()
         dbPool = HikariDataSource(config);
         notifWatcher = NotificationWatcher(pgSource)
@@ -86,13 +89,31 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         }
     }
 
+
+    suspend fun <R> serializable(lambda: suspend (PgConnection) -> R): R = 
conn { conn ->
+        repeat(SERIALIZATION_RETRY) {
+            try {
+                return@conn lambda(conn);
+            } catch (e: SQLException) {
+                logger.error(e.message)
+                if (e.sqlState != "40001") // serialization_failure
+                    throw e // rethrowing, not to hide other types of errors.
+            }
+        }
+        throw libeufinError(
+            HttpStatusCode.InternalServerError,
+            "Transaction serialization failure",
+            TalerErrorCode.BANK_SOFT_EXCEPTION
+        )
+    }
+
     // CUSTOMERS
 
     /**
      * Deletes a customer (including its bank account row) from
      * the database.  The bank account gets deleted by the cascade.
      */
-    suspend fun customerDeleteIfBalanceIsZero(login: String): 
CustomerDeletionResult = conn { conn ->
+    suspend fun customerDeleteIfBalanceIsZero(login: String): 
CustomerDeletionResult = serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               out_nx_customer,
@@ -138,7 +159,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         expirationTime: Instant,
         scope: TokenScope,
         isRefreshable: Boolean
-    ): Boolean = conn { conn ->
+    ): Boolean = serializable { conn ->
         val bankCustomer = conn.prepareStatement("""
             SELECT customer_id FROM customers WHERE login=?
         """).run {
@@ -192,7 +213,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
      * if deletion succeeds or false if the token could not be
      * deleted (= not found).
      */
-    suspend fun bearerTokenDelete(token: ByteArray): Boolean = conn { conn ->
+    suspend fun bearerTokenDelete(token: ByteArray): Boolean = serializable { 
conn ->
         val stmt = conn.prepareStatement("""
             DELETE FROM bearer_tokens
               WHERE content = ?
@@ -216,7 +237,8 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         isTalerExchange: Boolean,
         maxDebt: TalerAmount,
         bonus: TalerAmount?
-    ): CustomerCreationResult = conn { it ->
+    ): CustomerCreationResult = serializable { it ->
+        val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank();
         it.transaction { conn ->
             val idempotent = conn.prepareStatement("""
                 SELECT password_hash, name=?
@@ -270,6 +292,20 @@ class Database(dbConfig: String, internal val 
bankCurrency: String, internal val
                     setString(6, cashoutPayto?.canonical)
                     oneOrNull { it.getLong("customer_id") }!!
                 }
+
+                conn.prepareStatement("""
+                    INSERT INTO iban_history(
+                        iban
+                        ,creation_time
+                    ) VALUES (?, ?)
+                """).run {
+                    setString(1, internalPaytoUri.iban)
+                    setLong(2, now)
+                    if (!executeUpdateViolation()) {
+                        conn.rollback()
+                        return@transaction 
CustomerCreationResult.CONFLICT_PAY_TO
+                    }
+                }
             
                 conn.prepareStatement("""
                     INSERT INTO bank_accounts(
@@ -300,7 +336,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
                         setString(1, internalPaytoUri.canonical)
                         setLong(2, bonus.value)
                         setInt(3, bonus.frac)
-                        setLong(4, Instant.now().toDbMicros() ?: throw 
faultyTimestampByBank())
+                        setLong(4, now)
                         executeQuery().use {
                             when {
                                 !it.next() -> throw internalServerError("Bank 
transaction didn't properly return")
@@ -394,7 +430,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         isTalerExchange: Boolean?,
         debtLimit: TalerAmount?,
         isAdmin: Boolean
-    ): CustomerPatchResult = conn { conn ->
+    ): CustomerPatchResult = serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 out_not_found,
@@ -429,7 +465,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         }
     }
 
-    suspend fun accountReconfigPassword(login: String, newPw: String, oldPw: 
String?): CustomerPatchAuthResult = conn {
+    suspend fun accountReconfigPassword(login: String, newPw: String, oldPw: 
String?): CustomerPatchAuthResult = serializable {
         it.transaction { conn ->
             val currentPwh = conn.prepareStatement("""
                 SELECT password_hash FROM customers WHERE login=?
@@ -635,7 +671,7 @@ class Database(dbConfig: String, internal val bankCurrency: 
String, internal val
         subject: String,
         amount: TalerAmount,
         timestamp: Instant,
-    ): Pair<BankTransactionResult, Long?> = conn { conn ->
+    ): Pair<BankTransactionResult, Long?> = serializable { conn ->
         conn.transaction {
             val stmt = conn.prepareStatement("""
                 SELECT 
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
index 94539546..03d7a97a 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -127,7 +127,7 @@ class ExchangeDAO(private val db: Database) {
         req: TransferRequest,
         username: String,
         timestamp: Instant
-    ): TransferResult = db.conn { conn ->
+    ): TransferResult = db.serializable { conn ->
         val subject = OutgoingTxMetadata(req.wtid, 
req.exchange_base_url).encode()
         val stmt = conn.prepareStatement("""
             SELECT
@@ -195,7 +195,7 @@ class ExchangeDAO(private val db: Database) {
         req: AddIncomingRequest,
         username: String,
         timestamp: Instant
-        ): AddIncomingResult = db.conn { conn ->
+        ): AddIncomingResult = db.serializable { conn ->
             val subject = IncomingTxMetadata(req.reserve_pub).encode()
         val stmt = conn.prepareStatement("""
             SELECT
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
index 014abca8..201b4a41 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -58,7 +58,7 @@ class WithdrawalDAO(private val db: Database) {
         walletAccountUsername: String,
         uuid: UUID,
         amount: TalerAmount
-    ): WithdrawalCreationResult = db.conn { conn ->
+    ): WithdrawalCreationResult = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 out_account_not_found,
@@ -147,7 +147,7 @@ class WithdrawalDAO(private val db: Database) {
      * Aborts one Taler withdrawal, only if it wasn't previously
      * confirmed.  It returns false if the UPDATE didn't succeed.
      */
-    suspend fun abort(uuid: UUID): AbortResult = db.conn { conn ->
+    suspend fun abort(uuid: UUID): AbortResult = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             UPDATE taler_withdrawal_operations
             SET aborted = NOT confirmation_done
@@ -174,7 +174,7 @@ class WithdrawalDAO(private val db: Database) {
         uuid: UUID,
         exchangePayto: IbanPayTo,
         reservePub: EddsaPublicKey
-    ): Pair<WithdrawalSelectionResult, Boolean> = db.conn { conn ->
+    ): Pair<WithdrawalSelectionResult, Boolean> = db.serializable { conn ->
         val subject = IncomingTxMetadata(reservePub).encode()
         val stmt = conn.prepareStatement("""
             SELECT
@@ -213,7 +213,7 @@ class WithdrawalDAO(private val db: Database) {
     suspend fun confirm(
         uuid: UUID,
         now: Instant
-    ): WithdrawalConfirmationResult = db.conn { conn ->
+    ): WithdrawalConfirmationResult = db.serializable { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               out_no_op,
diff --git a/bank/src/test/kotlin/AmountTest.kt 
b/bank/src/test/kotlin/AmountTest.kt
index a1a93a44..3e0ba261 100644
--- a/bank/src/test/kotlin/AmountTest.kt
+++ b/bank/src/test/kotlin/AmountTest.kt
@@ -31,13 +31,13 @@ class AmountTest {
     @Test
     fun computationTest() = bankSetup { db ->  
         val conn = db.dbPool.getConnection().unwrap(PgConnection::class.java)
-        conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val 
= 100000 WHERE internal_payto_uri = 
'${IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ").canonical}'")
+        conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val 
= 100000 WHERE internal_payto_uri = '$exchangePayto'")
         val stmt = conn.prepareStatement("""
             UPDATE libeufin_bank.bank_accounts 
                 SET balance = (?, ?)::taler_amount
                     ,has_debt = ?
                     ,max_debt = (?, ?)::taler_amount
-            WHERE internal_payto_uri = 
'${IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ").canonical}'
+            WHERE internal_payto_uri = '$merchantPayto'
         """)
         suspend fun routine(balance: TalerAmount, due: TalerAmount, 
hasBalanceDebt: Boolean, maxDebt: TalerAmount): Boolean {
             stmt.setLong(1, balance.value)
@@ -49,7 +49,7 @@ class AmountTest {
             // Check bank transaction
             stmt.executeUpdate()
             val (txRes, _) = db.bankTransaction(
-                creditAccountPayto = 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"),
+                creditAccountPayto = exchangePayto,
                 debitAccountUsername = "merchant",
                 subject = "test",
                 amount = due,
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt 
b/bank/src/test/kotlin/BankIntegrationApiTest.kt
index 44731890..f81fcec6 100644
--- a/bank/src/test/kotlin/BankIntegrationApiTest.kt
+++ b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -11,7 +11,6 @@ import net.taler.common.errorcodes.TalerErrorCode
 import org.junit.Test
 import tech.libeufin.bank.*
 import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.stripIbanPayto
 import java.util.*
 import java.time.Instant
 import kotlin.test.*
@@ -53,7 +52,7 @@ class BankIntegrationApiTest {
         val reserve_pub = randEddsaPublicKey()
         val req = json {
             "reserve_pub" to reserve_pub
-            "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+            "selected_exchange" to exchangePayto
         }
 
         // Check bad UUID
@@ -104,14 +103,14 @@ class BankIntegrationApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/UNKNOWN-IBAN-XYZ")
+                    "selected_exchange" to unknownPayto
                 }
             }.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
             // Check account not exchange
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")
+                    "selected_exchange" to merchantPayto
                 }
             }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
         }
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt 
b/bank/src/test/kotlin/CoreBankApiTest.kt
index 91eab851..c5a8d207 100644
--- a/bank/src/test/kotlin/CoreBankApiTest.kt
+++ b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -634,7 +634,7 @@ class CoreBankTransactionsApiTest {
         authRoutine("/accounts/merchant/transactions/1", method = 
HttpMethod.Get)
 
         // Create transaction
-        tx("merchant", "KUDOS:0.3", "exchange")
+        tx("merchant", "KUDOS:0.3", "exchange", "tx")
         // Check OK
         client.get("/accounts/merchant/transactions/1") {
             basicAuth("merchant", "merchant-password")
@@ -657,7 +657,7 @@ class CoreBankTransactionsApiTest {
     @Test
     fun create() = bankSetup { _ -> 
         val valid_req = json {
-            "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
+            "payto_uri" to "$exchangePayto?message=payout"
             "amount" to "KUDOS:0.3"
         }
 
@@ -682,7 +682,7 @@ class CoreBankTransactionsApiTest {
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody {
-                "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout2&amount=KUDOS:1.05"
+                "payto_uri" to 
"$exchangePayto?message=payout2&amount=KUDOS:1.05"
             }
         }.assertOk().run {
             val id = json<TransactionCreateResponse>().row_id
@@ -699,7 +699,7 @@ class CoreBankTransactionsApiTest {
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody {
-                "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout3&amount=KUDOS:1.05"
+                "payto_uri" to 
"$exchangePayto?message=payout3&amount=KUDOS:1.05"
                 "amount" to "KUDOS:10.003"
             }
         }.assertOk().run {
@@ -732,7 +732,7 @@ class CoreBankTransactionsApiTest {
             basicAuth("merchant", "merchant-password")
             contentType(ContentType.Application.Json)
             jsonBody(valid_req) {
-                "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ"
+                "payto_uri" to "$exchangePayto"
             }
         }.assertBadRequest()
         // Unknown creditor
@@ -740,7 +740,7 @@ class CoreBankTransactionsApiTest {
             basicAuth("merchant", "merchant-password")
             contentType(ContentType.Application.Json)
             jsonBody(valid_req) {
-                "payto_uri" to "payto://iban/UNKNOWN-IBAN-XYZ?message=payout"
+                "payto_uri" to "$unknownPayto?message=payout"
             }
         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
         // Transaction to self
@@ -748,7 +748,7 @@ class CoreBankTransactionsApiTest {
             basicAuth("merchant", "merchant-password")
             contentType(ContentType.Application.Json)
             jsonBody(valid_req) {
-                "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout"
+                "payto_uri" to "$merchantPayto?message=payout"
             }
         }.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT)
 
@@ -782,17 +782,12 @@ class CoreBankTransactionsApiTest {
         checkBalance(true, "KUDOS:2.4", false, "KUDOS:0")
         // Send 2 times 3
         repeat(2) {
-            client.post("/accounts/merchant/transactions") {
-                basicAuth("merchant", "merchant-password")
-                jsonBody {
-                    "payto_uri" to 
"payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3"
-                }
-            }.assertOk()
+            tx("merchant", "KUDOS:3", "customer")
         }
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody {
-                "payto_uri" to 
"payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3"
+                "payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:3"
             }
         }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
         checkBalance(true, "KUDOS:8.4", false, "KUDOS:6")
@@ -800,7 +795,7 @@ class CoreBankTransactionsApiTest {
         client.post("/accounts/customer/transactions") {
             basicAuth("customer", "customer-password")
             jsonBody {
-                "payto_uri" to 
"payto://iban/MERCHANT-IBAN-XYZ?message=payout2&amount=KUDOS:10"
+                "payto_uri" to "$merchantPayto?message=payout2&amount=KUDOS:10"
             }
         }.assertOk()
         checkBalance(false, "KUDOS:1.6", true, "KUDOS:4")
@@ -879,7 +874,7 @@ class CoreBankWithdrawalApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
 
@@ -899,7 +894,7 @@ class CoreBankWithdrawalApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
             client.post("/withdrawals/$uuid/confirm").assertNoContent()
@@ -943,7 +938,7 @@ class CoreBankWithdrawalApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
 
@@ -963,7 +958,7 @@ class CoreBankWithdrawalApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
             client.post("/withdrawals/$uuid/abort").assertNoContent()
@@ -983,7 +978,7 @@ class CoreBankWithdrawalApiTest {
             client.post("/taler-integration/withdrawal-operation/$uuid") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
 
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index 55503c44..e9f13593 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -30,6 +30,12 @@ import java.util.UUID
 import java.util.concurrent.TimeUnit
 import kotlin.experimental.inv
 import kotlin.test.*
+import kotlinx.coroutines.*
+import io.ktor.http.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.client.HttpClient
 
 class DatabaseTest {
     // Testing the helper that update conversion config
@@ -49,6 +55,33 @@ class DatabaseTest {
         assert(maybeCreateAdminAccount(db, ctx))
     }
 
+    @Test
+    fun serialisation() = bankSetup {
+        assertBalance("customer", CreditDebitInfo.credit, "KUDOS:0")
+        assertBalance("merchant", CreditDebitInfo.credit, "KUDOS:0")
+        coroutineScope {
+            repeat(10) { 
+                launch {
+                    tx("customer", "KUDOS:0.$it", "merchant", "concurrent $it")
+                }
+            }
+        }
+        assertBalance("customer", CreditDebitInfo.debit, "KUDOS:4.5")
+        assertBalance("merchant", CreditDebitInfo.credit, "KUDOS:4.5")
+        coroutineScope {
+            repeat(5) { 
+                launch {
+                    tx("customer", "KUDOS:0.0$it", "merchant", "concurrent 
0$it")
+                }
+                launch {
+                    client.get("/accounts/merchant/transactions") {
+                        basicAuth("merchant", "merchant-password")
+                    }.assertOk()
+                }
+            }
+        }
+    }
+
     @Test
     fun challenge() = setup { db, _ -> db.conn { conn ->
         val createStmt = conn.prepareStatement("SELECT 
challenge_create(?,?,?,?)")
diff --git a/bank/src/test/kotlin/SecurityTest.kt 
b/bank/src/test/kotlin/SecurityTest.kt
index 90aac1cf..f3079aa6 100644
--- a/bank/src/test/kotlin/SecurityTest.kt
+++ b/bank/src/test/kotlin/SecurityTest.kt
@@ -35,7 +35,7 @@ class SecurityTest {
     @Test
     fun bodySizeLimit() = bankSetup { _ ->
         val valid_req = json {
-            "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
+            "payto_uri" to "$exchangePayto?message=payout"
             "amount" to "KUDOS:0.3"
         }
         client.post("/accounts/merchant/transactions") {
@@ -47,7 +47,7 @@ class SecurityTest {
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody(valid_req) {
-                "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+                "payto_uri" to 
"$exchangePayto?message=payout${"A".repeat(4100)}"
             }
         }.assertBadRequest()
 
@@ -55,7 +55,7 @@ class SecurityTest {
         client.post("/accounts/merchant/transactions") {
             basicAuth("merchant", "merchant-password")
             jsonBody(valid_req, deflate = true) {
-                "payto_uri" to 
"payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+                "payto_uri" to 
"$exchangePayto?message=payout${"A".repeat(4100)}"
             }
         }.assertBadRequest()
     }
diff --git a/bank/src/test/kotlin/StatsTest.kt 
b/bank/src/test/kotlin/StatsTest.kt
index 5dc3872e..b331468f 100644
--- a/bank/src/test/kotlin/StatsTest.kt
+++ b/bank/src/test/kotlin/StatsTest.kt
@@ -53,7 +53,7 @@ class StatsTest {
             db.conn { conn ->
                 val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, 
(?, ?)::taler_amount, ?)")
                 stmt.setLong(1, Instant.now().toDbMicros()!!)
-                stmt.setString(2, 
IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ").canonical)
+                stmt.setString(2, customerPayto.canonical)
                 val amount = TalerAmount(amount)
                 stmt.setLong(3, amount.value)
                 stmt.setInt(4, amount.frac)
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt 
b/bank/src/test/kotlin/WireGatewayApiTest.kt
index a3ebbc40..143420b0 100644
--- a/bank/src/test/kotlin/WireGatewayApiTest.kt
+++ b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -17,18 +17,6 @@ import kotlin.test.assertNotNull
 import randHashCode
 
 class WireGatewayApiTest {
-    suspend fun Database.genTransaction(from: String, to: IbanPayTo, subject: 
String) {
-        bankTransaction(
-            creditAccountPayto = to,
-            debitAccountUsername = from,
-            subject = subject,
-            amount = TalerAmount("KUDOS:10"),
-            timestamp = Instant.now(),
-        ).run {
-            assertEquals(BankTransactionResult.SUCCESS, first)
-        }
-    }
-
     // Test endpoint is correctly authenticated 
     suspend fun ApplicationTestBuilder.authRoutine(path: String, body: 
JsonObject? = null, method: HttpMethod = HttpMethod.Post, requireAdmin: Boolean 
= false) {
         // No body when authentication must happen before parsing the body
@@ -78,7 +66,7 @@ class WireGatewayApiTest {
             "amount" to "KUDOS:55"
             "exchange_base_url" to "http://exchange.example.com/";
             "wtid" to randShortHashCode()
-            "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+            "credit_account" to merchantPayto
         };
 
         authRoutine("/accounts/merchant/taler-wire-gateway/transfer", 
valid_req)
@@ -125,7 +113,7 @@ class WireGatewayApiTest {
             jsonBody(valid_req) { 
                 "request_uid" to randHashCode()
                 "wtid" to randShortHashCode()
-                "credit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+                "credit_account" to unknownPayto
             }
         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
 
@@ -135,7 +123,7 @@ class WireGatewayApiTest {
             jsonBody(valid_req) { 
                 "request_uid" to randHashCode()
                 "wtid" to randShortHashCode()
-                "credit_account" to "payto://iban/EXCHANGE-IBAN-XYZ"
+                "credit_account" to exchangePayto
             }
         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
 
@@ -176,7 +164,7 @@ class WireGatewayApiTest {
      * Testing the /history/incoming call from the TWG API.
      */
     @Test
-    fun historyIncoming() = bankSetup { db -> 
+    fun historyIncoming() = bankSetup { 
         // Give Foo reasonable debt allowance:
         setMaxDebt("merchant", TalerAmount("KUDOS:1000"))
 
@@ -198,11 +186,11 @@ class WireGatewayApiTest {
             addIncoming("KUDOS:10")
         }
         // Should not show up in the taler wire gateway API history
-        db.genTransaction("merchant", 
IbanPayTo("payto://iban/exchange-IBAN-XYZ"), "bogus")
+        tx("merchant", "KUDOS:10", "exchange", "bogus")
         // Exchange pays merchant once, but that should not appear in the 
result
-        db.genTransaction("exchange", 
IbanPayTo("payto://iban/merchant-IBAN-XYZ"), "ignored")
+        tx("exchange", "KUDOS:10", "merchant", "ignored")
         // Gen one transaction using raw bank transaction logic
-        db.genTransaction("merchant", 
IbanPayTo("payto://iban/exchange-IBAN-XYZ"), 
IncomingTxMetadata(randShortHashCode()).encode())
+        tx("merchant", "KUDOS:10", "exchange", 
IncomingTxMetadata(randShortHashCode()).encode())
         // Gen one transaction using withdraw logic
         client.post("/accounts/merchant/withdrawals") {
             basicAuth("merchant", "merchant-password")
@@ -213,7 +201,7 @@ class WireGatewayApiTest {
             client.post("/taler-integration/withdrawal-operation/${uuid}") {
                 jsonBody {
                     "reserve_pub" to randEddsaPublicKey()
-                    "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                    "selected_exchange" to exchangePayto
                 }
             }.assertOk()
             client.post("/withdrawals/${uuid}/confirm") {
@@ -274,7 +262,7 @@ class WireGatewayApiTest {
                 }
             }
             delay(200)
-            db.genTransaction("merchant", 
IbanPayTo("payto://iban/exchange-IBAN-XYZ"), 
IncomingTxMetadata(randShortHashCode()).encode())
+            tx("merchant", "KUDOS:10", "exchange", 
IncomingTxMetadata(randShortHashCode()).encode())
         }
 
         // Test trigger by withdraw operationr
@@ -296,7 +284,7 @@ class WireGatewayApiTest {
                 client.post("/taler-integration/withdrawal-operation/${uuid}") 
{
                     jsonBody {
                         "reserve_pub" to randEddsaPublicKey()
-                        "selected_exchange" to 
IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+                        "selected_exchange" to exchangePayto
                     }
                 }.assertOk()
                 client.post("/withdrawals/${uuid}/confirm") {
@@ -326,7 +314,7 @@ class WireGatewayApiTest {
      * Testing the /history/outgoing call from the TWG API.
      */
     @Test
-    fun historyOutgoing() = bankSetup { db -> 
+    fun historyOutgoing() = bankSetup {
         setMaxDebt("exchange", TalerAmount("KUDOS:1000000"))
 
         suspend fun HttpResponse.assertHistory(size: Int) {
@@ -347,12 +335,12 @@ class WireGatewayApiTest {
             transfer("KUDOS:10")
         }
         // Should not show up in the taler wire gateway API history
-        db.genTransaction("exchange", 
IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), "bogus")
+        tx("exchange", "KUDOS:10", "merchant", "bogus")
         // Merchant pays exchange once, but that should not appear in the 
result
-        db.genTransaction("merchant", 
IbanPayTo("payto://iban/exchange-IBAN-XYZ"), "ignored")
+        tx("merchant", "KUDOS:10", "exchange", "ignored")
         // Gen two transactions using raw bank transaction logic
         repeat(2) {
-            db.genTransaction("exchange", 
IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), 
OutgoingTxMetadata(randShortHashCode(), 
ExchangeUrl("http://exchange.example.com/";)).encode())
+            tx("exchange", "KUDOS:10", "merchant", 
OutgoingTxMetadata(randShortHashCode(), 
ExchangeUrl("http://exchange.example.com/";)).encode())
         }
 
         // Check ignore bogus subject
@@ -420,7 +408,7 @@ class WireGatewayApiTest {
         val valid_req = json {
             "amount" to "KUDOS:44"
             "reserve_pub" to randEddsaPublicKey()
-            "debit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+            "debit_account" to merchantPayto
         };
 
         
authRoutine("/accounts/merchant/taler-wire-gateway/admin/add-incoming", 
valid_req, requireAdmin = true)
@@ -455,7 +443,7 @@ class WireGatewayApiTest {
             basicAuth("admin", "admin-password")
             jsonBody(valid_req) { 
                 "reserve_pub" to randEddsaPublicKey()
-                "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+                "debit_account" to unknownPayto
             }
         }.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR)
 
@@ -464,7 +452,7 @@ class WireGatewayApiTest {
             basicAuth("admin", "admin-password")
             jsonBody(valid_req) { 
                 "reserve_pub" to randEddsaPublicKey()
-                "debit_account" to "payto://iban/EXCHANGE-IBAN-XYZ"
+                "debit_account" to exchangePayto
             }
         }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
 
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 6d2a53cd..80a52d76 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -16,6 +16,16 @@ import tech.libeufin.util.*
 
 /* ----- Setup ----- */
 
+val merchantPayto = IbanPayTo(genIbanPaytoUri())
+val exchangePayto = IbanPayTo(genIbanPaytoUri())
+val customerPayto = IbanPayTo(genIbanPaytoUri())
+val unknownPayto = IbanPayTo(genIbanPaytoUri())
+val paytos = mapOf(
+    "merchant" to merchantPayto, 
+    "exchange" to exchangePayto, 
+    "customer" to customerPayto
+)
+
 fun setup(
     conf: String = "test.conf",
     lambda: suspend (Database, BankConfig) -> Unit
@@ -43,7 +53,7 @@ fun bankSetup(
             login = "merchant",
             password = "merchant-password",
             name = "Merchant",
-            internalPaytoUri = IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"),
+            internalPaytoUri = merchantPayto,
             maxDebt = TalerAmount(10, 0, "KUDOS"),
             isTalerExchange = false,
             isPublic = false,
@@ -53,7 +63,7 @@ fun bankSetup(
             login = "exchange",
             password = "exchange-password",
             name = "Exchange",
-            internalPaytoUri = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"),
+            internalPaytoUri = exchangePayto,
             maxDebt = TalerAmount(10, 0, "KUDOS"),
             isTalerExchange = true,
             isPublic = false,
@@ -63,7 +73,7 @@ fun bankSetup(
             login = "customer",
             password = "customer-password",
             name = "Customer",
-            internalPaytoUri = IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ"),
+            internalPaytoUri = customerPayto,
             maxDebt = TalerAmount(10, 0, "KUDOS"),
             isTalerExchange = false,
             isPublic = false,
@@ -103,11 +113,11 @@ suspend fun ApplicationTestBuilder.assertBalance(account: 
String, info: CreditDe
     }
 }
 
-suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: 
String): Long {
+suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: 
String, subject: String = "payout"): Long {
     return client.post("/accounts/$from/transactions") {
         basicAuth("$from", "$from-password")
         jsonBody {
-            "payto_uri" to 
"payto://iban/$to-IBAN-XYZ?message=tx&amount=$amount"
+            "payto_uri" to 
"${paytos[to]}?message=${subject.encodeURLQueryComponent()}&amount=$amount"
         }
     }.assertOk().run {
         json<TransactionCreateResponse>().row_id
@@ -122,7 +132,7 @@ suspend fun ApplicationTestBuilder.transfer(amount: String) 
{
             "amount" to TalerAmount(amount)
             "exchange_base_url" to "http://exchange.example.com/";
             "wtid" to randShortHashCode()
-            "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+            "credit_account" to merchantPayto
         }
     }.assertOk()
 }
@@ -133,7 +143,7 @@ suspend fun ApplicationTestBuilder.addIncoming(amount: 
String) {
         jsonBody {
             "amount" to TalerAmount(amount)
             "reserve_pub" to randEddsaPublicKey()
-            "debit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+            "debit_account" to merchantPayto
         }
     }.assertOk()
 }
diff --git a/database-versioning/libeufin-bank-0001.sql 
b/database-versioning/libeufin-bank-0001.sql
index d6e6a4db..75aacaf4 100644
--- a/database-versioning/libeufin-bank-0001.sql
+++ b/database-versioning/libeufin-bank-0001.sql
@@ -67,7 +67,6 @@ CREATE TABLE IF NOT EXISTS customers
   ,phone TEXT
   ,cashout_payto TEXT
   );
-
 COMMENT ON COLUMN customers.cashout_payto
   IS 'RFC 8905 payto URI to collect fiat payments that come from the 
conversion of regional currency cash-out operations.';
 COMMENT ON COLUMN customers.name
@@ -82,11 +81,9 @@ CREATE TABLE IF NOT EXISTS bearer_tokens
   ,is_refreshable BOOLEAN
   ,bank_customer BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE 
CASCADE
 );
-
 COMMENT ON TABLE bearer_tokens
   IS 'Login tokens associated with one bank customer.  There is currently'
      ' no garbage collector that deletes the expired tokens from the table';
-
 COMMENT ON COLUMN bearer_tokens.bank_customer
   IS 'The customer that directly created this token, or the customer that'
      ' created the very first token that originated all the refreshes until'
@@ -104,7 +101,6 @@ CREATE TABLE IF NOT EXISTS bank_accounts
   ,max_debt taler_amount DEFAULT (0, 0)
   ,has_debt BOOLEAN NOT NULL DEFAULT FALSE
   );
-
 COMMENT ON TABLE bank_accounts
   IS 'In Sandbox, usernames (AKA logins) are different entities
 respect to bank accounts (in contrast to what the Python bank
@@ -114,14 +110,18 @@ one bank account for one user, and additionally the bank
 account label matches always the login.';
 COMMENT ON COLUMN bank_accounts.has_debt
   IS 'When true, the balance is negative';
-
 COMMENT ON COLUMN bank_accounts.is_public
   IS 'Indicates whether the bank account history
 can be publicly shared';
-
 COMMENT ON COLUMN bank_accounts.owning_customer_id
   IS 'Login that owns the bank account';
 
+CREATE TABLE IF NOT EXISTS iban_history 
+  (iban TEXT PRIMARY key
+  ,creation_time INT8 NOT NULL
+  );
+COMMENT ON TABLE iban_history IS 'Track all generated iban, some might be 
unused.';
+
 -- end of: bank accounts
 
 -- start of: money transactions
@@ -185,7 +185,7 @@ COMMENT ON COLUMN challenges.confirmation_date
 
 CREATE TABLE IF NOT EXISTS cashout_operations 
   (cashout_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
-  ,request_uid BYTEA NOT NULL UNIQUE CHECK (LENGTH(request_uid)=32)
+  ,request_uid BYTEA NOT NULL PRIMARY KEY CHECK (LENGTH(request_uid)=32)
   ,amount_debit taler_amount NOT NULL
   ,amount_credit taler_amount NOT NULL
   ,subject TEXT NOT NULL
@@ -204,6 +204,9 @@ CREATE TABLE IF NOT EXISTS cashout_operations
     ON DELETE RESTRICT
     ON UPDATE RESTRICT
   );
+COMMENT ON COLUMN cashout_operations.bank_account IS 'Bank amount to debit 
during confirmation';
+COMMENT ON COLUMN cashout_operations.challenge IS 'TAN challenge used to 
confirm the operation';
+COMMENT ON COLUMN cashout_operations.local_transaction IS 'Transaction 
generated during confirmation';
 
 -- end of: cashout management
 
@@ -229,7 +232,8 @@ CREATE TABLE IF NOT EXISTS taler_exchange_incoming
   );
 
 CREATE TABLE IF NOT EXISTS taler_withdrawal_operations
-  (withdrawal_uuid uuid NOT NULL PRIMARY KEY
+  (withdrawal_id BIGINT GENERATED BY DEFAULT AS IDENTITY
+  ,withdrawal_uuid uuid NOT NULL PRIMARY KEY
   ,amount taler_amount NOT NULL
   ,selection_done BOOLEAN DEFAULT FALSE NOT NULL
   ,aborted BOOLEAN DEFAULT FALSE NOT NULL

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