[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (3fcbe27f -> b5caa5c6)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (3fcbe27f -> b5caa5c6) |
Date: |
Mon, 06 Jan 2025 14:09:00 +0100 |
This is an automated email from the git hooks/post-receive script.
antoine pushed a change to branch master
in repository libeufin.
from 3fcbe27f common: better polymorphism schema
new 8383013a common: better subject parsing logic
new b5caa5c6 common: improve naming
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:
.../tech/libeufin/bank/api/WireGatewayApi.kt | 6 +-
.../kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 10 +-
.../kotlin/tech/libeufin/bank/db/TransactionDAO.kt | 2 +-
bank/src/test/kotlin/WireGatewayApiTest.kt | 18 +--
common/src/main/kotlin/Encoding.kt | 4 +-
common/src/main/kotlin/Subject.kt | 165 +++++++++++++++++++++
common/src/main/kotlin/TalerMessage.kt | 4 +-
common/src/main/kotlin/TxMedatada.kt | 101 -------------
common/src/test/kotlin/SubjectTest.kt | 140 +++++++++++++++++
common/src/test/kotlin/TxMedataTest.kt | 114 --------------
.../tech/libeufin/nexus/api/WireGatewayApi.kt | 8 +-
.../kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 6 +-
.../kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 10 +-
.../main/kotlin/tech/libeufin/nexus/db/ListDAO.kt | 8 +-
.../kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 2 +-
nexus/src/test/kotlin/WireGatewayApiTest.kt | 18 +--
testbench/src/test/kotlin/IntegrationTest.kt | 4 +-
17 files changed, 355 insertions(+), 265 deletions(-)
create mode 100644 common/src/main/kotlin/Subject.kt
delete mode 100644 common/src/main/kotlin/TxMedatada.kt
create mode 100644 common/src/test/kotlin/SubjectTest.kt
delete mode 100644 common/src/test/kotlin/TxMedataTest.kt
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
index 81d1e260..2ba9929d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
@@ -151,7 +151,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
amount: TalerAmount,
debitAccount: Payto,
subject: String,
- metadata: TalerIncomingMetadata
+ metadata: IncomingSubject
) {
cfg.checkRegionalCurrency(amount)
val timestamp = Instant.now()
@@ -199,7 +199,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
amount = req.amount,
debitAccount = req.debit_account,
subject = "Admin incoming ${req.reserve_pub}",
- metadata = TalerIncomingMetadata(TalerIncomingType.reserve,
req.reserve_pub)
+ metadata = IncomingSubject(IncomingType.reserve,
req.reserve_pub)
)
}
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth") {
@@ -208,7 +208,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: BankConfig) {
amount = req.amount,
debitAccount = req.debit_account,
subject = "Admin incoming KYC:${req.account_pub}",
- metadata = TalerIncomingMetadata(TalerIncomingType.kyc,
req.account_pub)
+ metadata = IncomingSubject(IncomingType.kyc, req.account_pub)
)
}
}
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 65baf5dc..f3b5a749 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -45,23 +45,23 @@ class ExchangeDAO(private val db: Database) {
ON bank_transaction=txs.bank_transaction_id
WHERE
""") {
- val type = it.getEnum<TalerIncomingType>("type")
+ val type = it.getEnum<IncomingType>("type")
when (type) {
- TalerIncomingType.reserve -> IncomingReserveTransaction(
+ IncomingType.reserve -> IncomingReserveTransaction(
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
amount = it.getAmount("amount", db.bankCurrency),
debit_account = it.getBankPayto("debtor_payto",
"debtor_name", db.ctx),
reserve_pub = EddsaPublicKey(it.getBytes("metadata")),
)
- TalerIncomingType.kyc -> IncomingKycAuthTransaction(
+ IncomingType.kyc -> IncomingKycAuthTransaction(
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
amount = it.getAmount("amount", db.bankCurrency),
debit_account = it.getBankPayto("debtor_payto",
"debtor_name", db.ctx),
account_pub = EddsaPublicKey(it.getBytes("metadata")),
)
- TalerIncomingType.wad -> throw UnsupportedOperationException()
+ IncomingType.wad -> throw UnsupportedOperationException()
}
}
@@ -253,7 +253,7 @@ class ExchangeDAO(private val db: Database) {
subject: String,
username: String,
timestamp: Instant,
- metadata: TalerIncomingMetadata
+ metadata: IncomingSubject
): AddIncomingResult = db.serializable(
"""
SELECT
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
index beb7a0f4..85188aee 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -113,7 +113,7 @@ class TransactionDAO(private val db: Database) {
if (exchangeCreditor && exchangeDebtor) {
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 bounceCause = runCatching {
parseIncomingTxMetadata(subject) }.fold(
+ val bounceCause = runCatching {
parseIncomingSubject(subject) }.fold(
onSuccess = { metadata ->
val registered = conn.withStatement("CALL
register_incoming(?, ?::taler_incoming_type, ?, ?)") {
setLong(1, creditRowId)
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt
b/bank/src/test/kotlin/WireGatewayApiTest.kt
index f5693ad1..5acbabee 100644
--- a/bank/src/test/kotlin/WireGatewayApiTest.kt
+++ b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023-2024 Taler Systems S.A.
+ * Copyright (C) 2023-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -279,11 +279,11 @@ class WireGatewayApiTest {
)
}
- suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
TalerIncomingType) {
+ suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
IncomingType) {
val (path, key) = when (type) {
- TalerIncomingType.reserve -> Pair("add-incoming", "reserve_pub")
- TalerIncomingType.kyc -> Pair("add-kycauth", "account_pub")
- TalerIncomingType.wad -> throw UnsupportedOperationException()
+ IncomingType.reserve -> Pair("add-incoming", "reserve_pub")
+ IncomingType.kyc -> Pair("add-kycauth", "account_pub")
+ IncomingType.wad -> throw UnsupportedOperationException()
}
val valid_req = obj {
"amount" to "KUDOS:44"
@@ -304,12 +304,12 @@ class WireGatewayApiTest {
json(valid_req)
}.assertOk()
- if (type == TalerIncomingType.reserve) {
+ if (type == IncomingType.reserve) {
// Trigger conflict due to reused reserve_pub
client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req)
}.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
- } else if (type == TalerIncomingType.kyc) {
+ } else if (type == IncomingType.kyc) {
// Non conflict on reuse
client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req)
@@ -355,13 +355,13 @@ class WireGatewayApiTest {
// POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming
@Test
fun addIncoming() = bankSetup {
- talerAddIncomingRoutine(TalerIncomingType.reserve)
+ talerAddIncomingRoutine(IncomingType.reserve)
}
// POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth
@Test
fun addKycAuth() = bankSetup {
- talerAddIncomingRoutine(TalerIncomingType.kyc)
+ talerAddIncomingRoutine(IncomingType.kyc)
}
@Test
diff --git a/common/src/main/kotlin/Encoding.kt
b/common/src/main/kotlin/Encoding.kt
index 7f90d22e..f3fa044c 100644
--- a/common/src/main/kotlin/Encoding.kt
+++ b/common/src/main/kotlin/Encoding.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -22,7 +22,7 @@ package tech.libeufin.common
/** Crockford's Base32 implementation */
object Base32Crockford {
/** Crockford's Base32 alphabet */
- private const val ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
+ const val ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
/** Base32 mark to extract 5 bits chunks */
private const val MASK = 0b11111
/** Crockford's Base32 inversed alphabet */
diff --git a/common/src/main/kotlin/Subject.kt
b/common/src/main/kotlin/Subject.kt
new file mode 100644
index 00000000..e1cdd0b3
--- /dev/null
+++ b/common/src/main/kotlin/Subject.kt
@@ -0,0 +1,165 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+package tech.libeufin.common
+
+import org.bouncycastle.math.ec.rfc8032.Ed25519
+
+data class IncomingSubject(val type: IncomingType, val key: EddsaPublicKey)
+
+/** Base32 quality by proximity to spec and error probability */
+private enum class Base32Quality {
+ /// Both mixed casing and mixed characters, thats weird
+ Mixed,
+ /// Standard but use lowercase, maybe the client shown lowercase in the UI
+ Standard,
+ /// Uppercase but mixed characters, its common when making typos
+ Upper,
+ /// Both uppercase and use the standard alphabet as it should
+ UpperStandard;
+
+ companion object {
+ fun measure(s: String): Base32Quality {
+ var uppercase = true;
+ var standard = true;
+ for (char in s) {
+ uppercase = uppercase && char.isUpperCase()
+ standard = standard && Base32Crockford.ALPHABET.contains(char)
+ }
+ return if (uppercase && standard) {
+ Base32Quality.UpperStandard
+ } else if (uppercase && !standard) {
+ Base32Quality.Upper
+ } else if (!uppercase && standard) {
+ Base32Quality.Standard
+ } else {
+ Base32Quality.Mixed
+ }
+ }
+ }
+}
+
+private data class Candidate(val subject: IncomingSubject, val quality:
Base32Quality)
+
+/**
+ * Extract the public key from an unstructured incoming transfer subject.
+ *
+ * When a user enters the transfer object in an unstructured way, for ex in
+ * their banking UI, they may mistakenly enter separators such as ' \n-+' and
+ * make typos.
+ * To parse them while ignoring user errors, we reconstruct valid keys from key
+ * parts, resolving ambiguities where possible.
+ **/
+fun parseIncomingSubject(subject: String): IncomingSubject {
+ /** Parse an incoming subject */
+ fun parseSingle(str: String): Candidate? {
+ // Check key type
+ val (isKyc, raw) = if (str.startsWith("KYC:")) {
+ Pair(true, str.substring(4))
+ } else {
+ Pair(false, str)
+ }
+
+ // Check key validity
+ val key = try {
+ EddsaPublicKey(raw)
+ } catch (e: Exception) {
+ return null
+ }
+ if (!Ed25519.validatePublicKeyFull(key.raw, 0)) {
+ return null
+ }
+
+ val quality = Base32Quality.measure(raw);
+ val type = if (isKyc) IncomingType.kyc else IncomingType.reserve
+ return Candidate(IncomingSubject(type, key), quality)
+ }
+
+ // Find and concatenate valid parts of a keys
+ val parts = mutableListOf<IntRange>()
+ val concatenated = StringBuilder()
+ var part: Int? = null
+ for ((i, c) in subject.withIndex()) {
+ if (c.isLetterOrDigit() || c == ':') {
+ part = part ?: i
+ } else if (part != null) {
+ val start = concatenated.length
+ concatenated.append(subject.substring(part until i));
+ val end = concatenated.length
+ parts.add(start until end);
+ part = null
+ }
+ }
+ if (part != null) {
+ val start = concatenated.length
+ concatenated.append(subject.substring(part until subject.length));
+ val end = concatenated.length
+ parts.add(start until end);
+ }
+
+ // Find best candidates
+ var best: Candidate? = null
+ // For each part as a starting point
+ for ((i, start) in parts.withIndex()) {
+ // Use progressively longer concatenation
+ for (end in parts.subList(i, parts.size)) {
+ val range = start.start..end.endInclusive
+ val len = range.count()
+ // Until they are to long to be a key
+ if (len > 56) {
+ break;
+ }
+ // If the slice is the right size for a key (56B with prefix else
54B)
+ if (len == 52 || len == 56) {
+ // Parse the concatenated parts
+ val slice = concatenated.substring(range)
+ parseSingle(slice)?.let { other ->
+ if (best != null) {
+ if (other.quality > best.quality // We prefer high
quality keys
+ || ( // We prefer prefixed keys over reserve keys
+ best.subject.type == IncomingType.reserve &&
+ (other.subject.type == IncomingType.kyc ||
other.subject.type == IncomingType.wad)
+ ))
+ {
+ best = other
+ } else if (best.subject.key != other.subject.key // If
keys are different
+ && best.quality == other.quality // Of same quality
+ && !( // And prefixing is diferent
+ (best.subject.type == IncomingType.kyc ||
best.subject.type == IncomingType.wad) &&
+ other.subject.type == IncomingType.reserve
+ ))
+ {
+ throw Exception("Found multiple reserve public
key")
+ }
+ } else {
+ best = other
+ }
+ }
+ }
+ }
+ }
+
+ return best?.subject ?: throw Exception("Missing reserve public key")
+}
+
+/** Extract the reserve public key from an incoming Taler transaction subject
*/
+fun parseOutgoingSubject(subject: String): Pair<ShortHashCode, ExchangeUrl> {
+ val (wtid, baseUrl) = subject.splitOnce(" ") ?: throw Exception("Malformed
outgoing subject")
+ return Pair(EddsaPublicKey(wtid), ExchangeUrl(baseUrl))
+}
\ No newline at end of file
diff --git a/common/src/main/kotlin/TalerMessage.kt
b/common/src/main/kotlin/TalerMessage.kt
index 62bdef2e..6a8b4505 100644
--- a/common/src/main/kotlin/TalerMessage.kt
+++ b/common/src/main/kotlin/TalerMessage.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
*
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -21,7 +21,7 @@ package tech.libeufin.common
import kotlinx.serialization.*
-enum class TalerIncomingType {
+enum class IncomingType {
reserve,
kyc,
wad
diff --git a/common/src/main/kotlin/TxMedatada.kt
b/common/src/main/kotlin/TxMedatada.kt
deleted file mode 100644
index c389f1af..00000000
--- a/common/src/main/kotlin/TxMedatada.kt
+++ /dev/null
@@ -1,101 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-package tech.libeufin.common
-
-import org.bouncycastle.math.ec.rfc8032.Ed25519
-
-private val PART_PATTERN = Regex("[:a-z0-9A-Z]*")
-private val BASE32_32B_PATTERN = Regex("(KYC:)?([a-z0-9A-Z]{52})")
-
-data class TalerIncomingMetadata(val type: TalerIncomingType, val key:
EddsaPublicKey)
-
-/**
- * Extract the reserve public key from an incoming Taler transaction subject
- *
- * We first try to find a whole key. If none are found we try to reconstruct
- * one from parts.
- **/
-fun parseIncomingTxMetadata(subject: String): TalerIncomingMetadata {
- /** Check one or no reserve public key is found **/
- fun check(matches: Sequence<MatchResult>): TalerIncomingMetadata? {
- val iterator = matches.iterator()
-
- // Check none
- if (!iterator.hasNext()) {
- return null
- }
-
- val (prefix, base32) = iterator.next().destructured
-
- // Check many
- if (iterator.hasNext()) {
- throw Exception("Found multiple reserve public key")
- }
-
- // Check key validity
- val key = EddsaPublicKey(base32)
- if (!Ed25519.validatePublicKeyFull(key.raw, 0)) {
- return null
- }
-
- // Check key type
- val type = if (prefix == "KYC:") TalerIncomingType.kyc else
TalerIncomingType.reserve
- return TalerIncomingMetadata(type, key)
- }
-
- // Wire transfer subjects are generally small in size, and not
- // being able to find the encoded reserve public key poses a huge
- // usability problem. So we're ready to work hard to find it.
-
- // Bank interfaces doesn't always allow a complete encoded key to
- // be entered on a single line, so users have to split them, which
- // may lead them to add an erroneous space or + or - separator.
- // If we can't find a key, we try to eliminate these common errors
- // before trying again.
-
- // Since any sequence of 52 upper and lowercase characters can be a
- // valid encoded key, deleting spaces and separators can create false
- // positives, so we always start by searching for a valid whole key
- // then we try to reconstruct a key from valid parts.
-
- // Whole key match
- val key = check(BASE32_32B_PATTERN.findAll(subject))
- if (key != null) return key
-
- // Key reconstruction from parts
- val parts = PART_PATTERN.findAll(subject).map { it.value }.toList()
- for (windowSize in 2..parts.size) {
- val matches = parts.windowed(windowSize).asSequence().map { window ->
- val joined = window.joinToString("")
- BASE32_32B_PATTERN.matchEntire(joined)
- }.filterNotNull()
- val key = check(matches)
- if (key != null) return key
- }
-
- // No key where found
- throw Exception("Missing reserve public key")
-}
-
-/** Extract the reserve public key from an incoming Taler transaction subject
*/
-fun parseOutgoingTxMetadata(subject: String): Pair<ShortHashCode, ExchangeUrl>
{
- val (wtid, baseUrl) = subject.splitOnce(" ") ?: throw Exception("Malformed
outgoing subject")
- return Pair(EddsaPublicKey(wtid), ExchangeUrl(baseUrl))
-}
\ No newline at end of file
diff --git a/common/src/test/kotlin/SubjectTest.kt
b/common/src/test/kotlin/SubjectTest.kt
new file mode 100644
index 00000000..4258f7e5
--- /dev/null
+++ b/common/src/test/kotlin/SubjectTest.kt
@@ -0,0 +1,140 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+import tech.libeufin.common.*
+import kotlin.test.*
+
+class SubjectTest {
+ fun assertFailsMsg(msg: String, lambda: () -> Unit) {
+ val failure = assertFails(lambda)
+ assertEquals(msg, failure.message)
+ }
+
+ @Test
+ fun parse() {
+ val key = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0";
+ val other = "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG";
+
+ for (ty in sequenceOf(IncomingType.reserve, IncomingType.kyc)) {
+ val prefix = if (ty == IncomingType.kyc) "KYC:" else "";
+ val standard = "$prefix$key"
+ val (standardL, standardR) = standard.chunked(standard.length / 2)
+ val mixed =
"${prefix}4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
+ val (mixedL, mixedR) = mixed.chunked(mixed.length / 2)
+ val other_standard = "$prefix$other"
+ val other_mixed =
"${prefix}TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60"
+ val key = IncomingSubject(ty, EddsaPublicKey(key))
+
+ // Check succeed if standard or mixed
+ for (case in sequenceOf(standard, mixed)) {
+ for (test in sequenceOf(
+ "noise $case noise",
+ "$case noise to the right",
+ "noise to the left $case",
+ " $case ",
+ "noise\n$case\nnoise",
+ "Test+$case"
+ )) {
+ assertEquals(key, parseIncomingSubject(test))
+ }
+ }
+
+ // Check succeed if standard or mixed and split
+ for ((L, R) in sequenceOf(standardL to standardR, mixedL to
mixedR)) {
+ for (case in sequenceOf(
+ "left $L$R right",
+ "left $L $R right",
+ "left $L-$R right",
+ "left $L+$R right",
+ "left $L\n$R right",
+ "left $L-+\n$R right",
+ "left $L - $R right",
+ "left $L + $R right",
+ "left $L \n $R right",
+ "left $L - + \n $R right",
+ )) {
+ assertEquals(key, parseIncomingSubject(case))
+ }
+ }
+
+ // Check concat parts
+ for (chunkSize in 1 until standard.length) {
+ val chunked = standard.chunked(chunkSize).joinToString(" ")
+ for (case in sequenceOf(chunked, "left ${chunked} right")) {
+ assertEquals(key, parseIncomingSubject(case))
+ }
+ }
+
+ // Check failed when multiple key
+ for (case in sequenceOf(
+ "$standard $other_standard",
+ "$mixed $other_mixed",
+ )) {
+ assertFailsMsg("Found multiple reserve public key") {
+ parseIncomingSubject(case)
+ }
+ }
+
+ // Check accept redundant key
+ for (case in sequenceOf(
+ "$standard $standard $mixed $mixed", // Accept redundant key
+ "$mixedL-$mixedR $standardL-$standardR",
+ "$standard $other_mixed", // Prefer high quality
+ )) {
+ assertEquals(key, parseIncomingSubject(case))
+ }
+
+ // Check failure if malformed or missing
+ for (case in sequenceOf(
+ "does not contain any reserve", //
Check fail if none
+ standard.substring(1), // Check fail if
missing char
+ "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" //
Check fail if not a valid key
+ )) {
+ assertFailsMsg("Missing reserve public key") {
+ parseIncomingSubject(case)
+ }
+ }
+
+ if (ty == IncomingType.kyc) {
+ // Prefer prefixed over unprefixed
+ for (case in sequenceOf(
+ "$other $standard", "$other $mixed"
+ )) {
+ assertEquals(key, parseIncomingSubject(case))
+ }
+ }
+ }
+ }
+
+ /** Test parsing logic using real use case */
+ @Test
+ fun real() {
+ // Good cases
+ for ((subject, key) in sequenceOf(
+ "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to
"TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
+ "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to
"00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
+ "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to
"NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
+ )) {
+ assertEquals(
+ IncomingSubject(IncomingType.reserve, EddsaPublicKey(key)),
+ parseIncomingSubject(subject)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/common/src/test/kotlin/TxMedataTest.kt
b/common/src/test/kotlin/TxMedataTest.kt
deleted file mode 100644
index 3367c7f8..00000000
--- a/common/src/test/kotlin/TxMedataTest.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
-
- * LibEuFin is free software; you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation; either version 3, or
- * (at your option) any later version.
-
- * LibEuFin is distributed in the hope that it will be useful, but
- * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
- * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
- * Public License for more details.
-
- * You should have received a copy of the GNU Affero General Public
- * License along with LibEuFin; see the file COPYING. If not, see
- * <http://www.gnu.org/licenses/>
- */
-
-import tech.libeufin.common.*
-import kotlin.test.*
-
-class TxMetadataTest {
- fun assertFailsMsg(msg: String, lambda: () -> Unit) {
- val failure = assertFails(lambda)
- assertEquals(msg, failure.message)
- }
-
- @Test
- fun parse() {
- val upper = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
- val (upperL, upperR) = upper.chunked(26)
- val mixed = "4mzt6RS3rvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
- val (mixedL, mixedR) = mixed.chunked(26)
- val key = TalerIncomingMetadata(TalerIncomingType.reserve,
EddsaPublicKey(upper))
-
- // Check succeed if upper or mixed
- for (case in sequenceOf(upper, mixed)) {
- for (test in sequenceOf(
- "noise $case noise",
- "$case noise to the right",
- "noise to the left $case",
- " $case ",
- "noise\n$case\nnoise",
- "Test+$case"
- )) {
- assertEquals(key, parseIncomingTxMetadata(test))
- }
- }
-
- // Check succeed if upper or mixed and split
- for ((L, R) in sequenceOf(upperL to upperR, mixedL to mixedR)) {
- for (case in sequenceOf(
- "left $L$R right",
- "left $L $R right",
- "left $L-$R right",
- "left $L+$R right",
- "left $L\n$R right",
- "left $L-+\n$R right",
- "left $L - $R right",
- "left $L + $R right",
- "left $L \n $R right",
- "left $L - + \n $R right",
- )) {
- assertEquals(key, parseIncomingTxMetadata(case))
- }
- }
-
- // Check parts
- for (case in sequenceOf(
- upper.chunked(12).joinToString(" "),
- "left ${upper.chunked(1).joinToString(" ")} right",
- ))
-
- // Check failure when multiple keys match
- for (case in sequenceOf(
- "$upper $upper",
- "$mixed $mixed",
- "$mixed $upper",
- "$mixedL-$mixedR $upperL-$upperR"
- )) {
- assertFailsMsg("Found multiple reserve public key") {
- parseIncomingTxMetadata(case)
- }
- }
-
- // Check failure if malformed or missing
- for (case in sequenceOf(
- "does not contain any reserve", // Check
fail if none
- upper.substring(0, upper.length-1), // Check
fail if missing char
- "2MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0" // Check
fail if not a valid key
- )) {
- assertFailsMsg("Missing reserve public key") {
- parseIncomingTxMetadata(case)
- }
- }
- }
-
- /** Test parsing logic using real use case */
- @Test
- fun real() {
- // Good cases
- for ((subject, key) in sequenceOf(
- "Taler TEGY6d9mh9pgwvwpgs0z0095z854xegfy7j j202yd0esp8p0za60" to
"TEGY6d9mh9pgwvwpgs0z0095z854xegfy7jj202yd0esp8p0za60",
- "00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N 0QT1RCBQ8FXJPZ6RG" to
"00Q979QSMJ29S7BJT3DDAVC5A0DR5Z05B7N0QT1RCBQ8FXJPZ6RG",
- "Taler NDDCAM9XN4HJZFTBD8V6FNE2FJE8G Y734PJ5AGQMY06C8D4HB3Z0" to
"NDDCAM9XN4HJZFTBD8V6FNE2FJE8GY734PJ5AGQMY06C8D4HB3Z0",
- )) {
- assertEquals(
- TalerIncomingMetadata(TalerIncomingType.reserve,
EddsaPublicKey(key)),
- parseIncomingTxMetadata(subject)
- )
- }
- }
-}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
index 953562e4..37e9238a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -107,7 +107,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig)
= conditional(cfg.wir
amount: TalerAmount,
debitAccount: Payto,
subject: String,
- metadata: TalerIncomingMetadata
+ metadata: IncomingSubject
) {
cfg.checkCurrency(amount)
val debitAccount = debitAccount.expectRequestIban()
@@ -139,7 +139,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig)
= conditional(cfg.wir
amount = req.amount,
debitAccount = req.debit_account,
subject = "Manual incoming ${req.reserve_pub}",
- metadata = TalerIncomingMetadata(TalerIncomingType.reserve,
req.reserve_pub)
+ metadata = IncomingSubject(IncomingType.reserve,
req.reserve_pub)
)
}
post("/taler-wire-gateway/admin/add-kycauth") {
@@ -148,7 +148,7 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig)
= conditional(cfg.wir
amount = req.amount,
debitAccount = req.debit_account,
subject = "Manual incoming KYC:${req.account_pub}",
- metadata = TalerIncomingMetadata(TalerIncomingType.kyc,
req.account_pub)
+ metadata = IncomingSubject(IncomingType.kyc, req.account_pub)
)
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
index d0a21675..acd94994 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -47,7 +47,7 @@ suspend fun registerOutgoingPayment(
payment: OutgoingPayment
): OutgoingRegistrationResult {
val metadata: Pair<ShortHashCode, ExchangeUrl>? = payment.subject?.let {
- runCatching { parseOutgoingTxMetadata(it) }.getOrNull()
+ runCatching { parseOutgoingSubject(it) }.getOrNull()
}
val result = db.payment.registerOutgoing(payment, metadata?.first,
metadata?.second)
if (result.new) {
@@ -132,7 +132,7 @@ suspend fun registerIncomingPayment(
}
}
}
- runCatching { parseIncomingTxMetadata(payment.subject) }.fold(
+ runCatching { parseIncomingSubject(payment.subject) }.fold(
onSuccess = { metadata ->
when (val res = db.payment.registerTalerableIncoming(payment,
metadata)) {
IncomingRegistrationResult.ReservePubReuse -> bounce("reverse
pub reuse")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
index a72e8280..bafe1a6c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2024 Taler Systems S.A.
+ * Copyright (C) 2024-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -44,9 +44,9 @@ class ExchangeDAO(private val db: Database) {
JOIN incoming_transactions USING(incoming_transaction_id)
WHERE
""", "incoming_transaction_id") {
- val type = it.getEnum<TalerIncomingType>("type")
+ val type = it.getEnum<IncomingType>("type")
when (type) {
- TalerIncomingType.reserve -> IncomingReserveTransaction(
+ IncomingType.reserve -> IncomingReserveTransaction(
row_id = it.getLong("incoming_transaction_id"),
date = it.getTalerTimestamp("execution_time"),
amount = it.getAmount("amount", db.bankCurrency),
@@ -54,7 +54,7 @@ class ExchangeDAO(private val db: Database) {
debit_account = it.getString("debit_payto"),
reserve_pub = EddsaPublicKey(it.getBytes("metadata")),
)
- TalerIncomingType.kyc -> IncomingKycAuthTransaction(
+ IncomingType.kyc -> IncomingKycAuthTransaction(
row_id = it.getLong("incoming_transaction_id"),
date = it.getTalerTimestamp("execution_time"),
amount = it.getAmount("amount", db.bankCurrency),
@@ -62,7 +62,7 @@ class ExchangeDAO(private val db: Database) {
debit_account = it.getString("debit_payto"),
account_pub = EddsaPublicKey(it.getBytes("metadata")),
)
- TalerIncomingType.wad -> throw UnsupportedOperationException()
+ IncomingType.wad -> throw UnsupportedOperationException()
}
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt
index d8b466bf..5cb5cf5d 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ListDAO.kt
@@ -50,7 +50,7 @@ class ListDAO(private val db: Database) {
"""
) {
all {
- val type = it.getOptEnum<TalerIncomingType>("type")
+ val type = it.getOptEnum<IncomingType>("type")
IncomingTxMetadata(
date = it.getLong("execution_time").asInstant(),
amount = it.getDecimal("amount"),
@@ -60,9 +60,9 @@ class ListDAO(private val db: Database) {
id = it.getString("bank_id"),
talerable = when (type) {
null -> if (it.getBoolean("bounced")) "bounced" else ""
- TalerIncomingType.reserve -> "reserve
${EddsaPublicKey(it.getBytes("metadata"))}"
- TalerIncomingType.kyc -> "kyc
${EddsaPublicKey(it.getBytes("metadata"))}"
- TalerIncomingType.wad -> throw
UnsupportedOperationException()
+ IncomingType.reserve -> "reserve
${EddsaPublicKey(it.getBytes("metadata"))}"
+ IncomingType.kyc -> "kyc
${EddsaPublicKey(it.getBytes("metadata"))}"
+ IncomingType.wad -> throw UnsupportedOperationException()
}
)
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
index 6c6a28a2..0217f883 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
@@ -116,7 +116,7 @@ class PaymentDAO(private val db: Database) {
/** Register an talerable incoming payment */
suspend fun registerTalerableIncoming(
payment: IncomingPayment,
- metadata: TalerIncomingMetadata
+ metadata: IncomingSubject
): IncomingRegistrationResult = db.serializable(
"""
SELECT out_reserve_pub_reuse, out_found, out_tx_id
diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt
b/nexus/src/test/kotlin/WireGatewayApiTest.kt
index ed2a31e0..7846036e 100644
--- a/nexus/src/test/kotlin/WireGatewayApiTest.kt
+++ b/nexus/src/test/kotlin/WireGatewayApiTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023-2024 Taler Systems S.A.
+ * Copyright (C) 2023-2025 Taler Systems S.A.
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -238,11 +238,11 @@ class WireGatewayApiTest {
)
}
- suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
TalerIncomingType) {
+ suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
IncomingType) {
val (path, key) = when (type) {
- TalerIncomingType.reserve -> Pair("add-incoming", "reserve_pub")
- TalerIncomingType.kyc -> Pair("add-kycauth", "account_pub")
- TalerIncomingType.wad -> throw UnsupportedOperationException()
+ IncomingType.reserve -> Pair("add-incoming", "reserve_pub")
+ IncomingType.kyc -> Pair("add-kycauth", "account_pub")
+ IncomingType.wad -> throw UnsupportedOperationException()
}
val valid_req = obj {
"amount" to "CHF:44"
@@ -257,12 +257,12 @@ class WireGatewayApiTest {
json(valid_req)
}.assertOk()
- if (type == TalerIncomingType.reserve) {
+ if (type == IncomingType.reserve) {
// Trigger conflict due to reused reserve_pub
client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req)
}.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
- } else if (type == TalerIncomingType.kyc) {
+ } else if (type == IncomingType.kyc) {
// Non conflict on reuse
client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req)
@@ -299,13 +299,13 @@ class WireGatewayApiTest {
// POST /taler-wire-gateway/admin/add-incoming
@Test
fun addIncoming() = serverSetup {
- talerAddIncomingRoutine(TalerIncomingType.reserve)
+ talerAddIncomingRoutine(IncomingType.reserve)
}
// POST /taler-wire-gateway/admin/add-kycauth
@Test
fun addKycAuth() = serverSetup {
- talerAddIncomingRoutine(TalerIncomingType.kyc)
+ talerAddIncomingRoutine(IncomingType.kyc)
}
@Test
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt
b/testbench/src/test/kotlin/IntegrationTest.kt
index c2b0047e..6fba92cb 100644
--- a/testbench/src/test/kotlin/IntegrationTest.kt
+++ b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023-2024 Taler Systems S.A.
+ * Copyright (C) 2023-2025 Taler Systems S.A.
*
* LibEuFin is free software; you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -217,7 +217,7 @@ class IntegrationTest {
}.assertNoContent()
assertException("ERROR: cashin failed: admin balance
insufficient") {
- db.payment.registerTalerableIncoming(reservePayment,
TalerIncomingMetadata(TalerIncomingType.reserve, reservePub))
+ db.payment.registerTalerableIncoming(reservePayment,
IncomingSubject(IncomingType.reserve, reservePub))
}
// Allow admin debt
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated (3fcbe27f -> b5caa5c6),
gnunet <=