commit b6811eb3c955ab4dedd90275c953c507d45a463e
parent 01524955d0cc9684f55d1262620f4e02dc0d8701
Author: Antoine A <>
Date: Wed, 8 Apr 2026 18:01:40 +0200
common: fix prepared transfer API
Diffstat:
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)
}