commit 30d6541149f8db5cc445506ecb18a026decfb255
parent 8e45d1be81105acd292502a02c03e001a60f6327
Author: Antoine A <>
Date: Thu, 5 Oct 2023 14:48:13 +0000
Add /taler-wire-gateway/history/outgoing
Diffstat:
3 files changed, 203 insertions(+), 4 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankMessages.kt
@@ -683,7 +683,29 @@ data class IncomingReserveTransaction(
val date: TalerProtocolTimestamp,
val amount: TalerAmount,
val debit_account: String, // Payto of the sender.
- val reserve_pub: String
+ val reserve_pub: EddsaPublicKey
+)
+
+/**
+ * Response of a TWG /history/outgoing call.
+ */
+@Serializable
+data class OutgoingHistory(
+ val outgoing_transactions: MutableList<OutgoingTransaction> = mutableListOf(),
+ val debit_account: String // Debitor's Payto URI.
+)
+
+/**
+ * TWG's outgoinf payment record.
+ */
+@Serializable
+data class OutgoingTransaction(
+ val row_id: Long, // DB row ID of the payment.
+ val date: TalerProtocolTimestamp,
+ val amount: TalerAmount,
+ val credit_account: String, // Payto of the receiver.
+ val wtid: ShortHashCode,
+ val exchange_base_url: String,
)
/**
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -141,12 +141,19 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
if (history.isEmpty())
break;
history.forEach {
- val reservePub = extractReservePubFromSubject(it.subject)
+ val reservePub = try {
+ EddsaPublicKey(it.subject)
+ } catch (e: Exception) {
+ logger.debug("Not containing a reserve pub: ${it.subject}")
+ null
+ }
if (reservePub == null) {
// This should usually not happen in the first place,
// because transactions to the exchange without a valid
// reserve pub should be bounced.
logger.warn("exchange account $username contains invalid incoming transaction ${it.expectRowId()}")
+ // Skip row
+ start = it.expectRowId()
} else {
// Register new transacation
resp.incoming_transactions.add(
@@ -172,6 +179,66 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
}
}
+ get("/accounts/{USERNAME}/taler-wire-gateway/history/outgoing") {
+ val username = call.authCheck(TokenScope.readonly, true)
+ val params = getHistoryParams(call.request)
+ val bankAccount = call.bankAccount()
+ if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.")
+
+ val resp = OutgoingHistory(debit_account = bankAccount.internalPaytoUri)
+ var start = params.start
+ var delta = params.delta
+
+ // As we may ignore rows containing incorrect subjects, we may have to run several queries.
+ while (delta != 0L) {
+ val history: List<BankAccountTransaction> = db.bankTransactionGetHistory(
+ start = start,
+ delta = delta,
+ bankAccountId = bankAccount.expectRowId(),
+ withDirection = TransactionDirection.debit
+ )
+ if (history.isEmpty())
+ break;
+ history.forEach {
+ val metadata = try {
+ val split = it.subject.split(" ")
+ Pair(ShortHashCode(split[0]), split[1])
+ } catch (e: Exception) {
+ logger.debug("Not containing metadata: ${it.subject}")
+ null
+ }
+ if (metadata == null) {
+ // This should usually not happen in the first place,
+ // because transactions from the exchange should be well formed
+ logger.warn("exchange account $username contains invalid outgoing transaction ${it.expectRowId()}")
+ // Skip row
+ start = it.expectRowId()
+ } else {
+ // Register new transacation
+ resp.outgoing_transactions.add(
+ OutgoingTransaction(
+ row_id = it.expectRowId(),
+ date = TalerProtocolTimestamp(it.transactionDate),
+ amount = it.amount,
+ credit_account = it.creditorPaytoUri,
+ wtid = metadata.first,
+ exchange_base_url = metadata.second
+ )
+ )
+ // Advance cursor
+ start = it.expectRowId()
+ if (delta < 0) delta++ else delta--;
+ }
+ }
+ }
+
+ if (resp.outgoing_transactions.isEmpty()) {
+ call.respond(HttpStatusCode.NoContent)
+ } else {
+ call.respond(resp)
+ }
+ }
+
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
call.authCheck(TokenScope.readwrite, false);
val req = call.receive<AddIncomingRequest>()
diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt
@@ -12,6 +12,7 @@ import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
import tech.libeufin.util.stripIbanPayto
import java.util.*
+import java.time.Instant
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import randHashCode
@@ -51,6 +52,20 @@ class TalerApiTest {
cashoutCurrency = "KUDOS"
)
+ suspend fun transfer(db: Database, from: Long, to: BankAccount) {
+ db.talerTransferCreate(
+ req = TransferRequest(
+ request_uid = randHashCode(),
+ amount = TalerAmount(10, 0, "Kudos"),
+ exchange_base_url = "http://exchange.example.com/",
+ wtid = randShortHashCode(),
+ credit_account ="${stripIbanPayto(to.internalPaytoUri)}"
+ ),
+ exchangeBankAccountId = from,
+ timestamp = Instant.now()
+ )
+ }
+
fun commonSetup(): Pair<Database, BankApplicationContext> {
val db = initDb()
val ctx = getTestContext()
@@ -206,7 +221,6 @@ class TalerApiTest {
)
)
- // Bar expects two entries in the incoming history
testApplication {
application {
corebankWebApp(db, ctx)
@@ -285,8 +299,104 @@ class TalerApiTest {
assert(history.incoming_transactions[0].row_id <= 300)
// testing that the row_id decreases.
assert(history.incoming_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id })
+ }
+ }
+ }
+
+
+ /**
+ * Testing the /history/outgoing call from the TWG API.
+ */
+ @Test
+ fun historyOutgoing() {
+ val (db, ctx) = commonSetup()
+ // Give Bar reasonable debt allowance:
+ assert(
+ db.bankAccountSetMaxDebt(
+ 2L,
+ TalerAmount(1000000, 0, "KUDOS")
+ )
+ )
+
+ testApplication {
+ application {
+ corebankWebApp(db, ctx)
}
-
+
+ authRoutine(client, "/accounts/foo/taler-wire-gateway/history/outgoing", HttpMethod.Get)
+
+ // Check error when no transactions
+ client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") {
+ basicAuth("bar", "secret")
+ }.assertStatus(HttpStatusCode.NoContent)
+
+ // Bar pays Foo three time
+ repeat(3) {
+ transfer(db, 2, bankAccountFoo)
+ }
+ // Should not show up in the taler wire gateway API history
+ db.bankTransactionCreate(genTx("bogus foobar", 1, 2)).assertSuccess()
+ // Foo pays Bar once, but that should not appear in the result.
+ db.bankTransactionCreate(genTx("payout")).assertSuccess()
+ // Bar pays Foo twice, we should see five valid transactions
+ repeat(2) {
+ transfer(db, 2, bankAccountFoo)
+ }
+
+ // Check ignore bogus subject
+ client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") {
+ basicAuth("bar", "secret")
+ }.assertOk().run {
+ val j: OutgoingHistory = Json.decodeFromString(this.bodyAsText())
+ assertEquals(5, j.outgoing_transactions.size)
+ }
+
+ // Check skip bogus subject
+ client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=5") {
+ basicAuth("bar", "secret")
+ }.assertOk().run {
+ val j: OutgoingHistory = Json.decodeFromString(this.bodyAsText())
+ assertEquals(5, j.outgoing_transactions.size)
+ }
+
+ // Testing ranges.
+ val mockReservePub = randShortHashCode().encoded
+ for (i in 1..400)
+ transfer(db, 2, bankAccountFoo)
+
+ // forward range:
+ client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=10&start=30") {
+ basicAuth("bar", "secret")
+ }.assertOk().run {
+ val txt = this.bodyAsText()
+ val history = Json.decodeFromString<OutgoingHistory>(txt)
+ // testing the size is like expected.
+ assert(history.outgoing_transactions.size == 10) {
+ println("outgoing_transactions has wrong size: ${history.outgoing_transactions.size}")
+ println("Response was: ${txt}")
+ }
+ // testing that the first row_id is at least the 'start' query param.
+ assert(history.outgoing_transactions[0].row_id >= 30)
+ // testing that the row_id increases.
+ assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id < b.row_id })
+ }
+
+ // backward range:
+ client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=-10&start=300") {
+ basicAuth("bar", "secret")
+ }.assertOk().run {
+ val txt = this.bodyAsText()
+ val history = Json.decodeFromString<OutgoingHistory>(txt)
+ // testing the size is like expected.
+ assert(history.outgoing_transactions.size == 10) {
+ println("outgoing_transactions has wrong size: ${history.outgoing_transactions.size}")
+ println("Response was: ${txt}")
+ }
+ // testing that the first row_id is at most the 'start' query param.
+ assert(history.outgoing_transactions[0].row_id <= 300)
+ // testing that the row_id decreases.
+ assert(history.outgoing_transactions.windowed(2).all { (a, b) -> a.row_id > b.row_id })
+ }
}
}