libeufin

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

commit b6811eb3c955ab4dedd90275c953c507d45a463e
parent 01524955d0cc9684f55d1262620f4e02dc0d8701
Author: Antoine A <>
Date:   Wed,  8 Apr 2026 18:01:40 +0200

common: fix prepared transfer API

Diffstat:
Mlibeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt | 2+-
Dlibeufin-bank/src/test/kotlin/PreparedApiTest.kt | 347-------------------------------------------------------------------------------
Alibeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt | 347+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlibeufin-bank/src/test/kotlin/bench.kt | 4++--
Mlibeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt | 2+-
Mlibeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt | 10+++++-----
Mlibeufin-nexus/src/test/kotlin/bench.kt | 4++--
7 files changed, 358 insertions(+), 358 deletions(-)

diff --git a/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt b/libeufin-bank/src/main/kotlin/tech/libeufin/bank/api/PreparedTransferApi.kt @@ -82,7 +82,7 @@ fun Routing.preparedTransferApi(db: Database, cfg: BankConfig) { } } } - delete("/accounts/{USERNAME}/taler-prepared-transfer/registration") { + post("/accounts/{USERNAME}/taler-prepared-transfer/unregistration") { val req = call.receive<Unregistration>(); val timestamp = Instant.parse(req.timestamp) diff --git a/libeufin-bank/src/test/kotlin/PreparedApiTest.kt b/libeufin-bank/src/test/kotlin/PreparedApiTest.kt @@ -1,346 +0,0 @@ -/* - * This file is part of LibEuFin. - * Copyright (C) 2026 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 org.junit.Test -import tech.libeufin.common.* -import tech.libeufin.common.crypto.CryptoUtil -import tech.libeufin.common.test.* -import java.time.Instant -import kotlin.test.* - -class WireTransferApiTest { - // GET /accounts/{USERNAME}/taler-prepared-transfer/config - @Test - fun config() = bankSetup { - client.get("/accounts/merchant/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() - } - - // POST /accounts/{USERNAME}/taler-prepared-transfer/registration - @Test - fun registration() = bankSetup { - val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - val amount = TalerAmount("KUDOS:1") - val valid_req = obj { - "credit_amount" to amount - "type" to "reserve" - "alg" to "EdDSA" - "account_pub" to pub - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) - "recurrent" to false - } - - val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) - - // Valid - val subjects = client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects[1], simpleSubject) - assertIs<TransferSubject.Uri>(it.subjects[0]) - }.subjects - - // Idempotent - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, subjects) - } - - // KYC has a different withdrawal uri - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "kyc" - } - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects[1], simpleSubject) - val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0]) - assertNotEquals(subjects[0], uriSubject) - } - - // Recurrent only has simple subject - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "recurrent" to true - } - }.assertOkJson<SubjectResult> { - assertEquals(it.subjects, listOf(simpleSubject)) - } - - // Bad signature - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "authorization_sig" to EddsaSignature.rand() - } - }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) - - // Not exchange - client.post("/accounts/merchant/taler-prepared-transfer/registration") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) - - // Unknown account - client.post("/accounts/unknown/taler-prepared-transfer/registration") { - json(valid_req) - }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) - - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - - // Non recurrent accept on then bounce - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "reserve" - } - }.assertOkJson<SubjectResult> { - val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') - client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - assertBalance("customer", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - } - - // Withdrawal is aborted on completion - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "kyc" - } - }.assertOkJson<SubjectResult> { - val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') - println("UUID $uuid") - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - client.postA("/accounts/customer/withdrawals/$uuid/confirm") - .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted - assertBalance("customer", "-KUDOS:2") - assertBalance("exchange", "+KUDOS:2") - } - - // Recurrent accept one and delay others - val newKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "account_pub" to newKey - "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) - "recurrent" to true - } - } - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - assertBalance("customer", "-KUDOS:7") - assertBalance("exchange", "+KUDOS:7") - - // Complete pending on recurrent update - val kycKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "kyc" - "account_pub" to kycKey - "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "reserve" - "account_pub" to kycKey - "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - assertBalance("customer", "-KUDOS:7") - assertBalance("exchange", "+KUDOS:7") - - // Kyc key reuse keep pending ones - tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") - assertBalance("customer", "-KUDOS:8") - assertBalance("exchange", "+KUDOS:8") - - // Switching to non recurrent cancel pending - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "kyc" - "account_pub" to kycKey - "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) - } - }.assertOkJson<SubjectResult>() - assertBalance("customer", "-KUDOS:6") - assertBalance("exchange", "+KUDOS:6") - - // Check authorization field in incoming history - val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() - val testKey = EddsaPublicKey.randEdsaKey() - val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv) - val qr = subjectFmtQrBill(testAuth) - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "account_pub" to testKey - "authorization_pub" to testAuth - "authorization_sig" to testSig - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "kyc" - "account_pub" to testKey - "authorization_pub" to testAuth - "authorization_sig" to testSig - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - val otherPub = EddsaPublicKey.randEdsaKey() - val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv) - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "type" to "reserve" - "account_pub" to otherPub - "authorization_pub" to testAuth - "authorization_sig" to otherSig - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - val lastPub = EddsaPublicKey.randEdsaKey() - tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") - tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") - val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") - .assertOkJson<IncomingHistory>().incoming_transactions.map { - when (it) { - is IncomingKycAuthTransaction -> Triple(it.account_pub, it.authorization_pub, it.authorization_sig) - is IncomingReserveTransaction -> Triple(it.reserve_pub, it.authorization_pub, it.authorization_sig) - else -> throw UnsupportedOperationException() - } - } - assertContentEquals(history, listOf( - Triple(lastPub, null, null), - Triple(lastPub, null, null), - Triple(otherPub, testAuth, otherSig), - Triple(testKey, testAuth, testSig), - Triple(testKey, testAuth, testSig) - )) - } - - // DELETE /accounts/{USERNAME}/taler-prepared-transfer/registration - @Test - fun unregistration() = bankSetup { - val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() - val valid_req = obj { - "credit_amount" to "KUDOS:1" - "type" to "reserve" - "alg" to "EdDSA" - "account_pub" to pub - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) - "recurrent" to false - } - - // Unknown - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Know - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) - }.assertOkJson<SubjectResult>() - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - }.assertNoContent() - - // Idempotent - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) - - // Bad signature - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign("lol".toByteArray(), priv) - } - }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) - - // Old timestamp - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().minusSeconds(1000000).toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) - - // Unknown bounce - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce - assertBalance("customer", "+KUDOS:0") - assertBalance("exchange", "+KUDOS:0") - - // Pending bounced after deletion - val newKey = EddsaPublicKey.randEdsaKey() - client.post("/accounts/exchange/taler-prepared-transfer/registration") { - json(valid_req) { - "account_pub" to newKey - "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) - "recurrent" to true - } - }.assertOkJson<SubjectResult>() - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending - assertBalance("customer", "-KUDOS:3") - assertBalance("exchange", "+KUDOS:3") - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { - val now = Instant.now().toString() - json { - "timestamp" to now - "authorization_pub" to pub - "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) - } - }.assertNoContent() - assertBalance("customer", "-KUDOS:1") - assertBalance("exchange", "+KUDOS:1") - } -} -\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt b/libeufin-bank/src/test/kotlin/PreparedTransferApiTest.kt @@ -0,0 +1,346 @@ +/* + * This file is part of LibEuFin. + * Copyright (C) 2026 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 org.junit.Test +import tech.libeufin.common.* +import tech.libeufin.common.crypto.CryptoUtil +import tech.libeufin.common.test.* +import java.time.Instant +import kotlin.test.* + +class PreparedTransferApiTest { + // GET /accounts/{USERNAME}/taler-prepared-transfer/config + @Test + fun config() = bankSetup { + client.get("/accounts/merchant/taler-prepared-transfer/config").assertOkJson<PreparedTransferConfig>() + } + + // POST /accounts/{USERNAME}/taler-prepared-transfer/registration + @Test + fun registration() = bankSetup { + val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() + val amount = TalerAmount("KUDOS:1") + val valid_req = obj { + "credit_amount" to amount + "type" to "reserve" + "alg" to "EdDSA" + "account_pub" to pub + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) + "recurrent" to false + } + + val simpleSubject = TransferSubject.Simple("Taler MAP:$pub", amount) + + // Valid + val subjects = client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects[1], simpleSubject) + assertIs<TransferSubject.Uri>(it.subjects[0]) + }.subjects + + // Idempotent + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects, subjects) + } + + // KYC has a different withdrawal uri + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "kyc" + } + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects[1], simpleSubject) + val uriSubject = assertIs<TransferSubject.Uri>(it.subjects[0]) + assertNotEquals(subjects[0], uriSubject) + } + + // Recurrent only has simple subject + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "recurrent" to true + } + }.assertOkJson<SubjectResult> { + assertEquals(it.subjects, listOf(simpleSubject)) + } + + // Bad signature + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "authorization_sig" to EddsaSignature.rand() + } + }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) + + // Not exchange + client.post("/accounts/merchant/taler-prepared-transfer/registration") { + json(valid_req) + }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE) + + // Unknown account + client.post("/accounts/unknown/taler-prepared-transfer/registration") { + json(valid_req) + }.assertNotFound(TalerErrorCode.BANK_UNKNOWN_ACCOUNT) + + assertBalance("customer", "+KUDOS:0") + assertBalance("exchange", "+KUDOS:0") + + // Non recurrent accept on then bounce + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "reserve" + } + }.assertOkJson<SubjectResult> { + val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') + client.postA("/accounts/customer/withdrawals/$uuid/confirm").assertNoContent() // reserve + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce + assertBalance("customer", "-KUDOS:1") + assertBalance("exchange", "+KUDOS:1") + } + + // Withdrawal is aborted on completion + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "kyc" + } + }.assertOkJson<SubjectResult> { + val uuid = (it.subjects[0] as? TransferSubject.Uri)!!.uri.substringAfterLast('/') + println("UUID $uuid") + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // kyc + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce + client.postA("/accounts/customer/withdrawals/$uuid/confirm") + .assertConflict(TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT) // aborted + assertBalance("customer", "-KUDOS:2") + assertBalance("exchange", "+KUDOS:2") + } + + // Recurrent accept one and delay others + val newKey = EddsaPublicKey.randEdsaKey() + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "account_pub" to newKey + "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) + "recurrent" to true + } + } + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + assertBalance("customer", "-KUDOS:7") + assertBalance("exchange", "+KUDOS:7") + + // Complete pending on recurrent update + val kycKey = EddsaPublicKey.randEdsaKey() + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "kyc" + "account_pub" to kycKey + "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "reserve" + "account_pub" to kycKey + "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + assertBalance("customer", "-KUDOS:7") + assertBalance("exchange", "+KUDOS:7") + + // Kyc key reuse keep pending ones + tx("customer", "KUDOS:1", "exchange", "Taler KYC:$kycKey") + assertBalance("customer", "-KUDOS:8") + assertBalance("exchange", "+KUDOS:8") + + // Switching to non recurrent cancel pending + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "kyc" + "account_pub" to kycKey + "authorization_sig" to CryptoUtil.eddsaSign(kycKey.raw, priv) + } + }.assertOkJson<SubjectResult>() + assertBalance("customer", "-KUDOS:6") + assertBalance("exchange", "+KUDOS:6") + + // Check authorization field in incoming history + val (testPriv, testAuth) = EddsaPublicKey.randEdsaKeyPair() + val testKey = EddsaPublicKey.randEdsaKey() + val testSig = CryptoUtil.eddsaSign(testKey.raw, testPriv) + val qr = subjectFmtQrBill(testAuth) + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "account_pub" to testKey + "authorization_pub" to testAuth + "authorization_sig" to testSig + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") + tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") + tx("customer", "KUDOS:0.1", "exchange", "Taler MAP:$testAuth") + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "kyc" + "account_pub" to testKey + "authorization_pub" to testAuth + "authorization_sig" to testSig + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + val otherPub = EddsaPublicKey.randEdsaKey() + val otherSig = CryptoUtil.eddsaSign(otherPub.raw, testPriv) + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "type" to "reserve" + "account_pub" to otherPub + "authorization_pub" to testAuth + "authorization_sig" to otherSig + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + val lastPub = EddsaPublicKey.randEdsaKey() + tx("customer", "KUDOS:0.1", "exchange", "Taler $lastPub") + tx("customer", "KUDOS:0.1", "exchange", "Taler KYC:$lastPub") + val history = client.getA("/accounts/exchange/taler-wire-gateway/history/incoming?limit=-5") + .assertOkJson<IncomingHistory>().incoming_transactions.map { + when (it) { + is IncomingKycAuthTransaction -> Triple(it.account_pub, it.authorization_pub, it.authorization_sig) + is IncomingReserveTransaction -> Triple(it.reserve_pub, it.authorization_pub, it.authorization_sig) + else -> throw UnsupportedOperationException() + } + } + assertContentEquals(history, listOf( + Triple(lastPub, null, null), + Triple(lastPub, null, null), + Triple(otherPub, testAuth, otherSig), + Triple(testKey, testAuth, testSig), + Triple(testKey, testAuth, testSig) + )) + } + + // DELETE /accounts/{USERNAME}/taler-prepared-transfer/registration + @Test + fun unregistration() = bankSetup { + val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() + val valid_req = obj { + "credit_amount" to "KUDOS:1" + "type" to "reserve" + "alg" to "EdDSA" + "account_pub" to pub + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(pub.raw, priv) + "recurrent" to false + } + + // Unknown + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) + } + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + + // Know + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) + }.assertOkJson<SubjectResult>() + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) + } + }.assertNoContent() + + // Idempotent + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) + } + }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) + + // Bad signature + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign("lol".toByteArray(), priv) + } + }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) + + // Old timestamp + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().minusSeconds(1000000).toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) + } + }.assertConflict(TalerErrorCode.BANK_OLD_TIMESTAMP) + + // Unknown bounce + assertBalance("customer", "+KUDOS:0") + assertBalance("exchange", "+KUDOS:0") + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // bounce + assertBalance("customer", "+KUDOS:0") + assertBalance("exchange", "+KUDOS:0") + + // Pending bounced after deletion + val newKey = EddsaPublicKey.randEdsaKey() + client.post("/accounts/exchange/taler-prepared-transfer/registration") { + json(valid_req) { + "account_pub" to newKey + "authorization_sig" to CryptoUtil.eddsaSign(newKey.raw, priv) + "recurrent" to true + } + }.assertOkJson<SubjectResult>() + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // reserve + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + tx("customer", "KUDOS:1", "exchange", "Taler MAP:$pub") // pending + assertBalance("customer", "-KUDOS:3") + assertBalance("exchange", "+KUDOS:3") + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { + val now = Instant.now().toString() + json { + "timestamp" to now + "authorization_pub" to pub + "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) + } + }.assertNoContent() + assertBalance("customer", "-KUDOS:1") + assertBalance("exchange", "+KUDOS:1") + } +} +\ No newline at end of file diff --git a/libeufin-bank/src/test/kotlin/bench.kt b/libeufin-bank/src/test/kotlin/bench.kt @@ -412,10 +412,10 @@ class Bench { "authorization_pub" to pub "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) } - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNoContent() - client.delete("/accounts/exchange/taler-prepared-transfer/registration") { + client.post("/accounts/exchange/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) } diff --git a/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt b/libeufin-nexus/src/main/kotlin/tech/libeufin/nexus/api/PreparedTransferApi.kt @@ -81,7 +81,7 @@ fun Routing.preparedTransferAPI(db: Database, cfg: NexusConfig) = conditional(cf } } } - delete("/taler-prepared-transfer/registration") { + post("/taler-prepared-transfer/unregistration") { val req = call.receive<Unregistration>(); val timestamp = Instant.parse(req.timestamp) diff --git a/libeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt b/libeufin-nexus/src/test/kotlin/PreparedTransferApiTest.kt @@ -137,7 +137,7 @@ class PreparedTransferApiTest { val (priv, pub) = EddsaPublicKey.randEdsaKeyPair() // Unknown - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -158,7 +158,7 @@ class PreparedTransferApiTest { "recurrent" to false } }.assertOkJson<SubjectResult>() - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -168,7 +168,7 @@ class PreparedTransferApiTest { }.assertNoContent() // Idempotent - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -178,7 +178,7 @@ class PreparedTransferApiTest { }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) // Bad signature - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().toString() json { "timestamp" to now @@ -188,7 +188,7 @@ class PreparedTransferApiTest { }.assertConflict(TalerErrorCode.BANK_BAD_SIGNATURE) // Old timestamp - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { val now = Instant.now().minusSeconds(1000000).toString() json { "timestamp" to now diff --git a/libeufin-nexus/src/test/kotlin/bench.kt b/libeufin-nexus/src/test/kotlin/bench.kt @@ -220,10 +220,10 @@ class Bench { "authorization_pub" to pub "authorization_sig" to CryptoUtil.eddsaSign(now.toByteArray(), priv) } - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNoContent() - client.delete("/taler-prepared-transfer/registration") { + client.post("/taler-prepared-transfer/unregistration") { json(valid_req) }.assertNotFound(TalerErrorCode.BANK_TRANSACTION_NOT_FOUND) }