diff options
author | Antoine A <> | 2024-04-15 16:28:24 +0900 |
---|---|---|
committer | Antoine A <> | 2024-04-26 11:47:47 +0900 |
commit | e1e8a3b2e28321a56ddc0206b7485e754e6f8f77 (patch) | |
tree | 5cdb1fc3e84270279632ffccd3d9753ed072dc41 /nexus | |
parent | fbec8bebc30a4619b6b192a0fc466dc81f15da14 (diff) | |
download | libeufin-e1e8a3b2e28321a56ddc0206b7485e754e6f8f77.tar.gz libeufin-e1e8a3b2e28321a56ddc0206b7485e754e6f8f77.tar.bz2 libeufin-e1e8a3b2e28321a56ddc0206b7485e754e6f8f77.zip |
nexus: wire gateway /transfer
Diffstat (limited to 'nexus')
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt | 35 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt | 1 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt | 80 | ||||
-rw-r--r-- | nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt | 15 | ||||
-rw-r--r-- | nexus/src/test/kotlin/DatabaseTest.kt | 22 | ||||
-rw-r--r-- | nexus/src/test/kotlin/WireGatewayApiTest.kt | 58 | ||||
-rw-r--r-- | nexus/src/test/kotlin/helpers.kt | 2 |
7 files changed, 129 insertions, 84 deletions
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 f7374204..877101b2 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/api/WireGatewayApi.kt @@ -29,6 +29,8 @@ import tech.libeufin.common.* import tech.libeufin.nexus.* import tech.libeufin.nexus.db.* import tech.libeufin.nexus.db.PaymentDAO.* +import tech.libeufin.nexus.db.InitiatedDAO.* +import tech.libeufin.nexus.db.ExchangeDAO.* import java.time.Instant @@ -41,38 +43,28 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) { post("/taler-wire-gateway/transfer") { val req = call.receive<TransferRequest>() cfg.checkCurrency(req.amount) - // TODO - /*val res = db.exchange.transfer( - req = req, - login = username, - now = Instant.now() + val bankId = run { + val bytes = ByteArray(16) + kotlin.random.Random.nextBytes(bytes) + Base32Crockford.encode(bytes) + } + val res = db.exchange.transfer( + req, + bankId, + Instant.now() ) when (res) { - is TransferResult.UnknownExchange -> throw unknownAccount(username) - is TransferResult.NotAnExchange -> throw conflict( - "$username is not an exchange account.", - TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE - ) - is TransferResult.UnknownCreditor -> throw unknownCreditorAccount(req.credit_account.canonical) - is TransferResult.BothPartyAreExchange -> throw conflict( - "Wire transfer attempted with credit and debit party being both exchange account", - TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE - ) - is TransferResult.ReserveUidReuse -> throw conflict( + TransferResult.RequestUidReuse -> throw conflict( "request_uid used already", TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED ) - is TransferResult.BalanceInsufficient -> throw conflict( - "Insufficient balance for exchange", - TalerErrorCode.BANK_UNALLOWED_DEBIT - ) is TransferResult.Success -> call.respond( TransferResponse( timestamp = res.timestamp, row_id = res.id ) ) - }*/ + } } /*suspend fun <T> PipelineContext<Unit, ApplicationCall>.historyEndpoint( reduce: (List<T>, String) -> Any, @@ -122,7 +114,6 @@ fun Routing.wireGatewayApi(db: Database, cfg: NexusConfig) { "reserve_pub used already", TalerErrorCode.BANK_DUPLICATE_RESERVE_PUB_SUBJECT ) - // TODO timestamp when idempotent is IncomingRegistrationResult.Success -> call.respond( AddIncomingResponse( timestamp = TalerProtocolTimestamp(timestamp), diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt index b6422612..4cc70452 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt @@ -42,4 +42,5 @@ data class InitiatedPayment( class Database(dbConfig: DatabaseConfig): DbPool(dbConfig, "libeufin_nexus") { val payment = PaymentDAO(this) val initiated = InitiatedDAO(this) + val exchange = ExchangeDAO(this) }
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt new file mode 100644 index 00000000..d3844167 --- /dev/null +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt @@ -0,0 +1,80 @@ +/* + * 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.nexus.db + +import tech.libeufin.common.db.one +import tech.libeufin.common.db.getTalerTimestamp +import tech.libeufin.common.micros +import tech.libeufin.common.TalerProtocolTimestamp +import tech.libeufin.common.TransferRequest +import java.sql.ResultSet +import java.time.Instant + +/** Data access logic for exchange specific logic */ +class ExchangeDAO(private val db: Database) { + + /** Result of taler transfer transaction creation */ + sealed interface TransferResult { + /** Transaction [id] and wire transfer [timestamp] */ + data class Success(val id: Long, val timestamp: TalerProtocolTimestamp): TransferResult + data object RequestUidReuse: TransferResult + } + + /** Perform a Taler transfer */ + suspend fun transfer( + req: TransferRequest, + bankId: String, + now: Instant + ): TransferResult = db.serializable { conn -> + val subject = "${req.wtid} ${req.exchange_base_url.url}" + val stmt = conn.prepareStatement(""" + SELECT + out_request_uid_reuse + ,out_tx_row_id + ,out_timestamp + FROM + taler_transfer ( + ?, ?, ?, + (?,?)::taler_amount, + ?, ?, ?, ? + ); + """) + + stmt.setBytes(1, req.request_uid.raw) + stmt.setBytes(2, req.wtid.raw) + stmt.setString(3, subject) + stmt.setLong(4, req.amount.value) + stmt.setInt(5, req.amount.frac) + stmt.setString(6, req.exchange_base_url.url) + stmt.setString(7, req.credit_account.canonical) + stmt.setString(8, bankId) + stmt.setLong(9, now.micros()) + + stmt.one { + when { + it.getBoolean("out_request_uid_reuse") -> TransferResult.RequestUidReuse + else -> TransferResult.Success( + id = it.getLong("out_tx_row_id"), + timestamp = it.getTalerTimestamp("out_timestamp") + ) + } + } + } +}
\ No newline at end of file diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt index 04fd3965..052b75f9 100644 --- a/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt +++ b/nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt @@ -22,6 +22,7 @@ package tech.libeufin.nexus.db import tech.libeufin.common.asInstant import tech.libeufin.common.db.all import tech.libeufin.common.db.executeUpdateViolation +import tech.libeufin.common.db.oneUniqueViolation import tech.libeufin.common.db.getAmount import tech.libeufin.common.db.oneOrNull import tech.libeufin.common.micros @@ -32,9 +33,9 @@ import java.time.Instant class InitiatedDAO(private val db: Database) { /** Outgoing payments initiation result */ - enum class PaymentInitiationResult { - REQUEST_UID_REUSE, - SUCCESS + sealed interface PaymentInitiationResult { + data class Success(val id: Long): PaymentInitiationResult + data object RequestUidReuse: PaymentInitiationResult } /** Register a new pending payment in the database */ @@ -47,16 +48,18 @@ class InitiatedDAO(private val db: Database) { ,initiation_time ,request_uid ) VALUES ((?,?)::taler_amount,?,?,?,?) + RETURNING initiated_outgoing_transaction_id """) + // TODO check payto uri stmt.setLong(1, paymentData.amount.value) stmt.setInt(2, paymentData.amount.frac) stmt.setString(3, paymentData.wireTransferSubject) stmt.setString(4, paymentData.creditPaytoUri.toString()) stmt.setLong(5, paymentData.initiationTime.micros()) stmt.setString(6, paymentData.requestUid) - if (stmt.executeUpdateViolation()) - return@conn PaymentInitiationResult.SUCCESS - return@conn PaymentInitiationResult.REQUEST_UID_REUSE + stmt.oneUniqueViolation(PaymentInitiationResult.RequestUidReuse) { + PaymentInitiationResult.Success(it.getLong("initiated_outgoing_transaction_id")) + } } /** Register EBICS submission success */ diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt index 66bbe564..1c2e82ae 100644 --- a/nexus/src/test/kotlin/DatabaseTest.kt +++ b/nexus/src/test/kotlin/DatabaseTest.kt @@ -22,6 +22,7 @@ import tech.libeufin.common.TalerAmount import tech.libeufin.nexus.db.InitiatedDAO.PaymentInitiationResult import java.time.Instant import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.test.assertFalse import kotlin.test.assertNull import kotlin.test.assertTrue @@ -31,8 +32,7 @@ class OutgoingPaymentsTest { fun register() = setup { db, _ -> // With reconciling genOutPay("paid by nexus", "first").run { - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay("waiting for reconciliation", "first")) ) db.payment.registerOutgoing(this).run { @@ -117,8 +117,7 @@ class PaymentInitiationsTest { @Test fun status() = setup { db, _ -> - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY1")) ) db.initiated.submissionFailure(1, Instant.now(), "First failure") @@ -126,8 +125,7 @@ class PaymentInitiationsTest { db.initiated.submissionSuccess(1, Instant.now(), "ORDER1") assertEquals(Pair("PAY1", null), db.initiated.logFailure("ORDER1")) - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY2")) ) db.initiated.submissionFailure(2, Instant.now(), "First failure") @@ -135,8 +133,7 @@ class PaymentInitiationsTest { db.initiated.logMessage("ORDER2", "status msg") assertEquals(Pair("PAY2", "status msg"), db.initiated.logFailure("ORDER2")) - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY3")) ) db.initiated.submissionSuccess(3, Instant.now(), "ORDER3") @@ -146,15 +143,13 @@ class PaymentInitiationsTest { assertNull(db.initiated.logSuccess("ORDER_X")) assertNull(db.initiated.logFailure("ORDER_X")) - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY4")) ) db.initiated.bankMessage("PAY4", "status progress") db.initiated.bankFailure("PAY4", "status failure") - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY5")) ) db.initiated.bankMessage("PAY5", "status progress") @@ -164,8 +159,7 @@ class PaymentInitiationsTest { @Test fun submittable() = setup { db, _ -> for (i in 0..5) { - assertEquals( - PaymentInitiationResult.SUCCESS, + assertIs<PaymentInitiationResult.Success>( db.initiated.create(genInitPay(requestUid = "PAY$i")) ) } diff --git a/nexus/src/test/kotlin/WireGatewayApiTest.kt b/nexus/src/test/kotlin/WireGatewayApiTest.kt index a8d94b2f..8832ae15 100644 --- a/nexus/src/test/kotlin/WireGatewayApiTest.kt +++ b/nexus/src/test/kotlin/WireGatewayApiTest.kt @@ -35,36 +35,30 @@ class WireGatewayApiTest { } // Testing the POST /transfer call from the TWG API. - /*@Test - fun transfer() = bankSetup { _ -> + @Test + fun transfer() = serverSetup { _ -> val valid_req = obj { "request_uid" to HashCode.rand() - "amount" to "KUDOS:55" + "amount" to "CHF:55" "exchange_base_url" to "http://exchange.example.com/" "wtid" to ShortHashCode.rand() - "credit_account" to merchantPayto.canonical + "credit_account" to grothoffPayto } - authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/transfer", valid_req) - - // Checking exchange debt constraint. - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) - }.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT) + //authRoutine(HttpMethod.Post, "/accounts/merchant/taler-wire-gateway/transfer", valid_req) - // Giving debt allowance and checking the OK case. - setMaxDebt("exchange", "KUDOS:1000") - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + // Check OK + client.post("/taler-wire-gateway/transfer") { json(valid_req) }.assertOk() // check idempotency - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) }.assertOk() // Trigger conflict due to reused request_uid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { "wtid" to ShortHashCode.rand() "exchange_base_url" to "http://different-exchange.example.com/" @@ -72,58 +66,40 @@ class WireGatewayApiTest { }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) // Currency mismatch - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { "amount" to "EUR:33" } }.assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) - // Unknown account - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - "credit_account" to unknownPayto - } - }.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR) - - // Same account - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { - json(valid_req) { - "request_uid" to HashCode.rand() - "wtid" to ShortHashCode.rand() - "credit_account" to exchangePayto - } - }.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE) - // Bad BASE32 wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { "wtid" to "I love chocolate" } }.assertBadRequest() // Bad BASE32 len wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { - "wtid" to randBase32Crockford(31) + "wtid" to Base32Crockford.encode(ByteArray(31).rand()) } }.assertBadRequest() // Bad BASE32 request_uid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { "request_uid" to "I love chocolate" } }.assertBadRequest() // Bad BASE32 len wtid - client.postA("/accounts/exchange/taler-wire-gateway/transfer") { + client.post("/taler-wire-gateway/transfer") { json(valid_req) { - "request_uid" to randBase32Crockford(65) + "request_uid" to Base32Crockford.encode(ByteArray(65).rand()) } }.assertBadRequest() - }*/ + } /* /** * Testing the /history/incoming call from the TWG API. diff --git a/nexus/src/test/kotlin/helpers.kt b/nexus/src/test/kotlin/helpers.kt index e6c4b1a7..a25a43b4 100644 --- a/nexus/src/test/kotlin/helpers.kt +++ b/nexus/src/test/kotlin/helpers.kt @@ -79,7 +79,7 @@ fun getMockedClient( followRedirects = false engine { addHandler { - request -> handler(request) + request -> handler(request) } } } |