libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 467711b15cc899d37e777ffbe096f95c25f6b82b
parent b638ddb63240f54f5ae2e710a151abd366ce0716
Author: Antoine A <>
Date:   Thu,  7 Dec 2023 13:16:39 +0000

Fix parsing incoming transaction subject

Diffstat:
MREADME | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/Constants.kt | 6+++---
Mbank/src/main/kotlin/tech/libeufin/bank/Metadata.kt | 42++++++++++--------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 5++---
Mbank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt | 43++++++++++++-------------------------------
Mbank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 3+--
Mbank/src/test/kotlin/CoreBankApiTest.kt | 10+++++-----
Mbank/src/test/kotlin/RevenueApiTest.kt | 10+++++-----
Mbank/src/test/kotlin/WireGatewayApiTest.kt | 10+++++-----
Mbank/src/test/kotlin/helpers.kt | 12++++++++++--
Mbank/src/test/kotlin/routines.kt | 2+-
11 files changed, 55 insertions(+), 90 deletions(-)

diff --git a/README b/README @@ -9,7 +9,7 @@ be available before trying the installation: - make - python3-venv -- openjdk-17-jre +- openjdk-17-jre-headless Run the following steps to install LibEuFin: diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -39,7 +39,7 @@ val RESERVED_ACCOUNTS = setOf("admin", "bank") const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB // API version -const val COREBANK_API_VERSION: String = "2:0:2" +const val COREBANK_API_VERSION: String = "2:1:2" const val CONVERSION_API_VERSION: String = "0:0:0" const val INTEGRATION_API_VERSION: String = "1:0:1" -const val WIRE_GATEWAY_API_VERSION: String = "0:0:0" -\ No newline at end of file +const val WIRE_GATEWAY_API_VERSION: String = "0:1:0" +\ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt b/bank/src/main/kotlin/tech/libeufin/bank/Metadata.kt @@ -18,35 +18,13 @@ */ package tech.libeufin.bank -sealed interface TxMetadata { - // TODO versioning ? - companion object { - fun parse(subject: String): TxMetadata? { - // IncomingTxMetadata - try { - return IncomingTxMetadata(EddsaPublicKey(subject)) - } catch (e: Exception) { } - - // OutgoingTxMetadata - try { - val (wtid, exchangeBaseUrl) = subject.split(" ", limit=2) - return OutgoingTxMetadata(ShortHashCode(wtid), ExchangeUrl(exchangeBaseUrl)) - } catch (e: Exception) { } - - // No well formed metadata - return null - } - - fun encode(metadata: TxMetadata): String { - return when (metadata) { - is IncomingTxMetadata -> "${metadata.reservePub}" - is OutgoingTxMetadata -> "${metadata.wtid} ${metadata.exchangeBaseUrl.url}" - } - } +private val PATTERN = Regex("[a-z0-9A-Z]{52}") + +fun parseIncomingTxMetadata(subject: String): EddsaPublicKey? { + val match = PATTERN.find(subject)?.value ?: return null; + try { + return EddsaPublicKey(match) + } catch (e: Exception) { + return null } - - fun encode(): String = TxMetadata.encode(this) -} - -data class IncomingTxMetadata(val reservePub: EddsaPublicKey): TxMetadata -data class OutgoingTxMetadata(val wtid: ShortHashCode, val exchangeBaseUrl: ExchangeUrl): TxMetadata -\ No newline at end of file +} +\ No newline at end of file diff --git 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) { login: String, now: Instant ): TransferResult = db.serializable { conn -> - val subject = OutgoingTxMetadata(req.wtid, req.exchange_base_url).encode() + val subject = "${req.wtid} ${req.exchange_base_url.url}" val stmt = conn.prepareStatement(""" SELECT out_debtor_not_found @@ -192,7 +192,6 @@ class ExchangeDAO(private val db: Database) { login: String, now: Instant ): AddIncomingResult = db.serializable { conn -> - val subject = IncomingTxMetadata(req.reserve_pub).encode() val stmt = conn.prepareStatement(""" SELECT out_creditor_not_found @@ -211,7 +210,7 @@ class ExchangeDAO(private val db: Database) { """) stmt.setBytes(1, req.reserve_pub.raw) - stmt.setString(2, subject) + stmt.setString(2, "Manual incoming ${req.reserve_pub}") stmt.setLong(3, req.amount.value) stmt.setInt(4, req.amount.frac) stmt.setString(5, req.debit_account.canonical) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -82,33 +82,29 @@ class TransactionDAO(private val db: Database) { val creditRowId = it.getLong("out_credit_row_id") val debitAccountId = it.getLong("out_debit_bank_account_id") val debitRowId = it.getLong("out_debit_row_id") - val metadata = TxMetadata.parse(subject) val exchangeCreditor = it.getBoolean("out_creditor_is_exchange") val exchangeDebtor = it.getBoolean("out_debtor_is_exchange") if (exchangeCreditor && exchangeDebtor) { - val kind = when (metadata) { - is IncomingTxMetadata -> "an incoming taler " - is OutgoingTxMetadata -> "an outgoing taler" - null -> "a common" - }; - logger.warn("exchange account $exchangeDebtor sent $kind transaction to exchange account $exchangeCreditor, this should never happens and is not bounced to prevent bouncing loop") + logger.warn("exchange account $exchangeDebtor sent a manual transaction to exchange account $exchangeCreditor, this should never happens and is not bounced to prevent bouncing loop, may fail in the future") } else if (exchangeCreditor) { - val bounce = if (metadata is IncomingTxMetadata) { + val reservePub = parseIncomingTxMetadata(subject) + val bounceCause = if (reservePub != null) { val registered = conn.prepareStatement("CALL register_incoming(?, ?)").run { - setBytes(1, metadata.reservePub.raw) + setBytes(1, reservePub.raw) setLong(2, creditRowId) executeProcedureViolation() } if (!registered) { logger.warn("exchange account $creditAccountId received an incoming taler transaction $creditRowId with an already used reserve public key") - + "reserve public key reuse" + } else { + null } - !registered } else { - logger.warn("exchange account $creditAccountId received a transaction $creditRowId with malformed metadata") - true + logger.warn("exchange account $creditAccountId received a manual transaction $creditRowId with malformed metadata") + "malformed metadata" } - if (bounce) { + if (bounceCause != null) { // No error can happens because an opposite transaction already took place in the same transaction conn.prepareStatement(""" SELECT bank_wire_transfer( @@ -119,7 +115,7 @@ class TransactionDAO(private val db: Database) { ).run { setLong(1, debitAccountId) setLong(2, creditAccountId) - setString(3, "Bounce $creditRowId") // TODO better subject + setString(3, "Bounce $creditRowId: $bounceCause") setLong(4, amount.value) setInt(5, amount.frac) setLong(6, now) @@ -127,22 +123,7 @@ class TransactionDAO(private val db: Database) { } } } else if (exchangeDebtor) { - if (metadata is OutgoingTxMetadata) { - val registered = conn.prepareStatement("CALL register_outgoing(NULL, ?, ?, ?, ?, ?, ?)").run { - setBytes(1, metadata.wtid.raw) - setString(2, metadata.exchangeBaseUrl.url) - setLong(3, debitAccountId) - setLong(4, creditAccountId) - setLong(5, debitRowId) - setLong(6, creditRowId) - executeProcedureViolation() - } - if (!registered) { - logger.warn("exchange account $debitAccountId sent an outgoing taler transaction $debitRowId with an already used withdraw ID, use the API to catch this error") - } - } else { - logger.warn("exchange account $debitAccountId sent a transaction $debitRowId with malformed metadata, use the API instead") - } + logger.warn("exchange account $debitAccountId sent a manual transaction $debitRowId which will not be recorderd as a taler outgoing transaction, use the API instead") } BankTransactionResult.Success(debitRowId) } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt @@ -103,7 +103,6 @@ class WithdrawalDAO(private val db: Database) { exchangePayto: IbanPayTo, reservePub: EddsaPublicKey ): WithdrawalSelectionResult = db.serializable { conn -> - val subject = IncomingTxMetadata(reservePub).encode() val stmt = conn.prepareStatement(""" SELECT out_no_op, @@ -117,7 +116,7 @@ class WithdrawalDAO(private val db: Database) { ) stmt.setObject(1, uuid) stmt.setBytes(2, reservePub.raw) - stmt.setString(3, subject) + stmt.setString(3, "Taler withdrawal $reservePub") stmt.setString(4, exchangePayto.canonical) stmt.executeQuery().use { when { diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -755,9 +755,9 @@ class CoreBankTransactionsApiTest { assertBalance("exchange", "+KUDOS:0") tx("merchant", "KUDOS:1", "exchange", "") // Bounce common to transaction tx("merchant", "KUDOS:1", "exchange", "Malformed") // Bounce malformed transaction - val reserve_pub = randShortHashCode(); - tx("merchant", "KUDOS:1", "exchange", IncomingTxMetadata(reserve_pub).encode()) // Accept incoming - tx("merchant", "KUDOS:1", "exchange", IncomingTxMetadata(reserve_pub).encode()) // Bounce reserve_pub reuse + val reserve_pub = randEddsaPublicKey(); + tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Accept incoming + tx("merchant", "KUDOS:1", "exchange", randIncomingSubject(reserve_pub)) // Bounce reserve_pub reuse assertBalance("merchant", "-KUDOS:1") assertBalance("exchange", "+KUDOS:1") @@ -768,8 +768,8 @@ class CoreBankTransactionsApiTest { tx("exchange", "KUDOS:1", "merchant", "Malformed") // Warn malformed transaction val wtid = randShortHashCode() val exchange = ExchangeUrl("http://exchange.example.com/") - tx("exchange", "KUDOS:1", "merchant", OutgoingTxMetadata(wtid, exchange).encode()) // Accept outgoing - tx("exchange", "KUDOS:1", "merchant", OutgoingTxMetadata(wtid, exchange).encode()) // Warn wtid reuse + tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Accept outgoing + tx("exchange", "KUDOS:1", "merchant", randOutgoingSubject(wtid, exchange)) // Warn wtid reuse assertBalance("merchant", "+KUDOS:3") assertBalance("exchange", "-KUDOS:3") } diff --git a/bank/src/test/kotlin/RevenueApiTest.kt b/bank/src/test/kotlin/RevenueApiTest.kt @@ -39,16 +39,16 @@ class RevenueApiTest { ids = { it.incoming_transactions.map { it.row_id } }, registered = listOf( { - // Transactions using clean add incoming logic + // Transactions using clean transfer logic transfer("KUDOS:10") - }, - { - // Transactions using raw bank transaction logic - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) } ), ignored = listOf( { + // Ignore manual incoming transaction + tx("exchange", "KUDOS:10", "merchant", "${randShortHashCode()} http://exchange.example.com/") + }, + { // Ignore malformed incoming transaction tx("merchant", "KUDOS:10", "exchange", "ignored") }, diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt @@ -140,7 +140,7 @@ class WireGatewayApiTest { }, { // Transactions using raw bank transaction logic - tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode()) + tx("merchant", "KUDOS:10", "exchange", "history test with ${randShortHashCode()} reserve pub") }, { // Transaction using withdraw logic @@ -175,13 +175,13 @@ class WireGatewayApiTest { { // Transactions using clean add incoming logic transfer("KUDOS:10") - }, - { - // Transactions using raw bank transaction logic - tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode()) } ), ignored = listOf( + { + // gnore manual incoming transaction + tx("exchange", "KUDOS:10", "merchant", "${randShortHashCode()} http://exchange.example.com/") + }, { // Ignore malformed incoming transaction tx("merchant", "KUDOS:10", "exchange", "ignored") diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -411,4 +411,12 @@ fun randBase32Crockford(lenght: Int) = Base32Crockford.encode(randBytes(lenght)) fun randHashCode(): HashCode = HashCode(randBase32Crockford(64)) fun randShortHashCode(): ShortHashCode = ShortHashCode(randBase32Crockford(32)) -fun randEddsaPublicKey(): EddsaPublicKey = EddsaPublicKey(randBase32Crockford(32)) -\ No newline at end of file +fun randEddsaPublicKey(): EddsaPublicKey = EddsaPublicKey(randBase32Crockford(32)) + +fun randIncomingSubject(reservePub: EddsaPublicKey): String { + return "$reservePub" +} + +fun randOutgoingSubject(wtid: ShortHashCode, url: ExchangeUrl): String { + return "$wtid $url" +} +\ No newline at end of file diff --git a/bank/src/test/kotlin/routines.kt b/bank/src/test/kotlin/routines.kt @@ -148,7 +148,7 @@ inline suspend fun <reified B> ApplicationTestBuilder.historyRoutine( } launch { // Check polling timeout assertTime(200, 300) { - history("delta=1&start=${id+10}&long_poll_ms=200") + history("delta=1&start=${id+nbTotal*3}&long_poll_ms=200") .assertNoContent() } }