summaryrefslogtreecommitdiff
path: root/nexus
diff options
context:
space:
mode:
authorAntoine A <>2024-04-15 16:28:24 +0900
committerAntoine A <>2024-04-26 11:47:47 +0900
commite1e8a3b2e28321a56ddc0206b7485e754e6f8f77 (patch)
tree5cdb1fc3e84270279632ffccd3d9753ed072dc41 /nexus
parentfbec8bebc30a4619b6b192a0fc466dc81f15da14 (diff)
downloadlibeufin-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.kt35
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/db/Database.kt1
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/db/ExchangeDAO.kt80
-rw-r--r--nexus/src/main/kotlin/tech/libeufin/nexus/db/InitiatedDAO.kt15
-rw-r--r--nexus/src/test/kotlin/DatabaseTest.kt22
-rw-r--r--nexus/src/test/kotlin/WireGatewayApiTest.kt58
-rw-r--r--nexus/src/test/kotlin/helpers.kt2
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)
}
}
}