gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated: Implementing TWG POST /transfer.


From: gnunet
Subject: [libeufin] branch master updated: Implementing TWG POST /transfer.
Date: Thu, 21 Sep 2023 16:14:58 +0200

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository libeufin.

The following commit(s) were added to refs/heads/master by this push:
     new 1606b995 Implementing TWG POST /transfer.
1606b995 is described below

commit 1606b9952d7eeb67e70656e0c19c9ae3206c5024
Author: MS <ms@taler.net>
AuthorDate: Thu Sep 21 16:14:39 2023 +0200

    Implementing TWG POST /transfer.
---
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 95 +++++++++++++++++++++-
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    | 23 ++++++
 .../tech/libeufin/bank/talerWireGatewayHandlers.kt | 52 +++++++++++-
 bank/src/main/kotlin/tech/libeufin/bank/types.kt   | 22 +++++
 bank/src/test/kotlin/DatabaseTest.kt               | 30 ++++++-
 bank/src/test/kotlin/TalerApiTest.kt               | 57 +++++++++++++
 database-versioning/libeufin-bank-0001.sql         | 16 ++++
 database-versioning/procedures.sql                 | 85 +++++++++++++++++++
 8 files changed, 377 insertions(+), 3 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index 1ceb2763..dff0b951 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -394,7 +394,6 @@ class Database(private val dbConfig: String) {
             )
         }
     }
-    // More bankAccountGetFrom*() to come, on a needed basis.
 
     // BANK ACCOUNT TRANSACTIONS
     enum class BankTransactionResult {
@@ -900,4 +899,98 @@ class Database(private val dbConfig: String) {
             )
         }
     }
+
+    // Gets a Taler transfer request, given its UID.
+    fun talerTransferGetFromUid(requestUid: String): TransferRequest? {
+        reconnect()
+        val stmt = prepare("""
+            SELECT
+              wtid
+              ,(amount).val AS amount_value
+              ,(amount).frac AS amount_frac
+              ,exchange_base_url
+              ,credit_account_payto
+              FROM taler_exchange_transfers
+              WHERE request_uid = ?;
+        """)
+        stmt.setString(1, requestUid)
+        val res = stmt.executeQuery()
+        res.use {
+            if (!it.next()) return null
+            return TransferRequest(
+                wtid = it.getString("wtid"),
+                amount = TalerAmount(
+                    value = it.getLong("amount_value"),
+                    frac = it.getInt("amount_frac"),
+                ),
+                credit_account = it.getString("credit_account_payto"),
+                exchange_base_url = it.getString("exchange_base_url"),
+                request_uid = requestUid,
+                // FIXME: fix the following two after setting the 
bank_transaction_id on this row.
+                row_id = 0L,
+                timestamp = 0L
+            )
+        }
+    }
+
+    /**
+     * This function calls the SQL function that (1) inserts the TWG
+     * requests details into the database and (2) performs the actual
+     * bank transaction to pay the merchant according to the 'req' parameter.
+     *
+     * 'req' contains the same data that was POSTed by the exchange
+     * to the TWG /transfer endpoint.  The exchangeBankAccountId parameter
+     * is the row ID of the exchange's bank account.  The return type
+     * is the same returned by "bank_wire_transfer()" where however
+     * the NO_DEBTOR error will hardly take place.
+     */
+    fun talerTransferCreate(
+        req: TransferRequest,
+        exchangeBankAccountId: Long,
+        timestamp: Long,
+        acctSvcrRef: String = "not used",
+        pmtInfId: String = "not used",
+        endToEndId: String = "not used",
+        ): BankTransactionResult {
+        reconnect()
+        // FIXME: future versions should return the exchange's latest bank 
transaction ID
+        val stmt = prepare("""
+            SELECT
+              out_exchange_balance_insufficient
+              ,out_nx_creditor
+              FROM
+                taler_transfer (
+                  ?,
+                  ?,
+                  (?,?)::taler_amount,
+                  ?,
+                  ?,
+                  ?,
+                  ?,
+                  ?,
+                  ?,
+                  ?
+                );
+        """)
+        stmt.setString(1, req.request_uid)
+        stmt.setString(2, req.wtid)
+        stmt.setLong(3, req.amount.value)
+        stmt.setInt(4, req.amount.frac)
+        stmt.setString(5, req.exchange_base_url)
+        stmt.setString(6, req.credit_account)
+        stmt.setLong(7, exchangeBankAccountId)
+        stmt.setLong(8, timestamp)
+        stmt.setString(9, acctSvcrRef)
+        stmt.setString(10, pmtInfId)
+        stmt.setString(11, endToEndId)
+
+        val res = stmt.executeQuery()
+        res.use {
+            if (!it.next())
+                throw internalServerError("SQL function taler_transfer did not 
return anything.")
+            if (it.getBoolean("out_exchange_balance_insufficient")) return 
BankTransactionResult.CONFLICT
+            if (it.getBoolean("out_nx_creditor")) return 
BankTransactionResult.NO_CREDITOR
+            return BankTransactionResult.SUCCESS
+        }
+    }
 }
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index 179f4212..bbe767e3 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -39,6 +39,7 @@ import kotlinx.serialization.encoding.Encoder
 import kotlinx.serialization.json.*
 import kotlinx.serialization.modules.SerializersModule
 import net.taler.common.errorcodes.TalerErrorCode
+import org.jetbrains.exposed.sql.stringLiteral
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.event.Level
@@ -88,6 +89,25 @@ object RelativeTimeSerializer : KSerializer<RelativeTime> {
         }
 }
 
+object TalerAmountSerializer : KSerializer<TalerAmount> {
+
+    override val descriptor: SerialDescriptor =
+        PrimitiveSerialDescriptor("TalerAmount", PrimitiveKind.STRING)
+    override fun serialize(encoder: Encoder, value: TalerAmount) {
+        throw internalServerError("Encoding of TalerAmount not implemented.") 
// API doesn't require this.
+    }
+    override fun deserialize(decoder: Decoder): TalerAmount {
+        val maybeAmount = try {
+            decoder.decodeString()
+        } catch (e: Exception) {
+            throw badRequest(
+                "Did not find any Taler amount as string: ${e.message}",
+                TalerErrorCode.TALER_EC_GENERIC_JSON_INVALID
+            )
+        }
+        return parseTalerAmount(maybeAmount)
+    }
+}
 
 /**
  * This function tries to authenticate the call according
@@ -148,6 +168,9 @@ val webApp: Application.() -> Unit = {
                 contextual(RelativeTime::class) {
                     RelativeTimeSerializer
                 }
+                contextual(TalerAmount::class) {
+                    TalerAmountSerializer
+                }
             }
         })
     }
diff --git 
a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
index dc460928..d6ef6910 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/talerWireGatewayHandlers.kt
@@ -30,7 +30,7 @@ import net.taler.common.errorcodes.TalerErrorCode
 import tech.libeufin.util.getNowUs
 
 fun Routing.talerWireGatewayHandlers() {
-    get("/accounts/{USERNAME}/taler-wire-gateway/config") {
+    get("/taler-wire-gateway/config") {
         val internalCurrency = db.configGet("internal_currency")
             ?: throw internalServerError("Could not find bank own currency.")
         call.respond(TWGConfigResponse(currency = internalCurrency))
@@ -69,6 +69,56 @@ fun Routing.talerWireGatewayHandlers() {
         return@get
     }
     post("/accounts/{USERNAME}/taler-wire-gateway/transfer") {
+        val c = call.myAuth(TokenScope.readwrite) ?: throw unauthorized()
+        if (!call.getResourceName("USERNAME").canI(c, withAdmin = false)) 
throw forbidden()
+        val req = call.receive<TransferRequest>()
+        // Checking for idempotency.
+        val maybeDoneAlready = db.talerTransferGetFromUid(req.request_uid)
+        if (maybeDoneAlready != null) {
+            val isIdempotent =
+                maybeDoneAlready.amount == req.amount
+                        && maybeDoneAlready.credit_account == 
req.credit_account
+                        && maybeDoneAlready.exchange_base_url == 
req.exchange_base_url
+                        && maybeDoneAlready.wtid == req.wtid
+            if (isIdempotent) {
+                val timestamp = maybeDoneAlready.timestamp
+                    ?: throw internalServerError("Timestamp not found on 
idempotent request")
+                val rowId = maybeDoneAlready.row_id
+                    ?: throw internalServerError("Row ID not found on 
idempotent request")
+                call.respond(TransferResponse(
+                    timestamp = timestamp,
+                    row_id = rowId
+                ))
+                return@post
+            }
+            throw conflict(
+                hint = "request_uid used already",
+                talerEc = TalerErrorCode.TALER_EC_END // FIXME: need 
appropriate Taler EC.
+            )
+        }
+        // Legitimate request, go on.
+        val exchangeBankAccount = db.bankAccountGetFromOwnerId(c.expectRowId())
+            ?: throw internalServerError("Exchange does not have a bank 
account")
+        val transferTimestamp = getNowUs()
+        val dbRes = db.talerTransferCreate(
+            req = req,
+            exchangeBankAccountId = exchangeBankAccount.expectRowId(),
+            timestamp = transferTimestamp
+        )
+        if (dbRes == Database.BankTransactionResult.CONFLICT)
+            throw conflict(
+                "Insufficient balance for exchange",
+                TalerErrorCode.TALER_EC_END // FIXME
+            )
+        if (dbRes == Database.BankTransactionResult.NO_CREDITOR)
+            throw notFound(
+                "Creditor account was not found",
+                TalerErrorCode.TALER_EC_END // FIXME
+            )
+        call.respond(TransferResponse(
+            timestamp = transferTimestamp,
+            row_id = 0 // FIXME!
+        ))
         return@post
     }
     post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
index bd64def0..cda4664d 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/types.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/types.kt
@@ -23,6 +23,7 @@ import io.ktor.http.*
 import io.ktor.server.application.*
 import kotlinx.serialization.Contextual
 import kotlinx.serialization.Serializable
+import java.io.Serial
 import java.util.*
 
 // Allowed lengths for fractional digits in amounts.
@@ -545,4 +546,25 @@ data class IncomingReserveTransaction(
     val amount: String,
     val debit_account: String, // Payto of the sender.
     val reserve_pub: String
+)
+
+// TWG's request to pay a merchant.
+@Serializable
+data class TransferRequest(
+    val request_uid: String,
+    @Contextual
+    val amount: TalerAmount,
+    val exchange_base_url: String,
+    val wtid: String,
+    val credit_account: String,
+    // Only used when this type if defined from a DB record
+    val timestamp: Long? = null, // when this request got finalized with a 
wire transfer
+    val row_id: Long? = null // DB row ID of this record
+)
+
+// TWG's response to merchant payouts
+@Serializable
+data class TransferResponse(
+    val timestamp: Long,
+    val row_id: Long
 )
\ No newline at end of file
diff --git a/bank/src/test/kotlin/DatabaseTest.kt 
b/bank/src/test/kotlin/DatabaseTest.kt
index f38e9559..9083d870 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -17,7 +17,6 @@
  * <http://www.gnu.org/licenses/>
  */
 
-
 import org.junit.Test
 import tech.libeufin.bank.*
 import tech.libeufin.util.getNowUs
@@ -75,6 +74,35 @@ class DatabaseTest {
         maxDebt = TalerAmount(10, 1, "KUDOS")
     )
     val fooPaysBar = genTx()
+
+    /**
+     * Tests the SQL function that performs the instructions
+     * given by the exchange to pay one merchant.
+     */
+    @Test
+    fun talerTransferTest() {
+        val exchangeReq = TransferRequest(
+            amount = TalerAmount(9, 0, "KUDOS"),
+            credit_account = "BAR-IBAN-ABC", // foo pays bar
+            exchange_base_url = "example.com/exchange",
+            request_uid = "entropic 0",
+            wtid = "entropic 1"
+        )
+        val db = initDb()
+        val fooId = db.customerCreate(customerFoo)
+        assert(fooId != null)
+        val barId = db.customerCreate(customerBar)
+        assert(barId != null)
+        assert(db.bankAccountCreate(bankAccountFoo))
+        assert(db.bankAccountCreate(bankAccountBar))
+        val res = db.talerTransferCreate(
+            req = exchangeReq,
+            exchangeBankAccountId = 1L,
+            timestamp = getNowUs()
+        )
+        assert(res == Database.BankTransactionResult.SUCCESS)
+    }
+
     @Test
     fun bearerTokenTest() {
         val db = initDb()
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index 7617467e..757d1e82 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -44,6 +44,63 @@ class TalerApiTest {
         cashoutPayto = "payto://external-IBAN",
         cashoutCurrency = "KUDOS"
     )
+    // Testing the POST /transfer call from the TWG API.
+    @Test
+    fun transfer() {
+        val db = initDb()
+        // Creating the exchange and merchant accounts first.
+        assert(db.customerCreate(customerFoo) != null)
+        assert(db.bankAccountCreate(bankAccountFoo))
+        assert(db.customerCreate(customerBar) != null)
+        assert(db.bankAccountCreate(bankAccountBar))
+        // Give the exchange reasonable debt allowance:
+        assert(db.bankAccountSetMaxDebt(
+            1L,
+            TalerAmount(1000, 0)
+        ))
+        // Do POST /transfer.
+        testApplication {
+            application(webApp)
+            val req = """
+                    {
+                      "request_uid": "entropic 0",
+                      "wtid": "entropic 1",
+                      "exchange_base_url": "http://exchange.example.com/";,
+                      "amount": "KUDOS:33",
+                      "credit_account": "BAR-IBAN-ABC"
+                    }
+                """.trimIndent()
+            client.post("/accounts/foo/taler-wire-gateway/transfer") {
+                basicAuth("foo", "pw")
+                contentType(ContentType.Application.Json)
+                expectSuccess = true
+                setBody(req)
+            }
+            // check idempotency
+            client.post("/accounts/foo/taler-wire-gateway/transfer") {
+                basicAuth("foo", "pw")
+                contentType(ContentType.Application.Json)
+                expectSuccess = true
+                setBody(req)
+            }
+            // Trigger conflict due to reused request_uid
+            val r = client.post("/accounts/foo/taler-wire-gateway/transfer") {
+                basicAuth("foo", "pw")
+                contentType(ContentType.Application.Json)
+                expectSuccess = false
+                setBody("""
+                    {
+                      "request_uid": "entropic 0",
+                      "wtid": "entropic 1",
+                      "exchange_base_url": 
"http://different-exchange.example.com/";,
+                      "amount": "KUDOS:33",
+                      "credit_account": "BAR-IBAN-ABC"
+                    }
+                """.trimIndent())
+            }
+            assert(r.status == HttpStatusCode.Conflict)
+        }
+    }
     // Testing the /history/incoming call from the TWG API.
     @Test
     fun historyIncoming() {
diff --git a/database-versioning/libeufin-bank-0001.sql 
b/database-versioning/libeufin-bank-0001.sql
index cb5a2f5e..e288f846 100644
--- a/database-versioning/libeufin-bank-0001.sql
+++ b/database-versioning/libeufin-bank-0001.sql
@@ -363,6 +363,22 @@ CREATE TABLE IF NOT EXISTS bank_account_statements
 -- end of: accounts activity report 
 
 -- start of: Taler integration
+CREATE TABLE IF NOT EXISTS taler_exchange_transfers
+  (exchange_transfer_id BIGINT GENERATED BY DEFAULT AS IDENTITY
+  ,request_uid TEXT NOT NULL UNIQUE
+  ,wtid TEXT NOT NULL UNIQUE
+  ,exchange_base_url TEXT NOT NULL
+  ,credit_account_payto TEXT NOT NULL
+  ,amount taler_amount NOT NULL
+  ,bank_transaction BIGINT UNIQUE -- NOT NULL FIXME: make this not null.
+    REFERENCES bank_account_transactions(bank_transaction_id)
+      ON DELETE RESTRICT
+      ON UPDATE RESTRICT
+  );
+COMMENT ON TABLE taler_exchange_transfers
+  IS 'Tracks all the requests made by Taler exchanges to pay merchants';
+COMMENT ON COLUMN taler_exchange_transfers.bank_transaction
+  IS 'Reference to the (outgoing) bank transaction that finalizes the exchange 
transfer request.';
 
 CREATE TABLE IF NOT EXISTS taler_withdrawal_operations
   (taler_withdrawal_id BIGINT GENERATED BY DEFAULT AS IDENTITY
diff --git a/database-versioning/procedures.sql 
b/database-versioning/procedures.sql
index 21585ced..3968389e 100644
--- a/database-versioning/procedures.sql
+++ b/database-versioning/procedures.sql
@@ -86,6 +86,91 @@ END $$;
 COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT)
   IS 'Update or insert configuration values';
 
+CREATE OR REPLACE FUNCTION taler_transfer(
+  IN in_request_uid TEXT,
+  IN in_wtid TEXT,
+  IN in_amount taler_amount,
+  IN in_exchange_base_url TEXT,
+  IN in_credit_account_payto TEXT,
+  IN in_exchange_bank_account_id BIGINT,
+  IN in_timestamp BIGINT,
+  IN in_account_servicer_reference TEXT,
+  IN in_payment_information_id TEXT,
+  IN in_end_to_end_id TEXT,
+  OUT out_exchange_balance_insufficient BOOLEAN,
+  OUT out_nx_creditor BOOLEAN
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+maybe_balance_insufficient BOOLEAN;
+receiver_bank_account_id BIGINT;
+payment_subject TEXT;
+BEGIN
+
+INSERT
+  INTO taler_exchange_transfers (
+    request_uid,
+    wtid,
+    exchange_base_url,
+    credit_account_payto,
+    amount
+    -- FIXME: this needs the bank transaction row ID here.
+) VALUES (
+  in_request_uid,
+  in_wtid,
+  in_exchange_base_url,
+  in_credit_account_payto,
+  in_amount
+);
+SELECT
+  bank_account_id
+  INTO receiver_bank_account_id
+  FROM bank_accounts
+  WHERE internal_payto_uri = in_credit_account_payto;
+IF NOT FOUND
+THEN
+  out_nx_creditor=TRUE;
+  RETURN;
+END IF;
+out_nx_creditor=FALSE;
+SELECT CONCAT(in_wtid, ' ', in_exchange_base_url)
+  INTO payment_subject;
+SELECT
+  out_balance_insufficient
+  INTO maybe_balance_insufficient
+  FROM bank_wire_transfer(
+    receiver_bank_account_id,
+    in_exchange_bank_account_id,
+    payment_subject,
+    in_amount,
+    in_timestamp,
+    in_account_servicer_reference,
+    in_payment_information_id,
+    in_end_to_end_id
+  );
+IF (maybe_balance_insufficient)
+THEN
+  out_exchange_balance_insufficient=TRUE;
+END IF;
+out_exchange_balance_insufficient=FALSE;
+END $$;
+COMMENT ON FUNCTION taler_transfer(
+  text,
+  text,
+  taler_amount,
+  text,
+  text,
+  bigint,
+  bigint,
+  text,
+  text,
+  text
+  )
+  IS 'function that (1) inserts the TWG requests'
+     'details into the database and (2) performs '
+     'the actual bank transaction to pay the merchant';
+
 CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
   IN in_withdrawal_uuid uuid,
   IN in_confirmation_date BIGINT,

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]