[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated (e396a3dd -> 096091fe)
From: |
gnunet |
Subject: |
[libeufin] branch master updated (e396a3dd -> 096091fe) |
Date: |
Mon, 05 Aug 2024 14:06:23 +0200 |
This is an automated email from the git hooks/post-receive script.
antoine pushed a change to branch master
in repository libeufin.
from e396a3dd nexus: fix test and improve logging
new 8cdd7686 bank: make customer name not null in sql schema
new cd98976b common: clean SQL enum extraction logic
new 715ffba3 common: support KYC taler incoming transaction
new 4810f439 nexus: bench and add missing index
new 5b19ae25 nexus: support KYC transaction in listing command
new 11bf2426 Merge branch 'master' into dev/antoinea/kyc
new 096091fe nexus: add an outgoing transactions status index for the
future extended wire gateway API
The 7 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:
Makefile | 8 +-
.../tech/libeufin/bank/api/WireGatewayApi.kt | 39 ++++-
.../kotlin/tech/libeufin/bank/db/AccountDAO.kt | 8 +-
.../kotlin/tech/libeufin/bank/db/CashoutDAO.kt | 2 +-
.../kotlin/tech/libeufin/bank/db/ConversionDAO.kt | 7 +-
.../kotlin/tech/libeufin/bank/db/ExchangeDAO.kt | 54 ++++---
.../main/kotlin/tech/libeufin/bank/db/TanDAO.kt | 13 +-
.../main/kotlin/tech/libeufin/bank/db/TokenDAO.kt | 9 +-
.../kotlin/tech/libeufin/bank/db/TransactionDAO.kt | 24 ++-
.../kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 11 +-
bank/src/test/kotlin/AmountTest.kt | 36 ++---
bank/src/test/kotlin/StatsTest.kt | 4 +
bank/src/test/kotlin/WireGatewayApiTest.kt | 150 +++++++++++-------
bank/src/test/kotlin/bench.kt | 174 ++++++++-------------
bank/src/test/kotlin/helpers.kt | 12 ++
common/src/main/kotlin/Client.kt | 4 +-
common/src/main/kotlin/Constants.kt | 2 +-
common/src/main/kotlin/TalerMessage.kt | 56 ++++++-
common/src/main/kotlin/TxMedatada.kt | 15 +-
common/src/main/kotlin/db/types.kt | 8 +
common/src/main/kotlin/test/bench.kt | 100 ++++++++++++
common/src/test/kotlin/TxMedataTest.kt | 7 +-
database-versioning/libeufin-bank-0007.sql | 44 ++++++
database-versioning/libeufin-bank-drop.sql | 2 +-
database-versioning/libeufin-bank-procedures.sql | 52 +++---
database-versioning/libeufin-conversion-setup.sql | 5 +
database-versioning/libeufin-nexus-0001.sql | 1 -
database-versioning/libeufin-nexus-0006.sql | 46 ++++++
database-versioning/libeufin-nexus-drop.sql | 2 +-
database-versioning/libeufin-nexus-procedures.sql | 71 ++++++---
.../kotlin/tech/libeufin/nexus/XmlCombinators.kt | 2 +-
.../tech/libeufin/nexus/api/WireGatewayApi.kt | 42 +++--
.../kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt | 4 +-
.../main/kotlin/tech/libeufin/nexus/cli/Testing.kt | 4 +-
.../kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 34 ++--
.../kotlin/tech/libeufin/nexus/db/PaymentDAO.kt | 30 +++-
nexus/src/test/kotlin/CliTest.kt | 2 +
nexus/src/test/kotlin/DatabaseTest.kt | 93 ++++++++---
nexus/src/test/kotlin/WireGatewayApiTest.kt | 147 ++++++++++-------
nexus/src/test/kotlin/bench.kt | 159 +++++++++++++++++++
nexus/src/test/kotlin/helpers.kt | 48 +++++-
nexus/src/test/kotlin/routines.kt | 6 -
testbench/src/test/kotlin/IntegrationTest.kt | 68 +++++---
testbench/src/test/kotlin/MigrationTest.kt | 24 +++
44 files changed, 1172 insertions(+), 457 deletions(-)
create mode 100644 common/src/main/kotlin/test/bench.kt
create mode 100644 database-versioning/libeufin-bank-0007.sql
create mode 100644 database-versioning/libeufin-nexus-0006.sql
create mode 100644 nexus/src/test/kotlin/bench.kt
diff --git a/Makefile b/Makefile
index 70d13dfa..729eca74 100644
--- a/Makefile
+++ b/Makefile
@@ -121,6 +121,10 @@ doc:
echo "Open build/dokka/htmlMultiModule/index.html"
.PHONY: bench-db
-bench-db: install-nobuild-files
- ./gradlew :bank:test --tests Bench.benchDb -i
+bank-bench-db: install-nobuild-files
+ ./gradlew cleanTest :bank:test --tests Bench.benchDb -i --no-build-cache
+
+.PHONY: bench-db
+nexus-bench-db: install-nobuild-files
+ ./gradlew cleanTest :nexus:test --tests Bench.benchDb -i
--no-build-cache
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 2c1a8ed9..4fc8cfc3 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/api/WireGatewayApi.kt
@@ -112,14 +112,21 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig)
{
}
}
authAdmin(db, ctx.pwCrypto, TokenScope.readwrite) {
- post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- val req = call.receive<AddIncomingRequest>()
- ctx.checkRegionalCurrency(req.amount)
+ suspend fun ApplicationCall.addIncoming(
+ amount: TalerAmount,
+ debitAccount: Payto,
+ subject: String,
+ metadata: TalerIncomingMetadata
+ ) {
+ ctx.checkRegionalCurrency(amount)
val timestamp = Instant.now()
val res = db.exchange.addIncoming(
- req = req,
+ amount = amount,
+ debitAccount = debitAccount,
+ subject = subject,
login = username,
- timestamp = timestamp
+ timestamp = timestamp,
+ metadata = metadata
)
when (res) {
is AddIncomingResult.UnknownExchange -> throw
unknownAccount(username)
@@ -128,7 +135,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE
)
is AddIncomingResult.UnknownDebtor -> throw conflict(
- "Debtor account ${req.debit_account} was not found",
+ "Debtor account $debitAccount was not found",
TalerErrorCode.BANK_UNKNOWN_DEBTOR
)
is AddIncomingResult.BothPartyAreExchange -> throw conflict(
@@ -143,7 +150,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
"Insufficient balance for debitor",
TalerErrorCode.BANK_UNALLOWED_DEBIT
)
- is AddIncomingResult.Success -> call.respond(
+ is AddIncomingResult.Success -> this.respond(
AddIncomingResponse(
timestamp = TalerProtocolTimestamp(timestamp),
row_id = res.id
@@ -151,5 +158,23 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
)
}
}
+ post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
+ val req = call.receive<AddIncomingRequest>()
+ call.addIncoming(
+ amount = req.amount,
+ debitAccount = req.debit_account,
+ subject = "Manual incoming ${req.reserve_pub}",
+ metadata = TalerIncomingMetadata(TalerIncomingType.reserve,
req.reserve_pub)
+ )
+ }
+ post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth") {
+ val req = call.receive<AddKycauthRequest>()
+ call.addIncoming(
+ amount = req.amount,
+ debitAccount = req.debit_account,
+ subject = "Manual incoming KYC:${req.account_pub}",
+ metadata = TalerIncomingMetadata(TalerIncomingType.kyc,
req.account_pub)
+ )
+ }
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
index 53cc1c94..e8be7476 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -283,7 +283,7 @@ class AccountDAO(private val db: Database) {
oneOrNull {
CurrentAccount(
id = it.getLong("customer_id"),
- channel = it.getString("tan_channel")?.run {
TanChannel.valueOf(this) },
+ channel = it.getOptEnum<TanChannel>("tan_channel"),
phone = it.getString("phone"),
email = it.getString("email"),
name = it.getString("name"),
@@ -542,7 +542,7 @@ class AccountDAO(private val db: Database) {
email = Option.Some(it.getString("email")),
phone = Option.Some(it.getString("phone"))
),
- tan_channel = it.getString("tan_channel")?.run {
TanChannel.valueOf(this) },
+ tan_channel = it.getOptEnum<TanChannel>("tan_channel"),
cashout_payto_uri = it.getString("cashout_payto"),
payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx),
balance = Balance(
@@ -558,7 +558,7 @@ class AccountDAO(private val db: Database) {
min_cashout = it.getOptAmount("min_cashout", db.bankCurrency),
is_public = it.getBoolean("is_public"),
is_taler_exchange = it.getBoolean("is_taler_exchange"),
- status = AccountStatus.valueOf(it.getString("status"))
+ status = it.getEnum("status")
)
}
}
@@ -663,7 +663,7 @@ class AccountDAO(private val db: Database) {
is_public = it.getBoolean("is_public"),
is_taler_exchange = it.getBoolean("is_taler_exchange"),
payto_uri = it.getBankPayto("internal_payto_uri", "name", ctx),
- status = AccountStatus.valueOf(it.getString("status"))
+ status = it.getEnum("status")
)
}
}
\ No newline at end of file
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 7298aa8c..ba6fd362 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt
@@ -123,7 +123,7 @@ class CashoutDAO(private val db: Database) {
0L -> null
else -> TalerProtocolTimestamp(timestamp.asInstant())
},
- tan_channel = it.getString("tan_channel")?.run {
TanChannel.valueOf(this) },
+ tan_channel = it.getOptEnum<TanChannel>("tan_channel"),
tan_info = it.getString("tan_info"),
)
}
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 d8de149f..458675af 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -23,10 +23,7 @@ import tech.libeufin.bank.ConversionRate
import tech.libeufin.bank.RoundingMode
import tech.libeufin.common.DecimalNumber
import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.db.getAmount
-import tech.libeufin.common.db.one
-import tech.libeufin.common.db.oneOrNull
-import tech.libeufin.common.db.withStatement
+import tech.libeufin.common.db.*
/** Data access logic for conversion */
class ConversionDAO(private val db: Database) {
@@ -84,7 +81,7 @@ class ConversionDAO(private val db: Database) {
fun getRatio(name: String): DecimalNumber = getAmount(name, "").run {
DecimalNumber(value, frac) }
fun getMode(name: String): RoundingMode {
roundingMode.setString(1, name)
- return roundingMode.oneOrNull {
RoundingMode.valueOf(it.getString(1)) }!!
+ return roundingMode.one { it.getEnum<RoundingMode>(1) }
}
val rate = ConversionRate(
cashin_ratio = getRatio("cashin_ratio"),
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 bba13c1e..5f497582 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -19,6 +19,7 @@
package tech.libeufin.bank.db
+import tech.libeufin.bank.*
import tech.libeufin.common.*
import tech.libeufin.common.db.*
import java.time.Instant
@@ -30,7 +31,7 @@ class ExchangeDAO(private val db: Database) {
params: HistoryParams,
exchangeId: Long,
ctx: BankPaytoCtx
- ): List<IncomingReserveTransaction>
+ ): List<IncomingBankTransaction>
= db.poolHistory(params, exchangeId, db::listenIncoming, """
SELECT
bank_transaction_id
@@ -39,19 +40,32 @@ class ExchangeDAO(private val db: Database) {
,(amount).frac AS amount_frac
,debtor_payto_uri
,debtor_name
+ ,type
,reserve_pub
+ ,account_pub
FROM taler_exchange_incoming AS tfr
JOIN bank_account_transactions AS txs
ON bank_transaction=txs.bank_transaction_id
WHERE
""") {
- 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_uri",
"debtor_name", ctx),
- reserve_pub = EddsaPublicKey(it.getBytes("reserve_pub")),
- )
+ val type = it.getEnum<TalerIncomingType>("type")
+ when (type) {
+ TalerIncomingType.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_uri",
"debtor_name", ctx),
+ reserve_pub = EddsaPublicKey(it.getBytes("reserve_pub")),
+ )
+ TalerIncomingType.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_uri",
"debtor_name", ctx),
+ account_pub = EddsaPublicKey(it.getBytes("account_pub")),
+ )
+ TalerIncomingType.wad -> throw UnsupportedOperationException()
+ }
}
/** Query [exchangeId] history of taler outgoing transactions */
@@ -163,9 +177,12 @@ class ExchangeDAO(private val db: Database) {
/** Add a new taler incoming transaction */
suspend fun addIncoming(
- req: AddIncomingRequest,
+ amount: TalerAmount,
+ debitAccount: Payto,
+ subject: String,
login: String,
- timestamp: Instant
+ timestamp: Instant,
+ metadata: TalerIncomingMetadata
): AddIncomingResult = db.serializable(
"""
SELECT
@@ -178,19 +195,20 @@ class ExchangeDAO(private val db: Database) {
,out_tx_row_id
FROM
taler_add_incoming (
- ?, ?,
- (?,?)::taler_amount,
- ?, ?, ?
+ ?, ?, (?,?)::taler_amount,
+ ?, ?, ?, ?::taler_incoming_type
);
"""
) {
- setBytes(1, req.reserve_pub.raw)
- setString(2, "Manual incoming ${req.reserve_pub}")
- setLong(3, req.amount.value)
- setInt(4, req.amount.frac)
- setString(5, req.debit_account.canonical)
+ println(metadata)
+ setBytes(1, metadata.key.raw)
+ setString(2, subject)
+ setLong(3, amount.value)
+ setInt(4, amount.frac)
+ setString(5, debitAccount.canonical)
setString(6, login)
setLong(7, timestamp.micros())
+ setString(8, metadata.type.name)
one {
when {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
index 393f452b..b4747cec 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TanDAO.kt
@@ -21,8 +21,7 @@ package tech.libeufin.bank.db
import tech.libeufin.bank.Operation
import tech.libeufin.bank.TanChannel
-import tech.libeufin.common.db.one
-import tech.libeufin.common.db.oneOrNull
+import tech.libeufin.common.db.*
import tech.libeufin.common.internalServerError
import tech.libeufin.common.micros
import java.time.Duration
@@ -85,9 +84,9 @@ class TanDAO(private val db: Database) {
one {
when {
it.getBoolean("out_no_op") -> TanSendResult.NotFound
- else -> TanSendResult.Success(
+ else -> TanSendResult.Success(
tanInfo = it.getString("out_tan_info"),
- tanChannel = it.getString("out_tan_channel").run {
TanChannel.valueOf(this) },
+ tanChannel = it.getEnum("out_tan_channel"),
tanCode = it.getString("out_tan_code")
)
}
@@ -139,8 +138,8 @@ class TanDAO(private val db: Database) {
when {
it.getBoolean("out_ok") -> TanSolveResult.Success(
body = it.getString("out_body"),
- op = Operation.valueOf(it.getString("out_op")),
- channel = it.getString("out_channel")?.run {
TanChannel.valueOf(this) },
+ op = it.getEnum("out_op"),
+ channel = it.getOptEnum<TanChannel>("out_channel"),
info = it.getString("out_info")
)
it.getBoolean("out_no_op") -> TanSolveResult.NotFound
@@ -176,7 +175,7 @@ class TanDAO(private val db: Database) {
oneOrNull {
Challenge(
body = it.getString("body"),
- channel = it.getString("tan_channel")?.run {
TanChannel.valueOf(this) },
+ channel = it.getOptEnum<TanChannel>("tan_channel"),
info = it.getString("tan_info")
)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
index b03ab409..bae86f75 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TokenDAO.kt
@@ -24,10 +24,7 @@ import tech.libeufin.bank.TokenInfo
import tech.libeufin.bank.TokenScope
import tech.libeufin.common.PageParams
import tech.libeufin.common.asInstant
-import tech.libeufin.common.db.executeUpdateViolation
-import tech.libeufin.common.db.getTalerTimestamp
-import tech.libeufin.common.db.oneOrNull
-import tech.libeufin.common.db.page
+import tech.libeufin.common.db.*
import tech.libeufin.common.micros
import java.time.Instant
@@ -93,7 +90,7 @@ class TokenDAO(private val db: Database) {
creationTime = it.getLong("creation_time").asInstant(),
expirationTime = it.getLong("expiration_time").asInstant(),
login = it.getString("login"),
- scope = TokenScope.valueOf(it.getString("scope")),
+ scope = it.getEnum("scope"),
isRefreshable = it.getBoolean("is_refreshable")
)
}
@@ -134,7 +131,7 @@ class TokenDAO(private val db: Database) {
TokenInfo(
creation_time = it.getTalerTimestamp("creation_time"),
expiration = it.getTalerTimestamp("expiration_time"),
- scope = TokenScope.valueOf(it.getString("scope")),
+ scope = it.getEnum("scope"),
isRefreshable = it.getBoolean("is_refreshable"),
description = it.getString("description"),
last_access = it.getTalerTimestamp("last_access"),
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 89f11aa4..f86b11f6 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -26,6 +26,7 @@ import tech.libeufin.bank.TransactionDirection
import tech.libeufin.common.*
import tech.libeufin.common.db.*
import java.time.Instant
+import java.sql.Types
private val logger: Logger = LoggerFactory.getLogger("libeufin-bank-tx-dao")
@@ -114,10 +115,21 @@ class TransactionDAO(private val db: Database) {
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(
- onSuccess = { reservePub ->
- val registered = conn.withStatement("CALL
register_incoming(?, ?)") {
- setBytes(1, reservePub.raw)
- setLong(2, creditRowId)
+ onSuccess = { metadata ->
+ val registered = conn.withStatement("CALL
register_incoming(?, ?::taler_incoming_type, ?, ?)") {
+ setLong(1, creditRowId)
+ setString(2, metadata.type.name)
+ when (metadata.type) {
+ TalerIncomingType.reserve -> {
+ setBytes(3, metadata.key.raw)
+ setNull(4, Types.BINARY)
+ }
+ TalerIncomingType.kyc -> {
+ setNull(3, Types.BINARY)
+ setBytes(4, metadata.key.raw)
+ }
+ TalerIncomingType.wad -> throw
UnsupportedOperationException()
+ }
executeProcedureViolation()
}
if (!registered) {
@@ -185,7 +197,7 @@ class TransactionDAO(private val db: Database) {
creditor_payto_uri = it.getBankPayto("creditor_payto_uri",
"creditor_name", ctx),
debtor_payto_uri = it.getBankPayto("debtor_payto_uri",
"debtor_name", ctx),
amount = it.getAmount("amount", db.bankCurrency),
- direction =
TransactionDirection.valueOf(it.getString("direction")),
+ direction = it.getEnum("direction"),
subject = it.getString("subject"),
date = it.getTalerTimestamp("transaction_date"),
row_id = it.getLong("bank_transaction_id")
@@ -220,7 +232,7 @@ class TransactionDAO(private val db: Database) {
debtor_payto_uri = it.getBankPayto("debtor_payto_uri",
"debtor_name", ctx),
amount = it.getAmount("amount", db.bankCurrency),
subject = it.getString("subject"),
- direction =
TransactionDirection.valueOf(it.getString("direction"))
+ direction = it.getEnum("direction")
)
}
}
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 af9c725a..abfc563f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -27,10 +27,7 @@ import tech.libeufin.bank.*
import tech.libeufin.common.EddsaPublicKey
import tech.libeufin.common.Payto
import tech.libeufin.common.TalerAmount
-import tech.libeufin.common.db.getAmount
-import tech.libeufin.common.db.getOptAmount
-import tech.libeufin.common.db.one
-import tech.libeufin.common.db.oneOrNull
+import tech.libeufin.common.db.*
import tech.libeufin.common.micros
import java.time.Instant
import java.util.*
@@ -191,7 +188,7 @@ class WithdrawalDAO(private val db: Database) {
it.getBoolean("out_reserve_pub_reuse") ->
WithdrawalSelectionResult.RequestPubReuse
it.getBoolean("out_account_not_found") ->
WithdrawalSelectionResult.UnknownAccount
it.getBoolean("out_account_is_not_exchange") ->
WithdrawalSelectionResult.AccountIsNotExchange
- else ->
WithdrawalSelectionResult.Success(WithdrawalStatus.valueOf(it.getString("out_status")))
+ else ->
WithdrawalSelectionResult.Success(it.getEnum("out_status"))
}
}
}
@@ -331,7 +328,7 @@ class WithdrawalDAO(private val db: Database) {
setObject(1, uuid)
oneOrNull {
WithdrawalPublicInfo(
- status =
WithdrawalStatus.valueOf(it.getString("status")),
+ status = it.getEnum("status"),
amount = it.getOptAmount("amount", db.bankCurrency),
suggested_amount = it.getOptAmount("suggested_amount",
db.bankCurrency),
username = it.getString("login"),
@@ -382,7 +379,7 @@ class WithdrawalDAO(private val db: Database) {
setObject(3, uuid)
oneOrNull {
BankWithdrawalOperationStatus(
- status =
WithdrawalStatus.valueOf(it.getString("status")),
+ status = it.getEnum("status"),
amount = it.getOptAmount("amount", db.bankCurrency),
suggested_amount = it.getOptAmount("suggested_amount",
db.bankCurrency),
max_amount = it.getAmount("max_amount",
db.bankCurrency),
diff --git a/bank/src/test/kotlin/AmountTest.kt
b/bank/src/test/kotlin/AmountTest.kt
index db4b3201..f4f26d49 100644
--- a/bank/src/test/kotlin/AmountTest.kt
+++ b/bank/src/test/kotlin/AmountTest.kt
@@ -264,16 +264,16 @@ class AmountTest {
@Test
fun conversionRevert() = dbSetup { db ->
db.conn { conn ->
+ val applyStmt = conn.prepareStatement("SELECT amount.val,
amount.frac FROM conversion_apply_ratio((?, ?)::taler_amount, (?,
?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode) as amount")
fun TalerAmount.apply(ratio: DecimalNumber, tiny: DecimalNumber =
DecimalNumber("0.00000001"), roundingMode: String = "zero"): TalerAmount {
- val stmt = conn.prepareStatement("SELECT amount.val,
amount.frac FROM conversion_apply_ratio((?, ?)::taler_amount, (?,
?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode) as amount")
- stmt.setLong(1, this.value)
- stmt.setInt(2, this.frac)
- stmt.setLong(3, ratio.value)
- stmt.setInt(4, ratio.frac)
- stmt.setLong(5, tiny.value)
- stmt.setInt(6, tiny.frac)
- stmt.setString(7, roundingMode)
- return stmt.one {
+ applyStmt.setLong(1, this.value)
+ applyStmt.setInt(2, this.frac)
+ applyStmt.setLong(3, ratio.value)
+ applyStmt.setInt(4, ratio.frac)
+ applyStmt.setLong(5, tiny.value)
+ applyStmt.setInt(6, tiny.frac)
+ applyStmt.setString(7, roundingMode)
+ return applyStmt.one {
TalerAmount(
it.getLong(1),
it.getInt(2),
@@ -282,16 +282,16 @@ class AmountTest {
}
}
+ val revertStmt = conn.prepareStatement("SELECT amount.val,
amount.frac FROM conversion_revert_ratio((?, ?)::taler_amount, (?,
?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode) as amount")
fun TalerAmount.revert(ratio: DecimalNumber, tiny: DecimalNumber =
DecimalNumber("0.00000001"), roundingMode: String = "zero"): TalerAmount {
- val stmt = conn.prepareStatement("SELECT amount.val,
amount.frac FROM conversion_revert_ratio((?, ?)::taler_amount, (?,
?)::taler_amount, (?, ?)::taler_amount, ?::rounding_mode) as amount")
- stmt.setLong(1, this.value)
- stmt.setInt(2, this.frac)
- stmt.setLong(3, ratio.value)
- stmt.setInt(4, ratio.frac)
- stmt.setLong(5, tiny.value)
- stmt.setInt(6, tiny.frac)
- stmt.setString(7, roundingMode)
- return stmt.one {
+ revertStmt.setLong(1, this.value)
+ revertStmt.setInt(2, this.frac)
+ revertStmt.setLong(3, ratio.value)
+ revertStmt.setInt(4, ratio.frac)
+ revertStmt.setLong(5, tiny.value)
+ revertStmt.setInt(6, tiny.frac)
+ revertStmt.setString(7, roundingMode)
+ return revertStmt.one {
TalerAmount(
it.getLong(1),
it.getInt(2),
diff --git a/bank/src/test/kotlin/StatsTest.kt
b/bank/src/test/kotlin/StatsTest.kt
index f1e57b8b..11e1e3e3 100644
--- a/bank/src/test/kotlin/StatsTest.kt
+++ b/bank/src/test/kotlin/StatsTest.kt
@@ -91,6 +91,10 @@ class StatsTest {
monitorTalerIn(2, "KUDOS:10.6")
addIncoming("KUDOS:12.3")
monitorTalerIn(3, "KUDOS:22.9")
+
+ // KYC are ignored
+ addKyc("KUDOS:3")
+ monitorTalerIn(3, "KUDOS:22.9")
transfer("KUDOS:10.0")
monitorTalerOut(1, "KUDOS:10.0")
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt
b/bank/src/test/kotlin/WireGatewayApiTest.kt
index bbfd7f14..752c60e9 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 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -18,8 +18,10 @@
*/
import io.ktor.http.*
+import io.ktor.server.testing.*
import org.junit.Test
import tech.libeufin.common.*
+import kotlin.test.*
class WireGatewayApiTest {
// GET /accounts/{USERNAME}/taler-wire-gateway/config
@@ -131,28 +133,27 @@ class WireGatewayApiTest {
url = "/accounts/exchange/taler-wire-gateway/history/incoming",
ids = { it.incoming_transactions.map { it.row_id } },
registered = listOf(
- {
- // Transactions using clean add incoming logic
- addIncoming("KUDOS:10")
- },
- {
- // Transactions using raw bank transaction logic
- tx("merchant", "KUDOS:10", "exchange", "history test with
${ShortHashCode.rand()} reserve pub")
- },
- {
- // Transaction using withdraw logic
- withdrawal("KUDOS:9")
- }
+ // Reserve transactions using clean add incoming logic
+ { addIncoming("KUDOS:10") },
+
+ // Reserve transactions using raw bank transaction logic
+ { tx("merchant", "KUDOS:10", "exchange", "history test with
${ShortHashCode.rand()} reserve pub") },
+
+ // Reserve transactions using withdraw logic
+ { withdrawal("KUDOS:9") },
+
+ // KYC transaction using clean add incoming logic
+ { addKyc("KUDOS:2") },
+
+ // KYC transactions using raw bank transaction logic
+ { tx("merchant", "KUDOS:2", "exchange", "history test with
KYC:${ShortHashCode.rand()} account pub") },
),
ignored = listOf(
- {
- // Ignore malformed incoming transaction
- tx("merchant", "KUDOS:10", "exchange", "ignored")
- },
- {
- // Ignore malformed outgoing transaction
- tx("exchange", "KUDOS:10", "merchant", "ignored")
- }
+ // Ignore malformed incoming transaction
+ { tx("merchant", "KUDOS:10", "exchange", "ignored") },
+
+ // Ignore malformed outgoing transaction
+ { tx("exchange", "KUDOS:10", "merchant", "ignored") },
)
)
}
@@ -166,88 +167,123 @@ class WireGatewayApiTest {
url = "/accounts/exchange/taler-wire-gateway/history/outgoing",
ids = { it.outgoing_transactions.map { it.row_id } },
registered = listOf(
- {
- // Transactions using clean add incoming logic
- transfer("KUDOS:10")
- }
+ // Transactions using clean add incoming logic
+ { transfer("KUDOS:10") }
),
ignored = listOf(
- {
- // gnore manual incoming transaction
- tx("exchange", "KUDOS:10", "merchant",
"${ShortHashCode.rand()} http://exchange.example.com/")
- },
- {
- // Ignore malformed incoming transaction
- tx("merchant", "KUDOS:10", "exchange", "ignored")
- },
- {
- // Ignore malformed outgoing transaction
- tx("exchange", "KUDOS:10", "merchant", "ignored")
- }
+ // Ignore manual outgoing transaction
+ { tx("exchange", "KUDOS:10", "merchant",
"${ShortHashCode.rand()} http://exchange.example.com/") },
+
+ // Ignore malformed incoming transaction
+ { tx("merchant", "KUDOS:10", "exchange", "ignored") },
+
+ // Ignore malformed outgoing transaction
+ { tx("exchange", "KUDOS:10", "merchant", "ignored") },
)
)
}
- // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming
- @Test
- fun addIncoming() = bankSetup {
+ suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
TalerIncomingType) {
+ val (path, key) = when (type) {
+ TalerIncomingType.reserve -> Pair("add-incoming", "reserve_pub")
+ TalerIncomingType.kyc -> Pair("add-kycauth", "account_pub")
+ TalerIncomingType.wad -> throw UnsupportedOperationException()
+ }
val valid_req = obj {
"amount" to "KUDOS:44"
- "reserve_pub" to EddsaPublicKey.rand()
+ key to EddsaPublicKey.rand()
"debit_account" to merchantPayto.canonical
}
- authRoutine(HttpMethod.Post,
"/accounts/merchant/taler-wire-gateway/admin/add-incoming", valid_req,
requireAdmin = true)
+ authRoutine(HttpMethod.Post,
"/accounts/merchant/taler-wire-gateway/admin/$path", valid_req, requireAdmin =
true)
// Checking exchange debt constraint.
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req)
}.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
// Giving debt allowance and checking the OK case.
setMaxDebt("merchant", "KUDOS:1000")
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req)
}.assertOk()
- // Trigger conflict due to reused reserve_pub
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
- json(valid_req)
- }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
+ if (type == TalerIncomingType.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) {
+ // Non conflict on reuse
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
+ json(valid_req)
+ }.assertOk()
+ }
// Currency mismatch
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req) { "amount" to "EUR:33" }
}.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
// Unknown account
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to EddsaPublicKey.rand()
+ key to EddsaPublicKey.rand()
"debit_account" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR)
// Same account
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to EddsaPublicKey.rand()
+ key to EddsaPublicKey.rand()
"debit_account" to exchangePayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
// Bad BASE32 reserve_pub
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to "I love chocolate"
+ key to "I love chocolate"
}
}.assertBadRequest()
// Bad BASE32 len reserve_pub
-
client.postA("/accounts/exchange/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/accounts/exchange/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to randBase32Crockford(31)
+ key to randBase32Crockford(31)
}
}.assertBadRequest()
}
+
+ // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming
+ @Test
+ fun addIncoming() = bankSetup {
+ talerAddIncomingRoutine(TalerIncomingType.reserve)
+ }
+
+ // POST /accounts/{USERNAME}/taler-wire-gateway/admin/add-kycauth
+ @Test
+ fun addKycAuth() = bankSetup {
+ talerAddIncomingRoutine(TalerIncomingType.kyc)
+ }
+
+ @Test
+ fun addIncomingMix() = bankSetup {
+ addIncoming("KUDOS:1")
+ addKyc("KUDOS:2")
+ tx("merchant", "KUDOS:3", "exchange", "test with
${ShortHashCode.rand()} reserve pub")
+ tx("merchant", "KUDOS:4", "exchange", "test with
KYC:${ShortHashCode.rand()} account pub")
+
client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?delta=25").assertOkJson<IncomingHistory>
{
+ assertEquals(4, it.incoming_transactions.size)
+ it.incoming_transactions.forEachIndexed { i, tx ->
+ assertEquals(TalerAmount("KUDOS:${i+1}"), tx.amount)
+ if (i % 2 == 1) {
+ assertIs<IncomingKycAuthTransaction>(tx)
+ } else {
+ assertIs<IncomingReserveTransaction>(tx)
+ }
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/bench.kt b/bank/src/test/kotlin/bench.kt
index 62c8e7ec..9bf15072 100644
--- a/bank/src/test/kotlin/bench.kt
+++ b/bank/src/test/kotlin/bench.kt
@@ -23,6 +23,7 @@ import org.junit.Test
import org.postgresql.jdbc.PgConnection
import tech.libeufin.bank.*
import tech.libeufin.common.*
+import tech.libeufin.common.test.*
import tech.libeufin.common.crypto.PwCrypto
import java.time.Instant
import java.time.LocalDateTime
@@ -49,73 +50,66 @@ class Bench {
// In general half of the data is for generated account and half is
for customer
val mid = amount / 2
- val copyManager = conn.getCopyAPI()
val password = PwCrypto.Bcrypt(cost = 4).hashpw("password")
- fun gen(table: String, lambda: (Int) -> String) {
- println("Gen rows for $table")
- val full = buildString(150*amount) {
- repeat(amount) {
- append(lambda(it+1))
- }
- }
- copyManager.copyIn("COPY $table FROM STDIN", full.reader())
- }
-
+
val token32 = ByteArray(32)
val token64 = ByteArray(64)
- gen("customers(login, name, password_hash, cashout_payto)") {
- "account_$it\t$password\tMr n°$it\t$unknownPayto\n"
- }
- gen("bank_accounts(internal_payto_uri, owning_customer_id,
is_public)") {
-
"payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\n"
- }
- gen("bearer_tokens(content, creation_time, expiration_time, scope,
is_refreshable, bank_customer, description, last_access)") {
- val account = if (it > mid) customerAccount else it+4
- val hex = token32.rand().encodeHex()
- "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n"
- }
- gen("bank_account_transactions(creditor_payto_uri, creditor_name,
debtor_payto_uri, debtor_name, subject, amount, transaction_date, direction,
bank_account_id)") {
- val account = if (it > mid) customerAccount else it+4
-
"creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n"
+
-
"creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n"
- }
- gen("bank_transaction_operations") {
- val hex = token32.rand().encodeHex()
- "\\\\x$hex\t$it\n"
- }
- gen("tan_challenges(body, op, code, creation_date, expiration_date,
retry_counter, customer)") {
- val account = if (it > mid) customerAccount else it+4
- "body\taccount_reconfig\tcode\t0\t0\t0\t$account\n"
- }
- gen("taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account,
reserve_pub, creation_date)") {
- val account = if (it > mid) customerAccount else it+4
- val hex = token32.rand().encodeHex()
- val uuid = UUID.randomUUID()
- "$uuid\t$account\t\\\\x$hex\t0\n"
- }
- gen("taler_exchange_outgoing(wtid, request_uid, exchange_base_url,
bank_transaction, creditor_account_id)") {
- val hex32 = token32.rand().encodeHex()
- val hex64 = token64.rand().encodeHex()
- "\\\\x$hex32\t\\\\x$hex64\turl\t${it*2-1}\t$it\n"
- }
- gen("taler_exchange_incoming(reserve_pub, bank_transaction)") {
- val hex = token32.rand().encodeHex()
- "\\\\x$hex\t${it*2}\n"
- }
- gen("bank_stats(timeframe, start_time)") {
- val instant = Instant.ofEpochSecond(it.toLong())
- val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
- "day\t$date\n"
- }
-
gen("cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)")
{
- val account = if (it > mid) customerAccount else it+4
- val hex = token32.rand().encodeHex()
- "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n"
- }
-
- // Update database statistics for better perf
- conn.execSQLUpdate("VACUUM ANALYZE");
+ conn.genData(amount, sequenceOf(
+ "customers(login, name, password_hash, cashout_payto)" to {
+ "account_$it\t$password\tMr n°$it\t$unknownPayto\n"
+ },
+ "bank_accounts(internal_payto_uri, owning_customer_id, is_public)"
to {
+
"payto://x-taler-bank/localhost/account_$it\t${it+skipAccount}\t${it%3==0}\n"
+ },
+ "bearer_tokens(content, creation_time, expiration_time, scope,
is_refreshable, bank_customer, description, last_access)" to {
+ val account = if (it > mid) customerAccount else it+4
+ val hex = token32.rand().encodeHex()
+ "\\\\x$hex\t0\t0\treadonly\tfalse\t$account\t\\N\t0\n"
+ },
+ "bank_account_transactions(creditor_payto_uri, creditor_name,
debtor_payto_uri, debtor_name, subject, amount, transaction_date, direction,
bank_account_id)" to {
+ val account = if (it > mid) customerAccount else it+4
+
"creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tcredit\t$exchangeAccount\n"
+
+
"creditor_payto\tcreditor_name\tdebtor_payto\tdebtor_name\tsubject\t(42,0)\t0\tdebit\t$account\n"
+ },
+ "bank_transaction_operations" to {
+ val hex = token32.rand().encodeHex()
+ "\\\\x$hex\t$it\n"
+ },
+ "tan_challenges(body, op, code, creation_date, expiration_date,
retry_counter, customer)" to {
+ val account = if (it > mid) customerAccount else it+4
+ "body\taccount_reconfig\tcode\t0\t0\t0\t$account\n"
+ },
+ "taler_withdrawal_operations(withdrawal_uuid, wallet_bank_account,
reserve_pub, creation_date)" to {
+ val account = if (it > mid) customerAccount else it+4
+ val hex = token32.rand().encodeHex()
+ val uuid = UUID.randomUUID()
+ "$uuid\t$account\t\\\\x$hex\t0\n"
+ },
+ "taler_exchange_outgoing(wtid, request_uid, exchange_base_url,
bank_transaction, creditor_account_id)" to {
+ val hex32 = token32.rand().encodeHex()
+ val hex64 = token64.rand().encodeHex()
+ "\\\\x$hex32\t\\\\x$hex64\turl\t${it*2-1}\t$it\n"
+ },
+ "taler_exchange_incoming(type, reserve_pub, account_pub,
bank_transaction)" to {
+ val hex = token32.rand().encodeHex()
+ if (it % 2 == 0) {
+ "reserve\t\\\\x$hex\t\\N\t${it*2}\n"
+ } else {
+ "kyc\t\\N\t\\\\x$hex\t${it*2}\n"
+ }
+ },
+ "bank_stats(timeframe, start_time)" to {
+ val instant = Instant.ofEpochSecond(it.toLong())
+ val date = LocalDateTime.ofInstant(instant, ZoneId.of("UTC"))
+ "day\t$date\n"
+ },
+
"cashout_operations(request_uid,amount_debit,amount_credit,subject,creation_time,bank_account,local_transaction)"
to {
+ val account = if (it > mid) customerAccount else it+4
+ val hex = token32.rand().encodeHex()
+ "\\\\x$hex\t(0,0)\t(0,0)\tsubject\t0\t$account\t$it\n"
+ }
+ ))
}
@Test
@@ -129,45 +123,7 @@ class Bench {
}
println("Bench $ITER times with $AMOUNT rows")
- val WARN = 4.toDuration(DurationUnit.MILLISECONDS)
- val ERR = 50.toDuration(DurationUnit.MILLISECONDS)
-
-
- suspend fun fmtMeasures(times: LongArray): List<String> {
- val min: Long = times.min()
- val max: Long = times.max()
- val mean: Long = times.average().toLong()
- val variance = times.map { (it.toDouble() - mean).pow(2)
}.average()
- val stdVar: Long = sqrt(variance.toDouble()).toLong()
- return sequenceOf(min, mean, max, stdVar).map {
- val duration = it.toDuration(DurationUnit.MICROSECONDS)
- val str = duration.toString()
- if (duration > ERR) {
- ANSI.red(str)
- } else if (duration > WARN) {
- ANSI.yellow(str)
- } else {
- ANSI.green(str)
- }
-
- }.toList()
- }
-
- val measures: MutableList<List<String>> = mutableListOf()
-
- suspend fun <R> measureAction(name: String, lambda: suspend (Int) ->
R): List<R> {
- val results = mutableListOf<R>()
- val times = LongArray(ITER) { idx ->
- measureTime {
- val result = lambda(idx)
- results.add(result)
- }.inWholeMicroseconds
- }
- measures.add(listOf(ANSI.magenta(name)) + fmtMeasures(times))
- return results
- }
-
- bankSetup { db ->
+ bench(ITER) { bankSetup { db ->
// Prepare custoemr accounts
fillCashoutInfo("customer")
setMaxDebt("customer", "KUDOS:1000000")
@@ -175,6 +131,9 @@ class Bench {
// Generate data
db.conn { genData(it, AMOUNT) }
+ // Warm HTTP client
+ client.get("/config").assertOk()
+
// Accounts
val paytos = measureAction("account_create") {
client.post("/accounts") {
@@ -385,15 +344,6 @@ class Bench {
measureAction("gc") {
db.gc.collect(Instant.now(), java.time.Duration.ZERO,
java.time.Duration.ZERO, java.time.Duration.ZERO)
}
- }
-
- val cols = IntArray(5) { 0 }
-
- printTable(
- listOf("benchmark", "min", "mean", "max", "std").map {
ANSI.bold(it) },
- measures,
- ' ',
- listOf(ColumnStyle.DEFAULT) + List(5) { ColumnStyle(false) }
- )
+ } }
}
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index dda84ad2..af048494 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -231,6 +231,18 @@ suspend fun ApplicationTestBuilder.addIncoming(amount:
String) {
}.assertOk()
}
+/** Perform a taler kyc transaction of [amount] from merchant to exchange */
+suspend fun ApplicationTestBuilder.addKyc(amount: String) {
+ client.post("/accounts/exchange/taler-wire-gateway/admin/add-kycauth") {
+ pwAuth("admin")
+ json {
+ "amount" to TalerAmount(amount)
+ "account_pub" to EddsaPublicKey.rand()
+ "debit_account" to merchantPayto
+ }
+ }.assertOk()
+}
+
/** Perform a cashout operation of [amount] from customer */
suspend fun ApplicationTestBuilder.cashout(amount: String) {
val res = client.postA("/accounts/customer/cashouts") {
diff --git a/common/src/main/kotlin/Client.kt b/common/src/main/kotlin/Client.kt
index ef431e25..6306d733 100644
--- a/common/src/main/kotlin/Client.kt
+++ b/common/src/main/kotlin/Client.kt
@@ -1,6 +1,6 @@
/*
* This file is part of LibEuFin.
- * Copyright (C) 2023 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -79,7 +79,7 @@ suspend inline fun <reified B>
HttpResponse.assertAcceptedJson(lambda: (B) -> Un
/* ----- Assert ----- */
suspend fun HttpResponse.assertStatus(status: HttpStatusCode, err:
TalerErrorCode?): HttpResponse {
- assertEquals(status, this.status, "$err")
+ assertEquals(status, this.status, if (err != null) "$err" else err)
if (err != null) {
val body = json<TalerError>()
assertEquals(err.code, body.code)
diff --git a/common/src/main/kotlin/Constants.kt
b/common/src/main/kotlin/Constants.kt
index cd25117a..f7926460 100644
--- a/common/src/main/kotlin/Constants.kt
+++ b/common/src/main/kotlin/Constants.kt
@@ -26,7 +26,7 @@ const val SERIALIZATION_RETRY: Int = 10
const val MAX_BODY_LENGTH: Int = 4 * 1024 // 4kB
// API version
-const val WIRE_GATEWAY_API_VERSION: String = "0:2:0"
+const val WIRE_GATEWAY_API_VERSION: String = "1:0:0"
const val REVENUE_API_VERSION: String = "0:0:0"
// HTTP headers
diff --git a/common/src/main/kotlin/TalerMessage.kt
b/common/src/main/kotlin/TalerMessage.kt
index 027700c5..04e3de44 100644
--- a/common/src/main/kotlin/TalerMessage.kt
+++ b/common/src/main/kotlin/TalerMessage.kt
@@ -19,7 +19,13 @@
package tech.libeufin.common
-import kotlinx.serialization.Serializable
+import kotlinx.serialization.*
+
+enum class TalerIncomingType {
+ reserve,
+ kyc,
+ wad
+}
/** Response GET /taler-wire-gateway/config */
@Serializable
@@ -62,23 +68,57 @@ data class AddIncomingResponse(
val row_id: Long
)
+/** Request POST /taler-wire-gateway/admin/add-kycauth */
+@Serializable
+data class AddKycauthRequest(
+ val amount: TalerAmount,
+ val account_pub: EddsaPublicKey,
+ val debit_account: Payto
+)
+
/** Request GET /taler-wire-gateway/history/incoming */
@Serializable
data class IncomingHistory(
- val incoming_transactions: List<IncomingReserveTransaction>,
+ val incoming_transactions: List<IncomingBankTransaction>,
val credit_account: String
)
/** Inner request GET /taler-wire-gateway/history/incoming */
@Serializable
+sealed interface IncomingBankTransaction {
+ val row_id: Long
+ val date: TalerProtocolTimestamp
+ val amount: TalerAmount
+ val debit_account: String
+};
+@Serializable
+@SerialName("KYCAUTH")
+data class IncomingKycAuthTransaction(
+ override val row_id: Long,
+ override val date: TalerProtocolTimestamp,
+ override val amount: TalerAmount,
+ override val debit_account: String,
+ val account_pub: EddsaPublicKey
+): IncomingBankTransaction
+@Serializable
+@SerialName("RESERVE")
data class IncomingReserveTransaction(
- val type: String = "RESERVE",
- val row_id: Long, // DB row ID of the payment.
- val date: TalerProtocolTimestamp,
- val amount: TalerAmount,
- val debit_account: String,
+ override val row_id: Long,
+ override val date: TalerProtocolTimestamp,
+ override val amount: TalerAmount,
+ override val debit_account: String,
val reserve_pub: EddsaPublicKey
-)
+): IncomingBankTransaction
+@Serializable
+@SerialName("WAD")
+data class IncomingWadTransaction(
+ override val row_id: Long,
+ override val date: TalerProtocolTimestamp,
+ override val amount: TalerAmount,
+ override val debit_account: String,
+ val origin_exchange_url: String,
+ val wad_id: String // TODO 24 bytes Base32
+): IncomingBankTransaction
/** Request GET /taler-wire-gateway/history/outgoing */
@Serializable
diff --git a/common/src/main/kotlin/TxMedatada.kt
b/common/src/main/kotlin/TxMedatada.kt
index d68f7160..3f430128 100644
--- a/common/src/main/kotlin/TxMedatada.kt
+++ b/common/src/main/kotlin/TxMedatada.kt
@@ -16,32 +16,37 @@
* License along with LibEuFin; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>
*/
+
package tech.libeufin.common
-private val BASE32_32B_UPPER_PATTERN = Regex("[0-9A-Z]{52}")
-private val BASE32_32B_PATTERN = Regex("[a-z0-9A-Z]{52}")
+private val BASE32_32B_UPPER_PATTERN = Regex("(KYC:)?([0-9A-Z]{52})")
+private val BASE32_32B_PATTERN = Regex("(KYC:)?([a-z0-9A-Z]{52})")
private val CLEAN_PATTERN = Regex(" ?[\\n\\-\\+] ?")
+data class TalerIncomingMetadata(val type: TalerIncomingType, val key:
EddsaPublicKey)
+
/**
* Extract the reserve public key from an incoming Taler transaction subject
*
* We first try to match an uppercase key then a lowercase key. If none are
* found we clean the subject and retry.
**/
-fun parseIncomingTxMetadata(subject: String): EddsaPublicKey {
+fun parseIncomingTxMetadata(subject: String): TalerIncomingMetadata {
/**
* Extract the reserve public key from [subject] using [pattern]
*
* Return null if found none and throw if find too many
**/
- fun run(subject: String, pattern: Regex): EddsaPublicKey? {
+ fun run(subject: String, pattern: Regex): TalerIncomingMetadata? {
val matches = pattern.findAll(subject).iterator()
if (!matches.hasNext()) return null
val match = matches.next()
if (matches.hasNext()) {
throw Exception("Found multiple reserve public key")
}
- return EddsaPublicKey(match.value)
+ val (prefix, key) = match.destructured
+ val type = if (prefix == "KYC:") TalerIncomingType.kyc else
TalerIncomingType.reserve
+ return TalerIncomingMetadata(type, EddsaPublicKey(key))
}
// Wire transfer subjects are generally small in size, and not
diff --git a/common/src/main/kotlin/db/types.kt
b/common/src/main/kotlin/db/types.kt
index 11584b1c..1f48290b 100644
--- a/common/src/main/kotlin/db/types.kt
+++ b/common/src/main/kotlin/db/types.kt
@@ -22,6 +22,14 @@ package tech.libeufin.common.db
import tech.libeufin.common.*
import java.sql.ResultSet
+inline fun <reified T : kotlin.Enum<T>> ResultSet.getEnum(name: String): T
+ = java.lang.Enum.valueOf(T::class.java, getString(name))
+inline fun <reified T : kotlin.Enum<T>> ResultSet.getEnum(idx: Int): T
+ = java.lang.Enum.valueOf(T::class.java, getString(idx))
+
+inline fun <reified T : kotlin.Enum<T>> ResultSet.getOptEnum(name: String): T?
+ = getString(name)?.run { java.lang.Enum.valueOf(T::class.java, this) }
+
fun ResultSet.getAmount(name: String, currency: String): TalerAmount {
return TalerAmount(
getLong("${name}_val"),
diff --git a/common/src/main/kotlin/test/bench.kt
b/common/src/main/kotlin/test/bench.kt
new file mode 100644
index 00000000..bbc4714e
--- /dev/null
+++ b/common/src/main/kotlin/test/bench.kt
@@ -0,0 +1,100 @@
+/*
+ * 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.test
+
+import tech.libeufin.common.*
+import org.postgresql.jdbc.PgConnection
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+import kotlin.math.max
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.time.DurationUnit
+import kotlin.time.measureTime
+import kotlin.time.toDuration
+
+fun PgConnection.genData(amount: Int, generators: Sequence<Pair<String, (Int)
-> String>>) {
+ val copyManager = this.getCopyAPI()
+
+ for ((table, generator) in generators) {
+ println("Gen rows for $table")
+ val full = buildString(150*amount) {
+ repeat(amount) {
+ append(generator(it+1))
+ }
+ }
+ copyManager.copyIn("COPY $table FROM STDIN", full.reader())
+ }
+
+ // Update database statistics for better perf
+ this.execSQLUpdate("VACUUM ANALYZE");
+}
+
+class Benchmark(private val iter: Int) {
+ private val WARN = 4.toDuration(DurationUnit.MILLISECONDS)
+ private val ERR = 50.toDuration(DurationUnit.MILLISECONDS)
+ internal val measures: MutableList<List<String>> = mutableListOf()
+
+ internal fun fmtMeasures(times: LongArray): List<String> {
+ val min: Long = times.min()
+ val max: Long = times.max()
+ val mean: Long = times.average().toLong()
+ val variance = times.map { (it.toDouble() - mean).pow(2) }.average()
+ val stdVar: Long = sqrt(variance.toDouble()).toLong()
+ return sequenceOf(min, mean, max, stdVar).map {
+ val duration = it.toDuration(DurationUnit.MICROSECONDS)
+ val str = duration.toString()
+ if (duration > ERR) {
+ ANSI.red(str)
+ } else if (duration > WARN) {
+ ANSI.yellow(str)
+ } else {
+ ANSI.green(str)
+ }
+
+ }.toList()
+ }
+
+ suspend fun <R> measureAction(name: String, lambda: suspend (Int) -> R):
List<R> {
+ val results = mutableListOf<R>()
+ val times = LongArray(iter) { idx ->
+ measureTime {
+ val result = lambda(idx)
+ results.add(result)
+ }.inWholeMicroseconds
+ }
+ measures.add(listOf(ANSI.magenta(name)) + fmtMeasures(times))
+ return results
+ }
+}
+
+fun bench(iter: Int, lambda: Benchmark.() -> Unit) {
+ val bench = Benchmark(iter)
+ lambda(bench)
+ val cols = IntArray(5) { 0 }
+ printTable(
+ listOf("benchmark", "min", "mean", "max", "std").map { ANSI.bold(it) },
+ bench.measures,
+ ' ',
+ listOf(ColumnStyle.DEFAULT) + List(5) { ColumnStyle(false) }
+ )
+}
\ No newline at end of file
diff --git a/common/src/test/kotlin/TxMedataTest.kt
b/common/src/test/kotlin/TxMedataTest.kt
index dd57aa8b..f6d61eb7 100644
--- a/common/src/test/kotlin/TxMedataTest.kt
+++ b/common/src/test/kotlin/TxMedataTest.kt
@@ -17,13 +17,12 @@
* <http://www.gnu.org/licenses/>
*/
-import tech.libeufin.common.EddsaPublicKey
-import tech.libeufin.common.parseIncomingTxMetadata
+import tech.libeufin.common.*
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFails
-class TxMetadataTest{
+class TxMetadataTest {
fun assertFailsMsg(msg: String, lambda: () -> Unit) {
val failure = assertFails(lambda)
assertEquals(msg, failure.message)
@@ -39,7 +38,7 @@ class TxMetadataTest{
val mixedR = "y0vphyv0cyde6xbb0ympfxceg0"
val otherUpper = "TEST6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
val otherMixed = "test6rRSrvb3b0e2rdmyw0yra3y0vphyv0cyde6xbb0ympfxceg0"
- val key = EddsaPublicKey(upper)
+ val key = TalerIncomingMetadata(TalerIncomingType.reserve,
EddsaPublicKey(upper))
// Check succeed if upper or mixed
for (case in sequenceOf(upper, mixed)) {
diff --git a/database-versioning/libeufin-bank-0007.sql
b/database-versioning/libeufin-bank-0007.sql
new file mode 100644
index 00000000..2689db2f
--- /dev/null
+++ b/database-versioning/libeufin-bank-0007.sql
@@ -0,0 +1,44 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2024 Taler Systems SA
+--
+-- TALER is free software; you can redistribute it and/or modify it under the
+-- terms of the GNU General Public License as published by the Free Software
+-- Foundation; either version 3, or (at your option) any later version.
+--
+-- TALER 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 General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License along with
+-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+BEGIN;
+
+SELECT _v.register_patch('libeufin-bank-0007', NULL, NULL);
+SET search_path TO libeufin_bank;
+
+-- Make customer not null
+-- Fill missing name with an empty string. All accounts created using the API
already
+-- have a non-null name, so this only applies to accounts created manually
with SQL.
+UPDATE customers SET name='' WHERE name is NULL;
+ALTER TABLE customers ALTER COLUMN name SET NOT NULL;
+
+-- Support all taler incoming transaction types
+CREATE TYPE taler_incoming_type AS ENUM
+ ('reserve' ,'kyc', 'wad');
+ALTER TABLE taler_exchange_incoming
+ ADD type taler_incoming_type NOT NULL DEFAULT 'reserve',
+ ADD account_pub BYTEA CHECK (LENGTH(account_pub)=32),
+ ADD origin_exchange_url TEXT,
+ ADD wad_id BYTEA CHECK (LENGTH(wad_id)=24),
+ ALTER COLUMN reserve_pub DROP NOT NULL,
+ ADD CONSTRAINT incoming_polymorphism CHECK(
+ CASE type
+ WHEN 'reserve' THEN reserve_pub IS NOT NULL AND account_pub IS NULL AND
origin_exchange_url IS NULL AND wad_id IS NULL
+ WHEN 'kyc' THEN reserve_pub IS NULL AND account_pub IS NOT NULL AND
origin_exchange_url IS NULL AND wad_id IS NULL
+ WHEN 'wad' THEN reserve_pub IS NULL AND account_pub IS NULL AND
origin_exchange_url IS NOT NULL AND wad_id IS NOT NULL
+ END
+ );
+ALTER TABLE taler_exchange_incoming ALTER COLUMN type DROP DEFAULT;
+COMMIT;
diff --git a/database-versioning/libeufin-bank-drop.sql
b/database-versioning/libeufin-bank-drop.sql
index 2b120b24..84d46d99 100644
--- a/database-versioning/libeufin-bank-drop.sql
+++ b/database-versioning/libeufin-bank-drop.sql
@@ -5,7 +5,7 @@ $do$
DECLARE
patch text;
BEGIN
- IF EXISTS(SELECT 1 FROM information_schema.schemata WHERE
schema_name='_v') THEN
+ IF EXISTS(SELECT FROM information_schema.schemata WHERE schema_name='_v')
THEN
FOR patch IN SELECT patch_name FROM _v.patches WHERE patch_name LIKE
'libeufin_bank_%' LOOP
PERFORM _v.unregister_patch(patch);
END LOOP;
diff --git a/database-versioning/libeufin-bank-procedures.sql
b/database-versioning/libeufin-bank-procedures.sql
index c444c0e6..4b8b52c8 100644
--- a/database-versioning/libeufin-bank-procedures.sql
+++ b/database-versioning/libeufin-bank-procedures.sql
@@ -570,8 +570,10 @@ COMMENT ON PROCEDURE register_outgoing
IS 'Register a bank transaction as a taler outgoing transaction and announce
it';
CREATE PROCEDURE register_incoming(
+ IN in_tx_row_id INT8,
+ IN in_type taler_incoming_type,
IN in_reserve_pub BYTEA,
- IN in_tx_row_id INT8
+ IN in_account_pub BYTEA
)
LANGUAGE plpgsql AS $$
DECLARE
@@ -582,17 +584,24 @@ BEGIN
INSERT
INTO taler_exchange_incoming (
reserve_pub,
- bank_transaction
+ account_pub,
+ bank_transaction,
+ type
) VALUES (
in_reserve_pub,
- in_tx_row_id
+ in_account_pub,
+ in_tx_row_id,
+ in_type
);
--- update stats
+-- Get bank transaction info
SELECT (amount).val, (amount).frac, bank_account_id
INTO local_amount.val, local_amount.frac, local_bank_account_id
FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id;
-CALL stats_register_payment('taler_in', NULL, local_amount, null);
--- notify new transaction
+-- Update stats
+IF in_type = 'reserve' THEN
+ CALL stats_register_payment('taler_in', NULL, local_amount, null);
+END IF;
+-- Notify new incoming transaction
PERFORM pg_notify('incoming_tx', local_bank_account_id || ' ' || in_tx_row_id);
END $$;
COMMENT ON PROCEDURE register_incoming
@@ -688,12 +697,13 @@ END $$;
COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction
and register it';
CREATE FUNCTION taler_add_incoming(
- IN in_reserve_pub BYTEA,
+ IN in_key BYTEA,
IN in_subject TEXT,
IN in_amount taler_amount,
IN in_debit_account_payto TEXT,
IN in_username TEXT,
IN in_timestamp INT8,
+ IN in_type taler_incoming_type,
-- Error status
OUT out_creditor_not_found BOOLEAN,
OUT out_creditor_not_exchange BOOLEAN,
@@ -710,12 +720,13 @@ exchange_bank_account_id INT8;
sender_bank_account_id INT8;
BEGIN
-- Check conflict
-SELECT true FROM taler_exchange_incoming WHERE reserve_pub = in_reserve_pub
-UNION ALL
-SELECT true FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub
- INTO out_reserve_pub_reuse;
-IF out_reserve_pub_reuse THEN
- RETURN;
+IF in_type = 'reserve'::taler_incoming_type THEN
+ SELECT EXISTS(SELECT FROM taler_exchange_incoming WHERE reserve_pub =
in_key) OR
+ EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_key)
+ INTO out_reserve_pub_reuse;
+ IF out_reserve_pub_reuse THEN
+ RETURN;
+ END IF;
END IF;
-- Find exchange bank account id
SELECT
@@ -760,7 +771,11 @@ IF out_debitor_balance_insufficient THEN
RETURN;
END IF;
-- Register incoming transaction
-CALL register_incoming(in_reserve_pub, out_tx_row_id);
+CASE in_type
+ WHEN 'reserve' THEN CALL register_incoming(out_tx_row_id, 'reserve', in_key,
NULL);
+ WHEN 'kyc' THEN CALL register_incoming(out_tx_row_id, 'kyc', NULL, in_key);
+ ELSE RAISE EXCEPTION 'Unsupported incoming type %', in_type;
+END CASE;
END $$;
COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler
transaction and register it';
@@ -968,9 +983,8 @@ END IF;
IF not_selected THEN
-- Check reserve_pub reuse
- SELECT true FROM taler_exchange_incoming WHERE reserve_pub = in_reserve_pub
- UNION ALL
- SELECT true FROM taler_withdrawal_operations WHERE reserve_pub =
in_reserve_pub
+ SELECT EXISTS(SELECT FROM taler_exchange_incoming WHERE reserve_pub =
in_reserve_pub) OR
+ EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub =
in_reserve_pub)
INTO out_reserve_pub_reuse;
IF out_reserve_pub_reuse THEN
RETURN;
@@ -1126,7 +1140,7 @@ UPDATE taler_withdrawal_operations
WHERE withdrawal_uuid=in_withdrawal_uuid;
-- Register incoming transaction
-CALL register_incoming(reserve_pub_local, tx_row_id);
+CALL register_incoming(tx_row_id, 'reserve'::taler_incoming_type,
reserve_pub_local, NULL);
-- Notify status change
PERFORM pg_notify('withdrawal_status', in_withdrawal_uuid::text || '
confirmed');
@@ -1204,7 +1218,7 @@ IF out_balance_insufficient THEN
END IF;
-- Register incoming transaction
-CALL register_incoming(in_reserve_pub, tx_row_id);
+CALL register_incoming(tx_row_id, 'reserve'::taler_incoming_type,
in_reserve_pub, NULL);
-- update stats
CALL stats_register_payment('cashin', NULL, converted_amount, in_amount);
diff --git a/database-versioning/libeufin-conversion-setup.sql
b/database-versioning/libeufin-conversion-setup.sql
index 6507443c..925a0f8d 100644
--- a/database-versioning/libeufin-conversion-setup.sql
+++ b/database-versioning/libeufin-conversion-setup.sql
@@ -50,6 +50,11 @@ LANGUAGE plpgsql AS $$
no_account BOOLEAN;
no_config BOOLEAN;
BEGIN
+ -- Only reserve transaction triggers cashin
+ IF NEW.type != 'reserve' THEN
+ RETURN NEW;
+ END IF;
+
SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time
INTO local_amount.val, local_amount.frac, subject, now_date
FROM libeufin_nexus.incoming_transactions
diff --git a/database-versioning/libeufin-nexus-0001.sql
b/database-versioning/libeufin-nexus-0001.sql
index a6899843..d54a8bba 100644
--- a/database-versioning/libeufin-nexus-0001.sql
+++ b/database-versioning/libeufin-nexus-0001.sql
@@ -52,7 +52,6 @@ CREATE TABLE incoming_transactions
COMMENT ON COLUMN incoming_transactions.bank_id
IS 'ISO20022 AccountServicerReference';
--- only active in exchange mode. Note: duplicate keys are another reason to
bounce.
CREATE TABLE talerable_incoming_transactions
(incoming_transaction_id INT8 NOT NULL UNIQUE REFERENCES
incoming_transactions(incoming_transaction_id) ON DELETE CASCADE
,reserve_public_key BYTEA NOT NULL UNIQUE CHECK
(LENGTH(reserve_public_key)=32)
diff --git a/database-versioning/libeufin-nexus-0006.sql
b/database-versioning/libeufin-nexus-0006.sql
new file mode 100644
index 00000000..0eb245d7
--- /dev/null
+++ b/database-versioning/libeufin-nexus-0006.sql
@@ -0,0 +1,46 @@
+--
+-- This file is part of TALER
+-- Copyright (C) 2024 Taler Systems SA
+--
+-- TALER is free software; you can redistribute it and/or modify it under the
+-- terms of the GNU General Public License as published by the Free Software
+-- Foundation; either version 3, or (at your option) any later version.
+--
+-- TALER 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 General Public License for more details.
+--
+-- You should have received a copy of the GNU General Public License along with
+-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
+
+BEGIN;
+
+SELECT _v.register_patch('libeufin-nexus-0006', NULL, NULL);
+
+SET search_path TO libeufin_nexus;
+
+-- Support all taler incoming transaction types
+CREATE TYPE taler_incoming_type AS ENUM
+ ('reserve' ,'kyc', 'wad');
+ALTER TABLE talerable_incoming_transactions
+ ADD type taler_incoming_type NOT NULL DEFAULT 'reserve',
+ ADD account_pub BYTEA CHECK (LENGTH(account_pub)=32),
+ ADD origin_exchange_url TEXT,
+ ADD wad_id BYTEA CHECK (LENGTH(wad_id)=24),
+ ALTER COLUMN reserve_public_key DROP NOT NULL,
+ ADD CONSTRAINT incoming_polymorphism CHECK(
+ CASE type
+ WHEN 'reserve' THEN reserve_public_key IS NOT NULL AND account_pub IS
NULL AND origin_exchange_url IS NULL AND wad_id IS NULL
+ WHEN 'kyc' THEN reserve_public_key IS NULL AND account_pub IS NOT NULL
AND origin_exchange_url IS NULL AND wad_id IS NULL
+ WHEN 'wad' THEN reserve_public_key IS NULL AND account_pub IS NULL AND
origin_exchange_url IS NOT NULL AND wad_id IS NOT NULL
+ END
+ );
+ALTER TABLE talerable_incoming_transactions ALTER COLUMN type DROP DEFAULT;
+
+CREATE INDEX talerable_incoming_transactions_kyc_index ON
talerable_incoming_transactions (account_pub) WHERE account_pub IS NOT NULL;
+COMMENT ON INDEX talerable_incoming_transactions_kyc_index IS 'for reconciling
KYC transaction without bank_id';
+
+CREATE INDEX initiated_outgoing_transactions_status_index ON
initiated_outgoing_transactions (submission_state);
+COMMENT ON INDEX initiated_outgoing_transactions_status_index IS 'for listing
taler transfers by status';
+
+COMMIT;
diff --git a/database-versioning/libeufin-nexus-drop.sql
b/database-versioning/libeufin-nexus-drop.sql
index 4eed7f3c..6fae5750 100644
--- a/database-versioning/libeufin-nexus-drop.sql
+++ b/database-versioning/libeufin-nexus-drop.sql
@@ -5,7 +5,7 @@ $do$
DECLARE
patch text;
BEGIN
- IF EXISTS(SELECT 1 FROM information_schema.schemata WHERE
schema_name='_v') THEN
+ IF EXISTS(SELECT FROM information_schema.schemata WHERE schema_name='_v')
THEN
FOR patch IN SELECT patch_name FROM _v.patches WHERE patch_name LIKE
'libeufin_nexus_%' LOOP
PERFORM _v.unregister_patch(patch);
END LOOP;
diff --git a/database-versioning/libeufin-nexus-procedures.sql
b/database-versioning/libeufin-nexus-procedures.sql
index ce65edc3..0f667059 100644
--- a/database-versioning/libeufin-nexus-procedures.sql
+++ b/database-versioning/libeufin-nexus-procedures.sql
@@ -220,7 +220,9 @@ CREATE FUNCTION register_incoming_and_talerable(
,IN in_execution_time INT8
,IN in_debit_payto_uri TEXT
,IN in_bank_id TEXT
- ,IN in_reserve_public_key BYTEA
+ ,IN in_type taler_incoming_type
+ ,IN in_reserve_pub BYTEA
+ ,IN in_account_pub BYTEA
-- Error status
,OUT out_reserve_pub_reuse BOOLEAN
-- Success return
@@ -231,26 +233,53 @@ LANGUAGE plpgsql AS $$
DECLARE
need_reconcile BOOLEAN;
BEGIN
--- Check if exists
-SELECT incoming_transaction_id,
- bank_id IS DISTINCT FROM in_bank_id,
- bank_id IS NULL AND amount = in_amount
- AND debit_payto_uri = in_debit_payto_uri
- AND wire_transfer_subject = in_wire_transfer_subject
- INTO out_tx_id, out_reserve_pub_reuse, need_reconcile
- FROM talerable_incoming_transactions
- JOIN incoming_transactions USING(incoming_transaction_id)
- WHERE reserve_public_key = in_reserve_public_key;
+IF in_type = 'reserve' THEN
+ -- Search if already inserted based on unique reserve_pub key
+ -- Reconcile missing bank_id if metadata match
+ -- Check for reserve_pub reuse
+ SELECT incoming_transaction_id, bank_id IS DISTINCT FROM in_bank_id,
+ bank_id IS NULL AND amount = in_amount
+ AND debit_payto_uri = in_debit_payto_uri
+ AND wire_transfer_subject = in_wire_transfer_subject
+ INTO out_tx_id, out_reserve_pub_reuse, need_reconcile
+ FROM talerable_incoming_transactions
+ JOIN incoming_transactions USING(incoming_transaction_id)
+ WHERE reserve_public_key = in_reserve_pub;
-IF FOUND THEN
- IF need_reconcile THEN
- IF in_bank_id IS NOT NULL THEN
- -- Update the bank_id now that we have it
+ IF FOUND THEN
+ IF need_reconcile THEN
+ IF in_bank_id IS NOT NULL THEN
+ UPDATE incoming_transactions SET bank_id = in_bank_id WHERE
incoming_transaction_id = out_tx_id;
+ END IF;
+ out_reserve_pub_reuse=false;
+ END IF;
+ RETURN;
+ END IF;
+ELSIF in_type = 'kyc' THEN
+ -- Search if already inserted based on metadata match and account_pub
+ -- Reconcile missing bank_id
+ SELECT incoming_transaction_id, bank_id IS NULL
+ INTO out_tx_id, need_reconcile
+ FROM talerable_incoming_transactions
+ JOIN incoming_transactions USING(incoming_transaction_id)
+ WHERE account_pub = in_account_pub
+ AND amount = in_amount
+ AND debit_payto_uri = in_debit_payto_uri
+ AND wire_transfer_subject = in_wire_transfer_subject;
+
+ IF FOUND THEN
+ -- If bank_id is missing we assume it's the same transaction
+ IF in_bank_id IS NULL THEN
+ RETURN;
+ -- Else if bank_id is present we assume it's the same transaction and
reconciliate
+ ELSIF need_reconcile THEN
UPDATE incoming_transactions SET bank_id = in_bank_id WHERE
incoming_transaction_id = out_tx_id;
+ RETURN;
END IF;
- out_reserve_pub_reuse=false;
+ -- Else we consider it's a new transaction
END IF;
- RETURN;
+ELSE
+ RAISE EXCEPTION 'Unsupported incoming type %', in_type;
END IF;
-- Register the incoming transaction
@@ -259,14 +288,18 @@ SELECT reg.out_found, reg.out_tx_id
INTO out_found, out_tx_id;
-- Register as talerable
-IF NOT EXISTS(SELECT 1 FROM talerable_incoming_transactions WHERE
incoming_transaction_id = out_tx_id) THEN
+IF NOT EXISTS(SELECT FROM talerable_incoming_transactions WHERE
incoming_transaction_id = out_tx_id) THEN
-- We cannot use ON CONFLICT here because conversion use a trigger before
insertion that isn't idempotent
INSERT INTO talerable_incoming_transactions (
incoming_transaction_id
+ ,type
,reserve_public_key
+ ,account_pub
) VALUES (
out_tx_id
- ,in_reserve_public_key
+ ,in_type
+ ,in_reserve_pub
+ ,in_account_pub
);
PERFORM pg_notify('incoming_tx', out_tx_id::text);
END IF;
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
index de4760e7..69ad6df8 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/XmlCombinators.kt
@@ -182,7 +182,7 @@ class XmlDestructor internal constructor(private val el:
Element) {
fun bool(): Boolean = el.textContent.toBoolean()
fun date(): LocalDate = LocalDate.parse(text(), DateTimeFormatter.ISO_DATE)
fun dateTime(): LocalDateTime = LocalDateTime.parse(text(),
DateTimeFormatter.ISO_DATE_TIME)
- inline fun <reified T : Enum<T>> enum(): T =
java.lang.Enum.valueOf(T::class.java, text())
+ inline fun <reified T : kotlin.Enum<T>> enum(): T =
java.lang.Enum.valueOf(T::class.java, text())
fun attr(index: String): String = el.getAttribute(index)
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 7b867fb4..3533af89 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt
@@ -86,28 +86,32 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig)
= authApi(cfg.wireGat
get("/taler-wire-gateway/history/outgoing") {
historyEndpoint(::OutgoingHistory, ExchangeDAO::outgoingHistory)
}
- post("/taler-wire-gateway/admin/add-incoming") {
- val req = call.receive<AddIncomingRequest>()
- cfg.checkCurrency(req.amount)
- req.debit_account.expectRequestIban()
+ suspend fun ApplicationCall.addIncoming(
+ amount: TalerAmount,
+ debitAccount: Payto,
+ subject: String,
+ metadata: TalerIncomingMetadata
+ ) {
+ cfg.checkCurrency(amount)
+ debitAccount.expectRequestIban()
val timestamp = Instant.now()
val bankId = run {
val bytes = ByteArray(16).rand()
Base32Crockford.encode(bytes)
}
val res = db.payment.registerTalerableIncoming(IncomingPayment(
- amount = req.amount,
- debitPaytoUri = req.debit_account.toString(),
- wireTransferSubject = "Manual incoming ${req.reserve_pub}",
- executionTime = Instant.now(),
+ amount = amount,
+ debitPaytoUri = debitAccount.toString(),
+ wireTransferSubject = subject,
+ executionTime = timestamp,
bankId = bankId
- ), req.reserve_pub)
+ ), metadata)
when (res) {
IncomingRegistrationResult.ReservePubReuse -> throw conflict(
"reserve_pub used already",
TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT
)
- is IncomingRegistrationResult.Success -> call.respond(
+ is IncomingRegistrationResult.Success -> respond(
AddIncomingResponse(
timestamp = TalerProtocolTimestamp(timestamp),
row_id = res.id
@@ -115,4 +119,22 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig)
= authApi(cfg.wireGat
)
}
}
+ post("/taler-wire-gateway/admin/add-incoming") {
+ val req = call.receive<AddIncomingRequest>()
+ call.addIncoming(
+ amount = req.amount,
+ debitAccount = req.debit_account,
+ subject = "Manual incoming ${req.reserve_pub}",
+ metadata = TalerIncomingMetadata(TalerIncomingType.reserve,
req.reserve_pub)
+ )
+ }
+ post("/taler-wire-gateway/admin/add-kycauth") {
+ val req = call.receive<AddKycauthRequest>()
+ call.addIncoming(
+ amount = req.amount,
+ debitAccount = req.debit_account,
+ subject = "Manual incoming KYC:${req.account_pub}",
+ metadata = TalerIncomingMetadata(TalerIncomingType.kyc,
req.account_pub)
+ )
+ }
}
\ No newline at end of file
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 f5dbae3f..3e6d73ab 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/EbicsFetch.kt
@@ -95,8 +95,8 @@ suspend fun ingestIncomingPayment(
}
}
runCatching { parseIncomingTxMetadata(payment.wireTransferSubject) }.fold(
- onSuccess = { reservePub ->
- when (val res = db.payment.registerTalerableIncoming(payment,
reservePub)) {
+ onSuccess = { metadata ->
+ when (val res = db.payment.registerTalerableIncoming(payment,
metadata)) {
IncomingRegistrationResult.ReservePubReuse -> bounce("reverse
pub reuse")
is IncomingRegistrationResult.Success -> {
if (res.new) {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt
index 189a352b..9bddeedb 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/cli/Testing.kt
@@ -217,13 +217,13 @@ class ListCmd: CliktCommand("List nexus transactions",
name = "list") {
val txs = db.payment.metadataIncoming()
Pair(
listOf(
- "transaction", "id", "reserve_pub", "debtor",
"subject"
+ "transaction", "id", "talerable", "debtor",
"subject"
),
txs.map {
listOf(
"${it.date} ${it.amount}",
it.id.toString(),
- it.reservePub?.toString() ?: "",
+ it.talerable?.toString() ?: "",
fmtPayto(it.debtor),
it.subject
)
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 0e36c89d..f9572559 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt
@@ -20,10 +20,7 @@
package tech.libeufin.nexus.db
import tech.libeufin.common.*
-import tech.libeufin.common.db.getAmount
-import tech.libeufin.common.db.getTalerTimestamp
-import tech.libeufin.common.db.one
-import tech.libeufin.common.db.poolHistoryGlobal
+import tech.libeufin.common.db.*
import java.time.Instant
/** Data access logic for exchange specific logic */
@@ -31,7 +28,7 @@ class ExchangeDAO(private val db: Database) {
/** Query history of taler incoming transactions */
suspend fun incomingHistory(
params: HistoryParams
- ): List<IncomingReserveTransaction>
+ ): List<IncomingBankTransaction>
= db.poolHistoryGlobal(params, db::listenIncoming, """
SELECT
incoming_transaction_id
@@ -39,18 +36,31 @@ class ExchangeDAO(private val db: Database) {
,(amount).val AS amount_val
,(amount).frac AS amount_frac
,debit_payto_uri
+ ,type
,reserve_public_key
+ ,account_pub
FROM talerable_incoming_transactions
JOIN incoming_transactions USING(incoming_transaction_id)
WHERE
""", "incoming_transaction_id") {
- IncomingReserveTransaction(
- row_id = it.getLong("incoming_transaction_id"),
- date = it.getTalerTimestamp("execution_time"),
- amount = it.getAmount("amount", db.bankCurrency),
- debit_account = it.getString("debit_payto_uri"),
- reserve_pub =
EddsaPublicKey(it.getBytes("reserve_public_key")),
- )
+ val type = it.getEnum<TalerIncomingType>("type")
+ when (type) {
+ TalerIncomingType.reserve -> IncomingReserveTransaction(
+ row_id = it.getLong("incoming_transaction_id"),
+ date = it.getTalerTimestamp("execution_time"),
+ amount = it.getAmount("amount", db.bankCurrency),
+ debit_account = it.getString("debit_payto_uri"),
+ reserve_pub =
EddsaPublicKey(it.getBytes("reserve_public_key")),
+ )
+ TalerIncomingType.kyc -> IncomingKycAuthTransaction(
+ row_id = it.getLong("incoming_transaction_id"),
+ date = it.getTalerTimestamp("execution_time"),
+ amount = it.getAmount("amount", db.bankCurrency),
+ debit_account = it.getString("debit_payto_uri"),
+ account_pub = EddsaPublicKey(it.getBytes("account_pub")),
+ )
+ TalerIncomingType.wad -> throw UnsupportedOperationException()
+ }
}
/** Query exchange history of taler outgoing transactions */
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 cbd09b8c..03947584 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/PaymentDAO.kt
@@ -24,6 +24,7 @@ import tech.libeufin.common.db.*
import tech.libeufin.nexus.IncomingPayment
import tech.libeufin.nexus.OutgoingPayment
import java.time.Instant
+import java.sql.Types
/** Data access logic for incoming & outgoing payments */
class PaymentDAO(private val db: Database) {
@@ -110,11 +111,11 @@ class PaymentDAO(private val db: Database) {
/** Register an talerable incoming payment */
suspend fun registerTalerableIncoming(
paymentData: IncomingPayment,
- reservePub: EddsaPublicKey
+ metadata: TalerIncomingMetadata
): IncomingRegistrationResult = db.serializable(
"""
SELECT out_reserve_pub_reuse, out_found, out_tx_id
- FROM register_incoming_and_talerable((?,?)::taler_amount,?,?,?,?,?)
+ FROM
register_incoming_and_talerable((?,?)::taler_amount,?,?,?,?,?::taler_incoming_type,?,?)
"""
) {
val executionTime = paymentData.executionTime.micros()
@@ -124,7 +125,18 @@ class PaymentDAO(private val db: Database) {
setLong(4, executionTime)
setString(5, paymentData.debitPaytoUri)
setString(6, paymentData.bankId)
- setBytes(7, reservePub.raw)
+ setString(7, metadata.type.name)
+ when (metadata.type) {
+ TalerIncomingType.reserve -> {
+ setBytes(8, metadata.key.raw)
+ setNull(9, Types.BINARY)
+ }
+ TalerIncomingType.kyc -> {
+ setNull(8, Types.BINARY)
+ setBytes(9, metadata.key.raw)
+ }
+ TalerIncomingType.wad -> throw UnsupportedOperationException()
+ }
one {
when {
it.getBoolean("out_reserve_pub_reuse") ->
IncomingRegistrationResult.ReservePubReuse
@@ -193,20 +205,28 @@ class PaymentDAO(private val db: Database) {
,execution_time
,debit_payto_uri
,bank_id
+ ,type
,reserve_public_key
+ ,account_pub
FROM incoming_transactions
LEFT OUTER JOIN talerable_incoming_transactions using
(incoming_transaction_id)
ORDER BY execution_time
"""
) {
all {
+ val type = it.getOptEnum<TalerIncomingType>("type")
IncomingTxMetadata(
date = it.getLong("execution_time").asInstant(),
amount = it.getDecimal("amount"),
subject = it.getString("wire_transfer_subject"),
debtor = it.getString("debit_payto_uri"),
id = it.getString("bank_id"),
- reservePub = it.getBytes("reserve_public_key")?.run {
EddsaPublicKey(this) }
+ talerable = when (type) {
+ null -> null
+ TalerIncomingType.reserve -> "reserve
${EddsaPublicKey(it.getBytes("reserve_public_key"))}"
+ TalerIncomingType.kyc -> "kyc
${EddsaPublicKey(it.getBytes("account_pub"))}"
+ TalerIncomingType.wad -> throw
UnsupportedOperationException()
+ }
)
}
}
@@ -282,7 +302,7 @@ data class IncomingTxMetadata(
val subject: String,
val debtor: String,
val id: String?,
- val reservePub: EddsaPublicKey?
+ val talerable: String?
)
/** Outgoing transaction metadata for debugging */
diff --git a/nexus/src/test/kotlin/CliTest.kt b/nexus/src/test/kotlin/CliTest.kt
index c7e53e50..ad2bf758 100644
--- a/nexus/src/test/kotlin/CliTest.kt
+++ b/nexus/src/test/kotlin/CliTest.kt
@@ -152,9 +152,11 @@ class CliTest {
// Check with taler transactions
talerableOut(db)
talerableIn(db)
+ talerableKycIn(db)
check()
// Check with null id
talerableIn(db, true)
+ talerableKycIn(db, true)
check()
}
}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt
b/nexus/src/test/kotlin/DatabaseTest.kt
index 5cde567b..2bce4595 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -21,6 +21,7 @@ import org.junit.Test
import tech.libeufin.common.ShortHashCode
import tech.libeufin.common.TalerAmount
import tech.libeufin.common.db.one
+import tech.libeufin.common.db.withStatement
import tech.libeufin.nexus.AccountType
import tech.libeufin.nexus.cli.ingestIncomingPayment
import tech.libeufin.nexus.cli.ingestOutgoingPayment
@@ -72,19 +73,27 @@ class OutgoingPaymentsTest {
}
}
-suspend fun Database.checkCount(nbIncoming: Int, nbBounce: Int, nbTalerable:
Int) {
- conn {
- val cIncoming = it.prepareStatement("SELECT count(*) FROM
incoming_transactions").one { it.getInt(1) }
- val cBounce = it.prepareStatement("SELECT count(*) FROM
bounced_transactions").one { it.getInt(1) }
- val cTalerable = it.prepareStatement("SELECT count(*) FROM
talerable_incoming_transactions").one { it.getInt(1) }
- assertEquals(Triple(nbIncoming, nbBounce, nbTalerable),
Triple(cIncoming, cBounce, cTalerable))
+suspend fun Database.getCount(): Triple<Int, Int, Int> = serializable(
+ """
+ SELECT (SELECT count(*) FROM incoming_transactions) AS incoming,
+ (SELECT count(*) FROM bounced_transactions) AS bounce,
+ (SELECT count(*) FROM talerable_incoming_transactions) AS
talerable;
+ """
+) {
+ one {
+ Triple(it.getInt("incoming"), it.getInt("bounce"),
it.getInt("talerable"))
}
}
-suspend fun Database.inTxExists(id: String): Boolean = conn {
- it.prepareStatement("SELECT EXISTS(SELECT FROM incoming_transactions WHERE
bank_id = ?)").apply {
- setString(1, id)
- }.one {
+suspend fun Database.checkCount(nbIncoming: Int, nbBounce: Int, nbTalerable:
Int) {
+ assertEquals(Triple(nbIncoming, nbBounce, nbTalerable), getCount())
+}
+
+suspend fun Database.inTxExists(id: String): Boolean = serializable(
+ "SELECT EXISTS(SELECT FROM incoming_transactions WHERE bank_id = ?)"
+) {
+ setString(1, id)
+ one {
it.getBoolean(1)
}
}
@@ -144,12 +153,13 @@ class IncomingPaymentsTest {
}
}
- // Test creating an incoming taler transaction without and ID and
reconcile it later again
+ // Test creating an incoming reserve taler transaction without and ID and
reconcile it later again
@Test
fun reconcileMissingId() = setup { db, _ ->
+ val subject = "test with ${ShortHashCode.rand()} reserve pub"
+
// Register with missing ID
- val reserve_pub = ShortHashCode.rand()
- val incoming = genInPay("history test with $reserve_pub reserve pub")
+ val incoming = genInPay(subject)
val incomingMissingId = incoming.copy(bankId = null)
ingestIncomingPayment(db, incomingMissingId, AccountType.exchange)
db.checkCount(1, 0, 1)
@@ -160,12 +170,14 @@ class IncomingPaymentsTest {
db.checkCount(1, 0, 1)
// Different metadata is bounced
- ingestIncomingPayment(db, genInPay("another $reserve_pub reserve
pub"), AccountType.exchange)
- db.checkCount(2, 1, 1)
+ ingestIncomingPayment(db, genInPay(subject, "KUDOS:9"),
AccountType.exchange)
+ ingestIncomingPayment(db, genInPay("another $subject"),
AccountType.exchange)
+ db.checkCount(3, 2, 1)
// Different medata with missing id is ignored
ingestIncomingPayment(db, incomingMissingId.copy(amount =
TalerAmount("KUDOS:9")), AccountType.exchange)
- db.checkCount(2, 1, 1)
+ ingestIncomingPayment(db, incomingMissingId.copy(wireTransferSubject =
"another $subject"), AccountType.exchange)
+ db.checkCount(3, 2, 1)
// Recover bank ID when metadata match
ingestIncomingPayment(db, incoming, AccountType.exchange)
@@ -173,15 +185,58 @@ class IncomingPaymentsTest {
// Idempotent
ingestIncomingPayment(db, incoming, AccountType.exchange)
- db.checkCount(2, 1, 1)
+ db.checkCount(3, 2, 1)
// Missing ID is ignored
ingestIncomingPayment(db, incomingMissingId, AccountType.exchange)
- db.checkCount(2, 1, 1)
+ db.checkCount(3, 2, 1)
// Other ID is bounced known that we know the id
ingestIncomingPayment(db, incomingMissingId.copy(bankId = "NEW"),
AccountType.exchange)
- db.checkCount(3, 2, 1)
+ db.checkCount(4, 3, 1)
+ }
+
+ // Test creating an incoming kyc taler transaction without and ID and
reconcile it later again
+ @Test
+ fun reconcileMissingIdKyc() = setup { db, _ ->
+ val subject = "test with KYC:${ShortHashCode.rand()} account pub"
+
+ // Register with missing ID
+ val incoming = genInPay(subject)
+ val incomingMissingId = incoming.copy(bankId = null)
+ ingestIncomingPayment(db, incomingMissingId, AccountType.exchange)
+ db.checkCount(1, 0, 1)
+ assertFalse(db.inTxExists(incoming.bankId!!))
+
+ // Idempotent
+ ingestIncomingPayment(db, incomingMissingId, AccountType.exchange)
+ db.checkCount(1, 0, 1)
+
+ // Different metadata is accepted
+ ingestIncomingPayment(db, genInPay(subject, "KUDOS:9"),
AccountType.exchange)
+ ingestIncomingPayment(db, genInPay("another $subject"),
AccountType.exchange)
+ db.checkCount(3, 0, 3)
+
+ // Different medata with missing id are accepted
+ ingestIncomingPayment(db, incomingMissingId.copy(amount =
TalerAmount("KUDOS:9.5")), AccountType.exchange)
+ ingestIncomingPayment(db, incomingMissingId.copy(wireTransferSubject =
"again another $subject"), AccountType.exchange)
+ db.checkCount(5, 0, 5)
+
+ // Recover bank ID when metadata match
+ ingestIncomingPayment(db, incoming, AccountType.exchange)
+ assertTrue(db.inTxExists(incoming.bankId!!))
+
+ // Idempotent
+ ingestIncomingPayment(db, incoming, AccountType.exchange)
+ db.checkCount(5, 0, 5)
+
+ // Missing ID is ignored
+ ingestIncomingPayment(db, incomingMissingId, AccountType.exchange)
+ db.checkCount(5, 0, 5)
+
+ // Other ID is accepted
+ ingestIncomingPayment(db, incomingMissingId.copy(bankId = "NEW"),
AccountType.exchange)
+ db.checkCount(6, 0, 6)
}
}
class PaymentInitiationsTest {
diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt
b/nexus/src/test/kotlin/WireGatewayApiTest.kt
index 46dd02a1..e22269e2 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 Taler Systems S.A.
+ * Copyright (C) 2023-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
@@ -19,9 +19,11 @@
import io.ktor.client.request.*
import io.ktor.http.*
+import io.ktor.server.testing.*
import org.junit.Test
import tech.libeufin.common.*
import tech.libeufin.nexus.cli.ingestOutgoingPayment
+import kotlin.test.*
class WireGatewayApiTest {
// GET /taler-wire-gateway/config
@@ -114,29 +116,24 @@ class WireGatewayApiTest {
url = "/taler-wire-gateway/history/incoming",
ids = { it.incoming_transactions.map { it.row_id } },
registered = listOf(
- {
- client.postA("/taler-wire-gateway/admin/add-incoming") {
- json {
- "amount" to "CHF:12"
- "reserve_pub" to EddsaPublicKey.rand()
- "debit_account" to grothoffPayto
- }
- }.assertOk()
- },
- {
- // Transactions using raw bank transaction logic
- talerableIn(db)
- }
+ // Reserve transactions using clean add incoming logic
+ { addIncoming("CHF:12") },
+
+ // Reserve transactions using raw bank transaction logic
+ { talerableIn(db) },
+
+ // KYC transactions using clean add incoming logic
+ { addKyc("CHF:12") },
+
+ // KYC transactions using raw bank transaction logic
+ { talerableKycIn(db) },
),
ignored = listOf(
- {
- // Ignore malformed incoming transaction
- ingestIn(db)
- },
- {
- // Ignore outgoing transaction
- talerableOut(db)
- }
+ // Ignore malformed incoming transaction
+ { ingestIn(db) },
+
+ // Ignore outgoing transaction
+ { talerableOut(db) },
)
)
}
@@ -149,79 +146,115 @@ class WireGatewayApiTest {
url = "/taler-wire-gateway/history/outgoing",
ids = { it.outgoing_transactions.map { it.row_id } },
registered = listOf(
- {
- talerableOut(db)
- }
+ // Transfer using raw bank transaction logic
+ { talerableOut(db) },
),
ignored = listOf(
- {
- // Ignore pending transfers
- transfer()
- },
- {
- // Ignore manual incoming transaction
- talerableIn(db)
- },
- {
- // Ignore malformed incoming transaction
- ingestIn(db)
- },
- {
- // Ignore malformed outgoing transaction
- ingestOutgoingPayment(db, genOutPay("ignored"))
- }
+ // Ignore pending transfers
+ { transfer() },
+
+ // Ignore manual incoming transaction
+ { talerableIn(db) },
+
+ // Ignore malformed incoming transaction
+ { ingestIn(db) },
+
+ // Ignore malformed outgoing transaction
+ { ingestOutgoingPayment(db, genOutPay("ignored")) },
)
)
}
- // POST /taler-wire-gateway/admin/add-incoming
- @Test
- fun addIncoming() = serverSetup {
+ suspend fun ApplicationTestBuilder.talerAddIncomingRoutine(type:
TalerIncomingType) {
+ val (path, key) = when (type) {
+ TalerIncomingType.reserve -> Pair("add-incoming", "reserve_pub")
+ TalerIncomingType.kyc -> Pair("add-kycauth", "account_pub")
+ TalerIncomingType.wad -> throw UnsupportedOperationException()
+ }
val valid_req = obj {
"amount" to "CHF:44"
- "reserve_pub" to EddsaPublicKey.rand()
+ key to EddsaPublicKey.rand()
"debit_account" to grothoffPayto
}
- authRoutine(HttpMethod.Post, "/taler-wire-gateway/admin/add-incoming")
+ authRoutine(HttpMethod.Post, "/taler-wire-gateway/admin/$path")
// Check OK
- client.postA("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req)
}.assertOk()
- // Trigger conflict due to reused reserve_pub
- client.postA("/taler-wire-gateway/admin/add-incoming") {
- json(valid_req)
- }.assertConflict(TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT)
+ if (type == TalerIncomingType.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) {
+ // Non conflict on reuse
+ client.postA("/taler-wire-gateway/admin/$path") {
+ json(valid_req)
+ }.assertOk()
+ }
// Currency mismatch
- client.postA("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req) { "amount" to "EUR:33" }
}.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
// Bad BASE32 reserve_pub
- client.postA("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to "I love chocolate"
+ key to "I love chocolate"
}
}.assertBadRequest()
// Bad BASE32 len reserve_pub
- client.postA("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req) {
- "reserve_pub" to Base32Crockford.encode(ByteArray(31).rand())
+ key to Base32Crockford.encode(ByteArray(31).rand())
}
}.assertBadRequest()
// Bad payto kind
- client.postA("/taler-wire-gateway/admin/add-incoming") {
+ client.postA("/taler-wire-gateway/admin/$path") {
json(valid_req) {
"debit_account" to
"payto://x-taler-bank/bank.hostname.test/bar"
}
}.assertBadRequest()
}
+ // POST /taler-wire-gateway/admin/add-incoming
+ @Test
+ fun addIncoming() = serverSetup {
+ talerAddIncomingRoutine(TalerIncomingType.reserve)
+ }
+
+ // POST /taler-wire-gateway/admin/add-kycauth
+ @Test
+ fun addKycAuth() = serverSetup {
+ talerAddIncomingRoutine(TalerIncomingType.kyc)
+ }
+
+ @Test
+ fun addIncomingMix() = serverSetup { db ->
+ addIncoming("CHF:1")
+ addKyc("CHF:2")
+ talerableIn(db, amount = "CHF:3")
+ talerableKycIn(db, amount = "CHF:4")
+
client.getA("/taler-wire-gateway/history/incoming?delta=25").assertOkJson<IncomingHistory>
{
+ assertEquals(4, it.incoming_transactions.size)
+ println(it)
+ it.incoming_transactions.forEachIndexed { i, tx ->
+ assertEquals(TalerAmount("CHF:${i+1}"), tx.amount)
+ if (i % 2 == 1) {
+ assertIs<IncomingKycAuthTransaction>(tx)
+ } else {
+ assertIs<IncomingReserveTransaction>(tx)
+ }
+ }
+ }
+ }
+
@Test
fun noApi() = serverSetup("mini.conf") {
client.get("/taler-wire-gateway/config").assertNotImplemented()
diff --git a/nexus/src/test/kotlin/bench.kt b/nexus/src/test/kotlin/bench.kt
new file mode 100644
index 00000000..1a7874c5
--- /dev/null
+++ b/nexus/src/test/kotlin/bench.kt
@@ -0,0 +1,159 @@
+/*
+ * 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 io.ktor.client.request.*
+import io.ktor.http.*
+import org.junit.Test
+import org.postgresql.jdbc.PgConnection
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.cli.*
+import tech.libeufin.common.*
+import tech.libeufin.common.test.*
+import tech.libeufin.common.crypto.PwCrypto
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.ZoneId
+import java.util.*
+import kotlin.math.max
+import kotlin.math.pow
+import kotlin.math.sqrt
+import kotlin.time.DurationUnit
+import kotlin.time.measureTime
+import kotlin.time.toDuration
+
+class Bench {
+
+ /** Generate [amount] rows to fill the database */
+ fun genData(conn: PgConnection, amount: Int) {
+ val amount = max(amount, 10)
+ val token32 = ByteArray(32)
+ val token64 = ByteArray(64)
+
+ conn.genData(amount, sequenceOf(
+ "incoming_transactions(amount, wire_transfer_subject,
execution_time, debit_payto_uri, bank_id)" to {
+ "(42,0)\tsubject\t0\tdebit_payto\tBANK_ID${it*2}\n" +
+ "(42,0)\tsubject\t0\tdebit_payto\tBANK_ID${it*2+1}\n" +
+ "(42,0)\tsubject\t0\tdebit_payto\t\\N\n"
+ },
+ "outgoing_transactions(amount, wire_transfer_subject,
execution_time, credit_payto_uri, message_id)" to {
+ "(42,0)\tsubject\t0\tcredit_payto\tMSG_ID${it*2}\n" +
+ "(42,0)\tsubject\t0\tcredit_payto\tMSG_ID${it*2+1}\n"
+ },
+ "initiated_outgoing_transactions(amount, wire_transfer_subject,
initiation_time, credit_payto_uri, outgoing_transaction_id, request_uid,
order_id)" to {
+
"(42,0)\tsubject\t0\tcredit_payto\t${it*2}\tREQUEST_UID$it\tORDER_ID$it\n"
+ },
+ "talerable_incoming_transactions(type, reserve_public_key,
account_pub, incoming_transaction_id)" to {
+ val hex = token32.rand().encodeHex()
+ if (it % 2 == 0) {
+ "reserve\t\\\\x$hex\t\\N\t${it*2}\n"
+ } else {
+ "kyc\t\\N\t\\\\x$hex\t${it*2}\n"
+ }
+ },
+ "talerable_outgoing_transactions(wtid, exchange_base_url,
outgoing_transaction_id)" to {
+ val hex = token32.rand().encodeHex()
+ "\\\\x$hex\turl\t${it*2-1}\n"
+ },
+ "transfer_operations(initiated_outgoing_transaction_id,
request_uid, wtid, exchange_base_url)" to {
+ val hex32 = token32.rand().encodeHex()
+ val hex64 = token64.rand().encodeHex()
+ "$it\t\\\\x$hex64\t\\\\x$hex32\turl\n"
+ }
+ ))
+ }
+
+ @Test
+ fun benchDb() {
+ val ITER = System.getenv("BENCH_ITER")?.toIntOrNull() ?: 0
+ val AMOUNT = System.getenv("BENCH_AMOUNT")?.toIntOrNull() ?: 0
+
+ if (ITER == 0) {
+ println("Skip benchmark, missing BENCH_ITER")
+ return
+ }
+ println("Bench $ITER times with $AMOUNT rows")
+
+ bench(ITER) { serverSetup { db ->
+ // Generate data
+ db.conn { genData(it, AMOUNT) }
+
+ // Warm HTTP client
+ client.getA("/taler-revenue/config").assertOk()
+
+ // Ingest
+ measureAction("ingest_in") {
+ ingestIn(db)
+ }
+ measureAction("ingest_out") {
+ ingestOut(db)
+ }
+ measureAction("ingest_reserve") {
+ talerableIn(db)
+ }
+ measureAction("ingest_kyc") {
+ talerableKycIn(db)
+ }
+ measureAction("ingest_reserve_missing_id") {
+ val incoming = genInPay("test with ${ShortHashCode.rand()}
reserve pub")
+ ingestIncomingPayment(db, incoming.copy(bankId = null),
AccountType.exchange)
+ ingestIncomingPayment(db, incoming, AccountType.exchange)
+ }
+ measureAction("ingest_kyc_missing_id") {
+ val incoming = genInPay("test with KYC:${ShortHashCode.rand()}
account pub")
+ ingestIncomingPayment(db, incoming.copy(bankId = null),
AccountType.exchange)
+ ingestIncomingPayment(db, incoming, AccountType.exchange)
+ }
+
+ // Revenue API
+ measureAction("transaction_revenue") {
+ client.getA("/taler-revenue/history").assertOk()
+ }
+
+ // Wire gateway
+ measureAction("wg_transfer") {
+ client.postA("/taler-wire-gateway/transfer") {
+ json {
+ "request_uid" to HashCode.rand()
+ "amount" to "CHF:0.0001"
+ "exchange_base_url" to "http://exchange.example.com/"
+ "wtid" to ShortHashCode.rand()
+ "credit_account" to grothoffPayto
+ }
+ }.assertOk()
+ }
+ measureAction("wg_add") {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
+ json {
+ "amount" to "CHF:0.0001"
+ "reserve_pub" to EddsaPublicKey.rand()
+ "debit_account" to grothoffPayto
+ }
+ }.assertOk()
+ }
+ measureAction("wg_incoming") {
+ client.getA("/taler-wire-gateway/history/incoming")
+ .assertOk()
+ }
+ measureAction("wg_outgoing") {
+ client.getA("/taler-wire-gateway/history/outgoing")
+ .assertOk()
+ }
+ }}
+ }
+}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt
index a312f095..891d91b1 100644
--- a/nexus/src/test/kotlin/helpers.kt
+++ b/nexus/src/test/kotlin/helpers.kt
@@ -67,7 +67,7 @@ const val grothoffPayto =
"payto://iban/CH4189144589712575493?receiver-name=Grot
val clientKeys = generateNewKeys()
-// Gets an HTTP client whose requests are going to be served by 'handler'.
+/** Gets an HTTP client whose requests are going to be served by 'handler' */
fun getMockedClient(
handler: MockRequestHandleScope.(HttpRequestData) -> HttpResponseData
): HttpClient = HttpClient(MockEngine) {
@@ -79,7 +79,7 @@ fun getMockedClient(
}
}
-// Generates a payment initiation, given its subject.
+/** Generates a payment initiation, given its subject */
fun genInitPay(
subject: String = "init payment",
requestUid: String = "unique"
@@ -92,7 +92,7 @@ fun genInitPay(
requestUid = requestUid
)
-// Generates an incoming payment, given its subject.
+/** Generates an incoming payment, given its subject */
fun genInPay(subject: String, amount: String = "KUDOS:44"): IncomingPayment {
val bankId = run {
val bytes = ByteArray(16).rand()
@@ -107,7 +107,7 @@ fun genInPay(subject: String, amount: String = "KUDOS:44"):
IncomingPayment {
)
}
-// Generates an outgoing payment, given its subject and messageId
+/** Generates an outgoing payment, given its subject and messageId */
fun genOutPay(subject: String, messageId: String? = null): OutgoingPayment {
val id = messageId ?: run {
val bytes = ByteArray(16).rand()
@@ -135,16 +135,50 @@ suspend fun ApplicationTestBuilder.transfer() {
}.assertOk()
}
+/** Perform a taler incoming transaction of [amount] from merchant to exchange
*/
+suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
+ client.postA("/taler-wire-gateway/admin/add-incoming") {
+ json {
+ "amount" to TalerAmount(amount)
+ "reserve_pub" to EddsaPublicKey.rand()
+ "debit_account" to grothoffPayto
+ }
+ }.assertOk()
+}
+
+/** Perform a taler kyc transaction of [amount] from merchant to exchange */
+suspend fun ApplicationTestBuilder.addKyc(amount: String) {
+ client.postA("/taler-wire-gateway/admin/add-kycauth") {
+ json {
+ "amount" to TalerAmount(amount)
+ "account_pub" to EddsaPublicKey.rand()
+ "debit_account" to grothoffPayto
+ }
+ }.assertOk()
+}
+
/** Ingest a talerable outgoing transaction */
suspend fun talerableOut(db: Database) {
val wtid = ShortHashCode.rand()
ingestOutgoingPayment(db, genOutPay("$wtid http://exchange.example.com/"))
}
-/** Ingest a talerable incoming transaction */
-suspend fun talerableIn(db: Database, nullId: Boolean = false) {
+/** Ingest a talerable reserve incoming transaction */
+suspend fun talerableIn(db: Database, nullId: Boolean = false, amount: String
= "CHF:44") {
val reserve_pub = ShortHashCode.rand()
- ingestIncomingPayment(db, genInPay("history test with $reserve_pub reserve
pub").run {
+ ingestIncomingPayment(db, genInPay("test with $reserve_pub reserve pub",
amount).run {
+ if (nullId) {
+ copy(bankId = null)
+ } else {
+ this
+ }
+ }, AccountType.exchange)
+}
+
+/** Ingest a talerable KYC incoming transaction */
+suspend fun talerableKycIn(db: Database, nullId: Boolean = false, amount:
String = "CHF:44") {
+ val account_pub = ShortHashCode.rand()
+ ingestIncomingPayment(db, genInPay("test with KYC:$account_pub account
pub", amount).run {
if (nullId) {
copy(bankId = null)
} else {
diff --git a/nexus/src/test/kotlin/routines.kt
b/nexus/src/test/kotlin/routines.kt
index fc178e49..bf072185 100644
--- a/nexus/src/test/kotlin/routines.kt
+++ b/nexus/src/test/kotlin/routines.kt
@@ -47,12 +47,6 @@ suspend fun ApplicationTestBuilder.authRoutine(
this.method = method
headers[HttpHeaders.Authorization] = "Bearer bad-token"
}.assertUnauthorized()
-
- // GLS deployment
- // - testing did work ?
- // token - basic bearer
- // libeufin-nexus
- // - wire gateway try camt.052 files
}
diff --git a/testbench/src/test/kotlin/IntegrationTest.kt
b/testbench/src/test/kotlin/IntegrationTest.kt
index d1c67824..03c8c559 100644
--- a/testbench/src/test/kotlin/IntegrationTest.kt
+++ b/testbench/src/test/kotlin/IntegrationTest.kt
@@ -44,7 +44,7 @@ import tech.libeufin.nexus.withDb
import java.time.Instant
import kotlin.io.path.Path
import kotlin.io.path.readText
-import kotlin.test.assertEquals
+import kotlin.test.*
import tech.libeufin.nexus.db.Database as NexusDb
fun CliktCommand.run(cmd: String) {
@@ -132,12 +132,20 @@ class IntegrationTest {
bankCmd.run("dbinit $flags -r")
bankCmd.run("passwd admin password $flags")
- suspend fun checkCount(db: NexusDb, nbIncoming: Int, nbBounce: Int,
nbTalerable: Int) {
- db.conn { conn ->
- val cIncoming = conn.prepareStatement("SELECT count(*) FROM
incoming_transactions").one { it.getInt(1) }
- val cBounce = conn.prepareStatement("SELECT count(*) FROM
bounced_transactions").one { it.getInt(1) }
- val cTalerable = conn.prepareStatement("SELECT count(*) FROM
talerable_incoming_transactions").one { it.getInt(1) }
- assertEquals(Triple(nbIncoming, nbBounce, nbTalerable),
Triple(cIncoming, cBounce, cTalerable))
+ suspend fun NexusDb.checkCount(nbIncoming: Int, nbBounce: Int,
nbTalerable: Int) {
+ serializable(
+ """
+ SELECT (SELECT count(*) FROM incoming_transactions) AS
incoming,
+ (SELECT count(*) FROM bounced_transactions) AS bounce,
+ (SELECT count(*) FROM talerable_incoming_transactions)
AS talerable;
+ """
+ ) {
+ one {
+ assertEquals(
+ Triple(nbIncoming, nbBounce, nbTalerable),
+ Triple(it.getInt("incoming"), it.getInt("bounce"),
it.getInt("talerable"))
+ )
+ }
}
}
@@ -153,23 +161,33 @@ class IntegrationTest {
}
val reservePub = EddsaPublicKey.rand()
- val payment = IncomingPayment(
+ val reservePayment = IncomingPayment(
amount = TalerAmount("EUR:10"),
debitPaytoUri = userPayTo.toString(),
wireTransferSubject = "Error test $reservePub",
executionTime = Instant.now(),
- bankId = "error"
+ bankId = "reserve_error"
)
assertException("ERROR: cashin failed: missing exchange account") {
- ingestIncomingPayment(db, payment, AccountType.exchange)
+ ingestIncomingPayment(db, reservePayment, AccountType.exchange)
}
+ // But KYC works
+ ingestIncomingPayment(
+ db,
+ reservePayment.copy(
+ bankId = "kyc",
+ wireTransferSubject = "Error test
KYC:${EddsaPublicKey.rand()}"
+ ),
+ AccountType.exchange
+ )
+
// Create exchange account
bankCmd.run("create-account $flags -u exchange -p password --name
'Mr Money' --exchange")
assertException("ERROR: cashin currency conversion failed: missing
conversion rates") {
- ingestIncomingPayment(db, payment, AccountType.exchange)
+ ingestIncomingPayment(db, reservePayment, AccountType.exchange)
}
// Start server
@@ -195,40 +213,39 @@ class IntegrationTest {
}.assertNoContent()
assertException("ERROR: cashin failed: admin balance
insufficient") {
- db.payment.registerTalerableIncoming(payment, reservePub)
+ db.payment.registerTalerableIncoming(reservePayment,
TalerIncomingMetadata(TalerIncomingType.reserve, reservePub))
}
// Allow admin debt
bankCmd.run("edit-account admin --debit_threshold KUDOS:100
$flags")
// Too small amount
- checkCount(db, 0, 0, 0)
- ingestIncomingPayment(db, payment.copy(
+ db.checkCount(1, 0, 1)
+ ingestIncomingPayment(db, reservePayment.copy(
amount = TalerAmount("EUR:0.01"),
), AccountType.exchange)
- checkCount(db, 1, 1, 0)
+ db.checkCount(2, 1, 1)
client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
basicAuth("exchange", "password")
}.assertNoContent()
// Check success
- val valid_payment = IncomingPayment(
- amount = TalerAmount("EUR:10"),
- debitPaytoUri = userPayTo.toString(),
- wireTransferSubject = "Success
${Base32Crockford32B.rand().encoded()}",
- executionTime = Instant.now(),
+ val validPayment = reservePayment.copy(
+ wireTransferSubject = "Success $reservePub",
bankId = "success"
)
- ingestIncomingPayment(db, valid_payment, AccountType.exchange)
- checkCount(db, 2, 1, 1)
+ ingestIncomingPayment(db, validPayment, AccountType.exchange)
+ db.checkCount(3, 1, 2)
client.get("http://0.0.0.0:8080/accounts/exchange/transactions") {
basicAuth("exchange", "password")
}.assertOkJson<BankAccountTransactionsResponse>()
// Check idempotency
- ingestIncomingPayment(db, valid_payment, AccountType.exchange)
- checkCount(db, 2, 1, 1)
- // TODO check double insert cashin with different subject
+ ingestIncomingPayment(db, validPayment, AccountType.exchange)
+ ingestIncomingPayment(db, validPayment.copy(
+ wireTransferSubject="Success 2 $reservePub"
+ ), AccountType.exchange)
+ db.checkCount(3, 1, 2)
}
}
@@ -304,6 +321,7 @@ class IntegrationTest {
}.assertOkJson<IncomingHistory> {
val tx = it.incoming_transactions.first()
assertEquals(converted, tx.amount)
+ assertIs<IncomingReserveTransaction>(tx)
assertEquals(reservePub, tx.reserve_pub)
}
}
diff --git a/testbench/src/test/kotlin/MigrationTest.kt
b/testbench/src/test/kotlin/MigrationTest.kt
index ac56cf20..f8e3f586 100644
--- a/testbench/src/test/kotlin/MigrationTest.kt
+++ b/testbench/src/test/kotlin/MigrationTest.kt
@@ -59,6 +59,18 @@ class MigrationTest {
// libeufin-bank-0003
conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0003.sql").readText())
+ // libeufin-bank-0004
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0004.sql").readText())
+
+ // libeufin-bank-0005
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0005.sql").readText())
+
+ // libeufin-bank-0006
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0006.sql").readText())
+
+ // libeufin-bank-0007
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-bank-0007.sql").readText())
+
// libeufin-nexus-0001
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0001.sql").readText())
conn.execSQLUpdate("""
@@ -70,5 +82,17 @@ class MigrationTest {
// libeufin-nexus-0002
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0002.sql").readText())
+
+ // libeufin-nexus-0003
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0003.sql").readText())
+
+ // libeufin-nexus-0004
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0004.sql").readText())
+
+ // libeufin-nexus-0005
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0005.sql").readText())
+
+ // libeufin-nexus-0006
+
conn.execSQLUpdate(Path("../database-versioning/libeufin-nexus-0006.sql").readText())
}
}
\ No newline at end of file
--
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 (e396a3dd -> 096091fe),
gnunet <=