[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] 01/03: nexus db: creating & getting payment initiations.
From: |
gnunet |
Subject: |
[libeufin] 01/03: nexus db: creating & getting payment initiations. |
Date: |
Tue, 24 Oct 2023 10:57:48 +0200 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
commit 553ce68abdb4c73e486994594bae2bcc6cfdef2d
Author: MS <ms@taler.net>
AuthorDate: Mon Oct 23 03:26:46 2023 +0200
nexus db: creating & getting payment initiations.
---
.idea/kotlinc.xml | 6 --
Makefile | 1 -
contrib/libeufin-nexus.conf | 3 +
database-versioning/libeufin-nexus-0001.sql | 2 +-
.../main/kotlin/tech/libeufin/nexus/Database.kt | 77 ++++++++++++++++++----
.../src/main/kotlin/tech/libeufin/nexus/DbInit.kt | 43 ++++++++++++
.../main/kotlin/tech/libeufin/nexus/EbicsSetup.kt | 14 ++--
nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 42 +++++++++---
nexus/src/test/kotlin/DatabaseTest.kt | 57 ++++++++++++++--
nexus/src/test/kotlin/Keys.kt | 4 +-
util/src/main/kotlin/DB.kt | 9 ++-
11 files changed, 210 insertions(+), 48 deletions(-)
diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml
deleted file mode 100644
index 4251b727..00000000
--- a/.idea/kotlinc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project version="4">
- <component name="KotlinJpsPluginSettings">
- <option name="version" value="1.7.22" />
- </component>
-</project>
\ No newline at end of file
diff --git a/Makefile b/Makefile
index 571125d9..9a6612e2 100644
--- a/Makefile
+++ b/Makefile
@@ -53,7 +53,6 @@ install-nexus:
install contrib/libeufin-nexus.conf $(nexus_config_dir)/
install -D database-versioning/libeufin-nexus*.sql -t $(nexus_sql_dir)
install -D database-versioning/versioning.sql -t $(nexus_sql_dir)
- install -D database-versioning/procedures.sql -t $(nexus_sql_dir)
./gradlew -q -Pprefix=$(abs_destdir)$(prefix) nexus:installToPrefix
.PHONY: assemble
diff --git a/contrib/libeufin-nexus.conf b/contrib/libeufin-nexus.conf
index 16153c81..5db6b00b 100644
--- a/contrib/libeufin-nexus.conf
+++ b/contrib/libeufin-nexus.conf
@@ -38,6 +38,9 @@ BANK_DIALECT = postfinance
[nexus-postgres]
CONFIG = postgres:///libeufin-nexus
+[libeufin-nexusdb-postgres]
+SQL_DIR = $DATADIR/sql/
+
[nexus-ebics-fetch]
FREQUENCY = 30s # used when long-polling is not supported
STATEMENT_LOG_DIRECTORY = /tmp/ebics-messages/
diff --git a/database-versioning/libeufin-nexus-0001.sql
b/database-versioning/libeufin-nexus-0001.sql
index 39f3bf5c..97dbb527 100644
--- a/database-versioning/libeufin-nexus-0001.sql
+++ b/database-versioning/libeufin-nexus-0001.sql
@@ -51,7 +51,7 @@ CREATE TABLE IF NOT EXISTS initiated_outgoing_transactions
(initiated_outgoing_transaction_id INT8 GENERATED BY DEFAULT AS IDENTITY
UNIQUE -- used as our ID in PAIN
,amount taler_amount NOT NULL
,wire_transfer_subject TEXT
- ,execution_time INT8 NOT NULL
+ ,initiation_time INT8 NOT NULL
,credit_payto_uri TEXT NOT NULL
,outgoing_transaction_id INT8 REFERENCES outgoing_transactions
(outgoing_transaction_id)
,submitted BOOL DEFAULT FALSE
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
index 9e15ad89..14d235fe 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
@@ -5,6 +5,7 @@ import kotlinx.coroutines.withContext
import org.postgresql.jdbc.PgConnection
import tech.libeufin.util.pgDataSource
import com.zaxxer.hikari.*
+import tech.libeufin.util.microsToJavaInstant
import tech.libeufin.util.stripIbanPayto
import tech.libeufin.util.toDbMicros
import java.sql.PreparedStatement
@@ -26,8 +27,8 @@ data class TalerAmount(
data class InitiatedPayment(
val amount: TalerAmount,
val wireTransferSubject: String,
- val executionTime: Instant,
val creditPaytoUri: String,
+ val initiationTime: Instant,
val clientRequestUuid: String? = null
)
@@ -36,7 +37,6 @@ data class InitiatedPayment(
* into the database.
*/
enum class PaymentInitiationOutcome {
- BAD_TIMESTAMP,
BAD_CREDIT_PAYTO,
UNIQUE_CONSTRAINT_VIOLATION,
SUCCESS
@@ -96,43 +96,96 @@ class Database(dbConfig: String): java.io.Closeable {
}
}
+ /**
+ * Sets payment initiation as submitted.
+ *
+ * @param rowId row ID of the record to set.
+ * @return true on success, false otherwise.
+ */
+ suspend fun initiatedPaymentSetSubmitted(rowId: Long): Boolean {
+ throw NotImplementedError()
+ }
+
+ /**
+ * Gets any initiated payment that was not submitted to the
+ * bank yet.
+ *
+ * @param currency in which currency should the payment be submitted to
the bank.
+ * @return potentially empty list of initiated payments.
+ */
+ suspend fun initiatedPaymentsUnsubmittedGet(currency: String): Map<Long,
InitiatedPayment> = runConn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT
+ initiated_outgoing_transaction_id
+ ,(amount).val as amount_val
+ ,(amount).frac as amount_frac
+ ,wire_transfer_subject
+ ,credit_payto_uri
+ ,initiation_time
+ ,client_request_uuid
+ FROM initiated_outgoing_transactions
+ WHERE submitted=false;
+ """)
+ val maybeMap = mutableMapOf<Long, InitiatedPayment>()
+ stmt.executeQuery().use {
+ if (!it.next()) return@use
+ do {
+ val rowId = it.getLong("initiated_outgoing_transaction_id")
+ val initiationTime =
it.getLong("initiation_time").microsToJavaInstant()
+ if (initiationTime == null) { // nexus fault
+ throw Exception("Found invalid timestamp at initiated
payment with ID: $rowId")
+ }
+ maybeMap[rowId] = InitiatedPayment(
+ amount = TalerAmount(
+ value = it.getLong("amount_val"),
+ fraction = it.getInt("amount_frac"),
+ currency = currency
+ ),
+ creditPaytoUri = it.getString("credit_payto_uri"),
+ wireTransferSubject =
it.getString("wire_transfer_subject"),
+ initiationTime = initiationTime,
+ clientRequestUuid = it.getString("client_request_uuid")
+ )
+ } while (it.next())
+ }
+ return@runConn maybeMap
+ }
/**
* Initiate a payment in the database. The "submit"
* command is then responsible to pick it up and submit
- * it at the bank.
+ * it to the bank.
*
* @param paymentData any data that's used to prepare the payment.
* @return true if the insertion went through, false in case of errors.
*/
- suspend fun initiatePayment(paymentData: InitiatedPayment):
PaymentInitiationOutcome = runConn { conn ->
+ suspend fun initiatedPaymentCreate(paymentData: InitiatedPayment):
PaymentInitiationOutcome = runConn { conn ->
val stmt = conn.prepareStatement("""
INSERT INTO initiated_outgoing_transactions (
amount
,wire_transfer_subject
- ,execution_time
,credit_payto_uri
+ ,initiation_time
,client_request_uuid
) VALUES (
(?,?)::taler_amount
,?
,?
,?
- ,?
+ ,?
)
""")
stmt.setLong(1, paymentData.amount.value)
stmt.setInt(2, paymentData.amount.fraction)
stmt.setString(3, paymentData.wireTransferSubject)
- val executionTime = paymentData.executionTime.toDbMicros() ?: run {
- logger.error("Execution time could not be converted to
microseconds for the database.")
- return@runConn PaymentInitiationOutcome.BAD_TIMESTAMP // nexus
fault.
- }
- stmt.setLong(4, executionTime)
val paytoOnlyIban = stripIbanPayto(paymentData.creditPaytoUri) ?: run {
logger.error("Credit Payto address is invalid.")
return@runConn PaymentInitiationOutcome.BAD_CREDIT_PAYTO // client
fault.
}
- stmt.setString(5, paytoOnlyIban)
+ stmt.setString(4, paytoOnlyIban)
+ val initiationTime = paymentData.initiationTime.toDbMicros() ?: run {
+ throw Exception("Initiation time could not be converted to
microseconds for the database.")
+ }
+ stmt.setLong(5, initiationTime)
stmt.setString(6, paymentData.clientRequestUuid) // can be null.
if (stmt.maybeUpdate())
return@runConn PaymentInitiationOutcome.SUCCESS
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DbInit.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/DbInit.kt
new file mode 100644
index 00000000..4cc03f88
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DbInit.kt
@@ -0,0 +1,43 @@
+package tech.libeufin.nexus
+
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.flag
+import com.github.ajalt.clikt.parameters.options.option
+import tech.libeufin.util.initializeDatabaseTables
+import tech.libeufin.util.resetDatabaseTables
+import kotlin.system.exitProcess
+
+fun doOrFail(doLambda: () -> Unit) {
+ try {
+ doLambda()
+ } catch (e: Exception) {
+ logger.error(e.message)
+ exitProcess(1)
+ }
+}
+
+/**
+ * This subcommand tries to load the SQL files that define
+ * the Nexus DB schema. Admits the --reset option to delete
+ * the data first.
+ */
+class DbInit : CliktCommand("Initialize the libeufin-nexus database", name =
"dbinit") {
+ private val configFile by option(
+ "--config", "-c",
+ help = "set the configuration file"
+ )
+ private val requestReset by option(
+ "--reset", "-r",
+ help = "reset database (DANGEROUS: All existing data is lost)"
+ ).flag()
+
+ override fun run() {
+ val cfg = loadConfigOrFail(configFile).extractDbConfigOrFail()
+ doOrFail {
+ if (requestReset) {
+ resetDatabaseTables(cfg, sqlFilePrefix = "libeufin-nexus")
+ }
+ initializeDatabaseTables(cfg, sqlFilePrefix = "libeufin-nexus")
+ }
+ }
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
index 2125d0a0..b842b978 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsSetup.kt
@@ -311,14 +311,8 @@ private fun findBic(maybeList:
List<EbicsTypes.AccountInfo>?): String? {
* @param configFile location of the configuration entry point.
* @return internal representation of the configuration.
*/
-private fun extractConfig(configFile: String?): EbicsSetupConfig {
- val config = TalerConfig(NEXUS_CONFIG_SOURCE)
- try {
- config.load(configFile)
- } catch (e: Exception) {
- logger.error("Could not load configuration from ${configFile}, detail:
${e.message}")
- exitProcess(1)
- }
+private fun extractEbicsConfig(configFile: String?): EbicsSetupConfig {
+ val config = loadConfigOrFail(configFile)
// Checking the config.
val cfg = try {
EbicsSetupConfig(config)
@@ -355,7 +349,7 @@ private fun makePdf(privs: ClientPrivateKeysFile, cfg:
EbicsSetupConfig) {
/**
* CLI class implementing the "ebics-setup" subcommand.
*/
-class EbicsSetup: CliktCommand() {
+class EbicsSetup: CliktCommand("Set up the EBICS subscriber") {
private val configFile by option(
"--config", "-c",
help = "set the configuration file"
@@ -380,7 +374,7 @@ class EbicsSetup: CliktCommand() {
* This function collects the main steps of setting up an EBICS access.
*/
override fun run() {
- val cfg = extractConfig(this.configFile)
+ val cfg = extractEbicsConfig(this.configFile)
if (checkFullConfig) {
throw NotImplementedError("--check-full-config flag not
implemented")
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index b148c4b2..8668912c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -25,15 +25,11 @@
package tech.libeufin.nexus
import ConfigSource
import TalerConfig
-import TalerConfigError
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.subcommands
-import com.github.ajalt.clikt.parameters.options.flag
-import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.versionOption
import io.ktor.client.*
import io.ktor.util.*
-import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Contextual
import kotlinx.serialization.KSerializer
import org.slf4j.Logger
@@ -44,20 +40,15 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import net.taler.wallet.crypto.Base32Crockford
-import org.slf4j.event.Level
import tech.libeufin.nexus.ebics.*
import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.EbicsTypes
import java.security.interfaces.RSAPrivateCrtKey
import java.security.interfaces.RSAPublicKey
-import java.time.Instant
-import kotlin.reflect.typeOf
val NEXUS_CONFIG_SOURCE = ConfigSource("libeufin-nexus", "libeufin-nexus")
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus.Main")
@@ -267,13 +258,44 @@ fun loadPrivateKeysFromDisk(location: String):
ClientPrivateKeysFile? {
}
}
+/**
+ * Abstracts the config loading and exception handling.
+ *
+ * @param configFile potentially NULL configuration file location.
+ * @return the configuration handle.
+ */
+fun loadConfigOrFail(configFile: String?): TalerConfig {
+ val config = TalerConfig(NEXUS_CONFIG_SOURCE)
+ try {
+ config.load(configFile)
+ } catch (e: Exception) {
+ logger.error("Could not load configuration from ${configFile}, detail:
${e.message}")
+ exitProcess(1)
+ }
+ return config
+}
+
+/**
+ * Abstracts fetching the DB config values to set up Nexus.
+ */
+fun TalerConfig.extractDbConfigOrFail(): DatabaseConfig =
+ try {
+ DatabaseConfig(
+ dbConnStr = requireString("nexus-postgres", "config"),
+ sqlDir = requirePath("libeufin-nexusdb-postgres", "sql_dir")
+ )
+ } catch (e: Exception) {
+ logger.error("Could not load config options for Nexus DB, detail:
${e.message}.")
+ exitProcess(1)
+ }
+
/**
* Main CLI class that collects all the subcommands.
*/
class LibeufinNexusCommand : CliktCommand() {
init {
versionOption(getVersion())
- subcommands(EbicsSetup())
+ subcommands(EbicsSetup(), DbInit())
}
override fun run() = Unit
}
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt
b/nexus/src/test/kotlin/DatabaseTest.kt
index 2858af8e..9a1b945a 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -4,24 +4,73 @@ import tech.libeufin.nexus.InitiatedPayment
import tech.libeufin.nexus.NEXUS_CONFIG_SOURCE
import tech.libeufin.nexus.PaymentInitiationOutcome
import tech.libeufin.nexus.TalerAmount
+import tech.libeufin.util.connectWithSchema
import java.time.Instant
import kotlin.test.assertEquals
+import kotlin.test.assertTrue
class DatabaseTest {
@Test
fun paymentInitiation() {
val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
+ runBlocking {
+ val beEmpty = db.initiatedPaymentsUnsubmittedGet("KUDOS")// expect
no records.
+ assertEquals(beEmpty.size, 0)
+ }
val initPay = InitiatedPayment(
amount = TalerAmount(44, 0, "KUDOS"),
creditPaytoUri = "payto://iban/not-used",
- executionTime = Instant.now(),
wireTransferSubject = "test",
- clientRequestUuid = "unique"
+ clientRequestUuid = "unique",
+ initiationTime = Instant.now()
)
runBlocking {
- assertEquals(db.initiatePayment(initPay),
PaymentInitiationOutcome.SUCCESS)
- assertEquals(db.initiatePayment(initPay),
PaymentInitiationOutcome.UNIQUE_CONSTRAINT_VIOLATION)
+ assertEquals(db.initiatedPaymentCreate(initPay),
PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(initPay),
PaymentInitiationOutcome.UNIQUE_CONSTRAINT_VIOLATION)
+ val haveOne = db.initiatedPaymentsUnsubmittedGet("KUDOS")
+ assertTrue {
+ haveOne.size == 1
+ && haveOne.containsKey(1)
+ && haveOne[1]?.clientRequestUuid == "unique"
+ }
+ }
+ }
+
+ /**
+ * Tests how the fetch method gets the list of
+ * multiple unsubmitted payment initiations.
+ */
+ @Test
+ fun paymentInitiationsMultiple() {
+ val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
+ fun genInitPay(subject: String, rowUuid: String? = null) =
+ InitiatedPayment(
+ amount = TalerAmount(44, 0, "KUDOS"),
+ creditPaytoUri = "payto://iban/not-used",
+ wireTransferSubject = subject,
+ initiationTime = Instant.now(),
+ clientRequestUuid = rowUuid
+ )
+ runBlocking {
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#1")),
PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#2")),
PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#3")),
PaymentInitiationOutcome.SUCCESS)
+ assertEquals(db.initiatedPaymentCreate(genInitPay("#4")),
PaymentInitiationOutcome.SUCCESS)
+ // Marking one as submitted, hence not expecting it in the results.
+ db.runConn { conn ->
+ conn.execSQLUpdate("""
+ UPDATE initiated_outgoing_transactions
+ SET submitted = true
+ WHERE initiated_outgoing_transaction_id=3;
+ """.trimIndent())
+ }
+ db.initiatedPaymentsUnsubmittedGet("KUDOS").apply {
+ assertEquals(3, this.size)
+ assertEquals("#1", this[1]?.wireTransferSubject)
+ assertEquals("#2", this[2]?.wireTransferSubject)
+ assertEquals("#4", this[4]?.wireTransferSubject)
+ }
}
}
}
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/Keys.kt b/nexus/src/test/kotlin/Keys.kt
index db598d44..d2894c04 100644
--- a/nexus/src/test/kotlin/Keys.kt
+++ b/nexus/src/test/kotlin/Keys.kt
@@ -23,9 +23,9 @@ class PublicKeys {
bank_encryption_public_key =
CryptoUtil.generateRsaKeyPair(2028).public
)
// storing them on disk.
- assertTrue(syncJsonToDisk(fileContent, config.bankPublicKeysFilename))
+ assertTrue(syncJsonToDisk(fileContent,
"/tmp/nexus-tests-bank-keys.json"))
// loading them and check that values are the same.
- val fromDisk = loadBankKeys(config.bankPublicKeysFilename)
+ val fromDisk = loadBankKeys("/tmp/nexus-tests-bank-keys.json")
assertNotNull(fromDisk)
assertTrue {
fromDisk.accepted &&
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index 9c530e2b..2966880a 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -400,8 +400,13 @@ fun initializeDatabaseTables(cfg: DatabaseConfig,
sqlFilePrefix: String) {
val sqlPatchText = path.readText()
conn.execSQLUpdate(sqlPatchText)
}
- val sqlProcedures = File("${cfg.sqlDir}/procedures.sql").readText()
- conn.execSQLUpdate(sqlProcedures)
+ val sqlProcedures = File("${cfg.sqlDir}/procedures.sql")
+ // Nexus doesn't have any procedures.
+ if (!sqlProcedures.exists()) {
+ logger.info("No procedures.sql for the SQL collection:
$sqlFilePrefix")
+ return@transaction
+ }
+ conn.execSQLUpdate(sqlProcedures.readText())
}
}
}
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.