[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: nexus fetch, adding:
From: |
gnunet |
Subject: |
[libeufin] branch master updated: nexus fetch, adding: |
Date: |
Tue, 14 Nov 2023 17:47:26 +0100 |
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 cc35923a nexus fetch, adding:
cc35923a is described below
commit cc35923acc45c1fe0acf4fa277ec490b43a27094
Author: MS <ms@taler.net>
AuthorDate: Tue Nov 14 17:45:27 2023 +0100
nexus fetch, adding:
- SQL to store talerable and bounced incoming transactions
- draft of camt.054 parser
- logic to bounce or accept based on the payment subject
---
database-versioning/libeufin-nexus-procedures.sql | 63 ++++-
.../main/kotlin/tech/libeufin/nexus/Database.kt | 95 ++++++--
.../main/kotlin/tech/libeufin/nexus/EbicsFetch.kt | 270 ++++++++++++++++++++-
nexus/src/test/kotlin/DatabaseTest.kt | 76 ++----
nexus/src/test/kotlin/Parsing.kt | 72 ++++++
util/src/main/kotlin/time.kt | 14 +-
6 files changed, 505 insertions(+), 85 deletions(-)
diff --git a/database-versioning/libeufin-nexus-procedures.sql
b/database-versioning/libeufin-nexus-procedures.sql
index 84dbcfc6..7a36b91a 100644
--- a/database-versioning/libeufin-nexus-procedures.sql
+++ b/database-versioning/libeufin-nexus-procedures.sql
@@ -9,8 +9,12 @@ CREATE OR REPLACE FUNCTION create_incoming_and_bounce(
,IN in_bank_transfer_id TEXT
,IN in_timestamp BIGINT
,IN in_request_uid TEXT
-) RETURNS void
+ ,OUT out_ok BOOLEAN
+) RETURNS BOOLEAN
LANGUAGE plpgsql AS $$
+DECLARE
+new_tx_id INT8;
+new_init_id INT8;
BEGIN
-- creating the bounced incoming transaction.
INSERT INTO incoming_transactions (
@@ -19,15 +23,14 @@ INSERT INTO incoming_transactions (
,execution_time
,debit_payto_uri
,bank_transfer_id
- ,bounced
) VALUES (
in_amount
,in_wire_transfer_subject
,in_execution_time
,in_debit_payto_uri
,in_bank_transfer_id
- ,true
- );
+ ) RETURNING incoming_transaction_id INTO new_tx_id;
+
-- creating its reimbursement.
INSERT INTO initiated_outgoing_transactions (
amount
@@ -41,7 +44,16 @@ INSERT INTO initiated_outgoing_transactions (
,in_debit_payto_uri
,in_timestamp
,in_request_uid
- );
+ ) RETURNING initiated_outgoing_transaction_id INTO new_init_id;
+
+INSERT INTO bounced_transactions (
+ incoming_transaction_id
+ ,initiated_outgoing_transaction_id
+) VALUES (
+ new_tx_id
+ ,new_init_id
+);
+out_ok = TRUE;
END $$;
COMMENT ON FUNCTION create_incoming_and_bounce(taler_amount, TEXT, BIGINT,
TEXT, TEXT, BIGINT, TEXT)
@@ -141,3 +153,44 @@ UPDATE incoming_transactions
END $$;
COMMENT ON FUNCTION bounce_payment(BIGINT, BIGINT, TEXT) IS 'Marks an incoming
payment as bounced and initiates its refunding payment';
+
+CREATE OR REPLACE FUNCTION create_incoming_talerable(
+ IN in_amount taler_amount
+ ,IN in_wire_transfer_subject TEXT
+ ,IN in_execution_time BIGINT
+ ,IN in_debit_payto_uri TEXT
+ ,IN in_bank_transfer_id TEXT
+ ,IN in_reserve_public_key BYTEA
+ ,OUT out_ok BOOLEAN
+) RETURNS BOOLEAN
+LANGUAGE plpgsql AS $$
+DECLARE
+new_tx_id INT8;
+BEGIN
+INSERT INTO incoming_transactions (
+ amount
+ ,wire_transfer_subject
+ ,execution_time
+ ,debit_payto_uri
+ ,bank_transfer_id
+ ) VALUES (
+ in_amount
+ ,in_wire_transfer_subject
+ ,in_execution_time
+ ,in_debit_payto_uri
+ ,in_bank_transfer_id
+ ) RETURNING incoming_transaction_id INTO new_tx_id;
+INSERT INTO talerable_incoming_transactions (
+ incoming_transaction_id
+ ,reserve_public_key
+) VALUES (
+ new_tx_id
+ ,in_reserve_public_key
+);
+out_ok = TRUE;
+END $$;
+
+COMMENT ON FUNCTION create_incoming_talerable(taler_amount, TEXT, BIGINT,
TEXT, TEXT, BYTEA) IS '
+Creates one row in the incoming transactions table and one row
+in the talerable transactions table. The talerable row links the
+incoming one.';
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
index cc91b557..9b83a707 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Database.kt
@@ -39,7 +39,7 @@ fun TalerAmount.stringify(): String {
*/
data class IncomingPayment(
val amount: TalerAmount,
- val wireTransferSubject: String?,
+ val wireTransferSubject: String,
val debitPaytoUri: String,
val executionTime: Instant,
val bankTransferId: String
@@ -281,13 +281,13 @@ class Database(dbConfig: String): java.io.Closeable {
suspend fun incomingPaymentCreateBounced(
paymentData: IncomingPayment,
requestUid: String
- ) = runConn { conn ->
+ ): Boolean = runConn { conn ->
val refundTimestamp = Instant.now().toDbMicros()
?: throw Exception("Could not convert refund execution time from
Instant.now() to microsends.")
val executionTime = paymentData.executionTime.toDbMicros()
?: throw Exception("Could not convert payment execution time from
Instant to microseconds.")
val stmt = conn.prepareStatement("""
- SELECT create_incoming_and_bounce (
+ SELECT out_ok FROM create_incoming_and_bounce (
(?,?)::taler_amount
,?
,?
@@ -304,7 +304,11 @@ class Database(dbConfig: String): java.io.Closeable {
stmt.setString(6, paymentData.bankTransferId)
stmt.setLong(7, refundTimestamp)
stmt.setString(8, requestUid)
- stmt.executeQuery()
+ val res = stmt.executeQuery()
+ res.use {
+ if (!it.next()) return@runConn false
+ return@runConn it.getBoolean("out_ok")
+ }
}
/**
@@ -329,7 +333,78 @@ class Database(dbConfig: String): java.io.Closeable {
}
/**
- * Creates a new incoming payment record in the database.
+ * Checks if the reserve public key already exists.
+ *
+ * @param maybeReservePub reserve public key to look up
+ * @return true if found, false otherwise
+ */
+ suspend fun isReservePubFound(maybeReservePub: ByteArray): Boolean =
runConn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT 1
+ FROM talerable_incoming_transactions
+ WHERE reserve_public_key = ?;
+ """)
+ stmt.setBytes(1, maybeReservePub)
+ val res = stmt.executeQuery()
+ res.use {
+ return@runConn it.next()
+ }
+ }
+
+ /**
+ * Creates an incoming transaction row and links a new talerable
+ * row to it.
+ *
+ * @param paymentData incoming talerable payment.
+ * @param reservePub reserve public key. The caller is
+ * responsible to check it.
+ */
+ suspend fun incomingTalerablePaymentCreate(
+ paymentData: IncomingPayment,
+ reservePub: ByteArray
+ ): Boolean = runConn { conn ->
+ val stmt = conn.prepareStatement("""
+ SELECT out_ok FROM create_incoming_talerable(
+ (?,?)::taler_amount
+ ,?
+ ,?
+ ,?
+ ,?
+ ,?
+ )""")
+ bindIncomingPayment(paymentData, stmt)
+ stmt.setBytes(7, reservePub)
+ stmt.executeQuery().use {
+ if (!it.next()) return@runConn false
+ return@runConn it.getBoolean("out_ok")
+ }
+ }
+
+ /**
+ * Binds the values of an incoming payment to the prepared
+ * statement's placeholders. Warn: may easily break in case
+ * the placeholders get their positions changed!
+ *
+ * @param data incoming payment to bind to the placeholders
+ * @param stmt statement to receive the values in its placeholders
+ */
+ private fun bindIncomingPayment(
+ data: IncomingPayment,
+ stmt: PreparedStatement
+ ) {
+ stmt.setLong(1, data.amount.value)
+ stmt.setInt(2, data.amount.fraction)
+ stmt.setString(3, data.wireTransferSubject)
+ val executionTime = data.executionTime.toDbMicros() ?: run {
+ throw Exception("Execution time could not be converted to
microseconds for the database.")
+ }
+ stmt.setLong(4, executionTime)
+ stmt.setString(5, data.debitPaytoUri)
+ stmt.setString(6, data.bankTransferId)
+ }
+ /**
+ * Creates a new incoming payment record in the database. It does NOT
+ * update the "talerable" table.
*
* @param paymentData information related to the incoming payment.
* @return true on success, false otherwise.
@@ -350,15 +425,7 @@ class Database(dbConfig: String): java.io.Closeable {
,?
)
""")
- stmt.setLong(1, paymentData.amount.value)
- stmt.setInt(2, paymentData.amount.fraction)
- stmt.setString(3, paymentData.wireTransferSubject)
- val executionTime = paymentData.executionTime.toDbMicros() ?: run {
- throw Exception("Execution time could not be converted to
microseconds for the database.")
- }
- stmt.setLong(4, executionTime)
- stmt.setString(5, paymentData.debitPaytoUri)
- stmt.setString(6, paymentData.bankTransferId)
+ bindIncomingPayment(paymentData, stmt)
return@runConn stmt.maybeUpdate()
}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
index c26ea937..138a695a 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/EbicsFetch.kt
@@ -5,21 +5,21 @@ import com.github.ajalt.clikt.parameters.options.flag
import com.github.ajalt.clikt.parameters.options.option
import io.ktor.client.*
import kotlinx.coroutines.runBlocking
-import org.apache.commons.compress.archivers.zip.ZipFile
-import org.apache.commons.compress.utils.SeekableInMemoryByteChannel
+import net.taler.wallet.crypto.Base32Crockford
+import net.taler.wallet.crypto.EncodingException
import tech.libeufin.nexus.ebics.*
-import tech.libeufin.util.EbicsOrderParams
+import tech.libeufin.util.*
import tech.libeufin.util.ebics_h005.Ebics3Request
-import tech.libeufin.util.getXmlDate
-import tech.libeufin.util.toDbMicros
import java.io.File
+import java.io.IOException
+import java.lang.StringBuilder
import java.nio.file.Path
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId
+import java.util.UUID
import kotlin.concurrent.fixedRateTimer
import kotlin.io.path.createDirectories
-import kotlin.reflect.typeOf
import kotlin.system.exitProcess
/**
@@ -158,6 +158,239 @@ fun maybeLogFile(
}
}
+/**
+ * Converts the given fractional value to the sub-cent 8 digits
+ * fraction used in Taler. Note: this value has very likely a <2
+ * length, but the function is general, for each fraction with at
+ * most 8 digits.
+ *
+ * @param bankFrac fractional value
+ * @return the Taler fractional value with at most 8 digits.
+ */
+fun makeTalerFrac(bankFrac: String): Int {
+ if (bankFrac.length > 8) throw Exception("Fractional value has more than 8
digits")
+ var buf = bankFrac.toIntOrNull() ?: throw Exception("Fractional value not
an Int: $bankFrac")
+ repeat(8 - bankFrac.length) {
+ buf *= 10
+ }
+ return buf
+}
+
+/**
+ * Gets Taler amount from a currency-agnostic value.
+ *
+ * @param noCurrencyAmount currency-agnostic value coming from the bank.
+ * @param currency currency to set to the result.
+ * @return [TalerAmount]
+ */
+fun getTalerAmount(
+ noCurrencyAmount: String,
+ currency: String
+): TalerAmount {
+ if (currency.isEmpty()) throw Exception("Currency is empty")
+ val split = noCurrencyAmount.split(".")
+ // only 1 (no fraction) or 2 (with fraction) sizes allowed.
+ if (split.size != 1 && split.size != 2) throw Exception("Invalid amount:
${noCurrencyAmount}")
+ val value = split[0].toLongOrNull() ?: throw Exception("value part not a
long")
+ if (split.size == 1) return TalerAmount(
+ value = value,
+ fraction = 0,
+ currency = currency
+ )
+ return TalerAmount(
+ value = value,
+ fraction = makeTalerFrac(split[1]),
+ currency = currency
+ )
+}
+
+/**
+ * Searches for incoming transactions in a camt.054 document, that
+ * was downloaded via EBICS notification.
+ *
+ * @param notifXml the input document.
+ * @return any incoming payment as a list of [IncomingPayment]
+ */
+fun findIncomingTxInNotification(
+ notifXml: String,
+ acceptedCurrency: String
+): List<IncomingPayment> {
+ val notifDoc = XMLUtil.parseStringIntoDom(notifXml)
+ val ret = mutableListOf<IncomingPayment>()
+ destructXml(notifDoc) {
+ requireRootElement("Document") {
+ requireUniqueChildNamed("BkToCstmrDbtCdtNtfctn") {
+ mapEachChildNamed("Ntfctn") {
+ mapEachChildNamed("Ntry") {
+ mapEachChildNamed("NtryDtls") {
+ mapEachChildNamed("TxDtls") maybeDbit@{
+
+ // currently, only incoming payments are
considered.
+ if (requireUniqueChildNamed("CdtDbtInd") {
+ focusElement.textContent == "DBIT"
+ }) return@maybeDbit
+
+ // Obtaining the amount.
+ val amount: TalerAmount =
requireUniqueChildNamed("Amt") {
+ val currency =
focusElement.getAttribute("Ccy")
+ if (currency != acceptedCurrency) throw
Exception("Currency $currency not supported")
+ getTalerAmount(focusElement.textContent,
currency)
+ }
+ // Obtaining payment UID.
+ val uidFromBank: String =
requireUniqueChildNamed("Refs") {
+ requireUniqueChildNamed("AcctSvcrRef") {
+ focusElement.textContent
+ }
+ }
+ // Obtaining payment subject.
+ val subject = StringBuilder()
+ requireUniqueChildNamed("RmtInf") {
+ this.mapEachChildNamed("Ustrd") {
+ val piece =
this.focusElement.textContent
+ subject.append(piece)
+ }
+ }
+ // Obtaining the execution time.
+ val executionTime: Instant =
requireUniqueChildNamed("RltdDts") {
+ requireUniqueChildNamed("AccptncDtTm") {
+
parseGregorianTime(focusElement.textContent)
+ }
+ }
+ // Obtaining the payer's details
+ val debtorPayto =
StringBuilder("payto://iban/")
+ requireUniqueChildNamed("RltdPties") {
+ requireUniqueChildNamed("DbtrAcct") {
+ requireUniqueChildNamed("Id") {
+ requireUniqueChildNamed("IBAN") {
+
debtorPayto.append(focusElement.textContent)
+ }
+ }
+ }
+ // warn: it might need the postal address
too..
+ requireUniqueChildNamed("Dbtr") {
+ requireUniqueChildNamed("Nm") {
+
debtorPayto.append("?receiver-name=${focusElement.textContent}")
+ }
+ }
+ }
+ val incomingPayment = IncomingPayment(
+ amount = amount,
+ bankTransferId = uidFromBank,
+ debitPaytoUri = debtorPayto.toString(),
+ executionTime = executionTime,
+ wireTransferSubject = subject.toString()
+ )
+ ret.add(incomingPayment)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return ret
+}
+
+/**
+ * Converts valid reserve pubs to its binary representation.
+ *
+ * @param maybeReservePub input.
+ * @return [ByteArray] or null if not valid.
+ */
+fun isReservePub(maybeReservePub: String): ByteArray? {
+ val dec = try {
+ Base32Crockford.decode(maybeReservePub)
+ } catch (e: EncodingException) {
+ logger.error("Not a reserve pub: $maybeReservePub")
+ return null
+ }
+ logger.debug("Reserve how many bytes: ${dec.size}")
+ if (dec.size != 32) {
+ logger.error("Not a reserve pub, wrong length: ${dec.size}")
+ return null
+ }
+ return dec
+}
+/**
+ * Checks the two conditions that may invalidate one incoming
+ * payment: subject validity and availability.
+ *
+ * @param db database connection.
+ * @param payment incoming payment whose subject is to be checked.
+ * @return [ByteArray] as the reserve public key, or null if the
+ * payment cannot lead to a Taler withdrawal.
+ */
+suspend fun isTalerable(
+ db: Database,
+ payment: IncomingPayment
+): ByteArray? {
+ // Checking validity first.
+ val dec = isReservePub(payment.wireTransferSubject) ?: return null
+ // Now checking availability.
+ val maybeUnavailable = db.isReservePubFound(dec)
+ if (maybeUnavailable) {
+ logger.error("Incoming payment with subject
'${payment.wireTransferSubject}' exists already")
+ return null
+ }
+ return dec
+}
+
+/**
+ * Parses the response of an EBICS notification looking for
+ * incoming payments. As a result, it either creates a Taler
+ * withdrawal or bounces the incoming payment. In detail, this
+ * function extracts the camt.054 from the ZIP archive, invokes
+ * the lower-level camt.054 parser and updates the database.
+ *
+ * @param db database connection.
+ * @param content the ZIP file that contains the EBICS
+ * notification as camt.054 records.
+ * @return true if the ingestion succeeded, false otherwise.
+ * False should fail the process, since it means that
+ * the notification could not be parsed.
+ */
+fun ingestNotification(
+ db: Database,
+ ctx: FetchContext,
+ content: ByteArray
+): Boolean {
+ val incomingPayments = mutableListOf<IncomingPayment>()
+ try {
+ content.unzipForEach { fileName, xmlContent ->
+ // discarding plain "avisierung", since they don't bring any
payment subject.
+ if (!fileName.startsWith("camt.054_P_")) return@unzipForEach
+ val found = findIncomingTxInNotification(xmlContent,
ctx.cfg.currency)
+ incomingPayments += found
+ }
+ } catch (e: IOException) {
+ logger.error("Could not open any ZIP archive")
+ return false
+ } catch (e: Exception) {
+ logger.error(e.message)
+ return false
+ }
+ // Distinguishing now valid and invalid payments.
+ // Any error at this point is only due to Nexus.
+ try {
+ incomingPayments.forEach {
+ runBlocking {
+ val reservePub = isTalerable(db, it)
+ if (reservePub == null) {
+ db.incomingPaymentCreateBounced(
+ it, UUID.randomUUID().toString().take(35)
+ )
+ return@runBlocking
+ }
+ db.incomingTalerablePaymentCreate(it, reservePub)
+ }
+ }
+ } catch (e: Exception) {
+ logger.error(e.message)
+ return false
+ }
+ return true
+}
+
/**
* Fetches the banking records via EBICS notifications requests.
*
@@ -184,14 +417,25 @@ private suspend fun fetchDocuments(
) {
// maybe get last execution_date.
val lastExecutionTime: Instant? = ctx.pinnedStart ?:
db.incomingPaymentLastExecTime()
- logger.debug("Fetching documents from timestamp: $lastExecutionTime")
+ logger.debug("Fetching ${ctx.whichDocument} from timestamp:
$lastExecutionTime")
+ // downloading the content
val maybeContent = downloadHelper(ctx, lastExecutionTime) ?:
exitProcess(1) // client is wrong, failing.
if (maybeContent.isEmpty()) return
+ // logging, if the configuration wants.
maybeLogFile(
ctx.cfg,
maybeContent,
nonZip = ctx.whichDocument == SupportedDocument.PAIN_002_LOGS
)
+ // Parsing the XML: only camt.054 (Detailavisierung) supported currently.
+ if (ctx.whichDocument != SupportedDocument.CAMT_054) {
+ logger.warn("Not parsing ${ctx.whichDocument}. Only camt.054
notifications supported.")
+ return
+ }
+ if (!ingestNotification(db, ctx, maybeContent)) {
+ logger.error("Ingesting notifications failed")
+ exitProcess(1)
+ }
}
class EbicsFetch: CliktCommand("Fetches bank records. Defaults to camt.054
notifications") {
@@ -218,13 +462,15 @@ class EbicsFetch: CliktCommand("Fetches bank records.
Defaults to camt.054 noti
).flag(default = false)
private val onlyLogs by option(
- help = "Downloads only EBICS activity logs via pain.002, only
available to --transient mode. Config needs log directory"
+ help = "Downloads only EBICS activity logs via pain.002," +
+ " only available to --transient mode. Config needs" +
+ " log directory"
).flag(default = false)
private val pinnedStart by option(
- help = "constant YYYY-MM-DD date for the earliest document to download
" +
- "(only consumed in --transient mode). The latest document is
always" +
- " until the current time."
+ help = "constant YYYY-MM-DD date for the earliest document" +
+ " to download (only consumed in --transient mode). The" +
+ " latest document is always until the current time."
)
/**
@@ -252,6 +498,7 @@ class EbicsFetch: CliktCommand("Fetches bank records.
Defaults to camt.054 noti
logger.error("Client private keys not found at:
${cfg.clientPrivateKeysFilename}")
exitProcess(1)
}
+
// Deciding what to download.
var whichDoc = SupportedDocument.CAMT_054
if (onlyAck) whichDoc = SupportedDocument.PAIN_002
@@ -266,7 +513,6 @@ class EbicsFetch: CliktCommand("Fetches bank records.
Defaults to camt.054 noti
bankKeys,
whichDoc
)
-
if (transient) {
logger.info("Transient mode: fetching once and returning.")
val pinnedStartVal = pinnedStart
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt
b/nexus/src/test/kotlin/DatabaseTest.kt
index 9a5cd79e..b8b12c0e 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -1,11 +1,10 @@
import kotlinx.coroutines.runBlocking
-import org.junit.Ignore
import org.junit.Test
import tech.libeufin.nexus.*
import java.time.Instant
+import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertFalse
-import kotlin.test.assertNull
import kotlin.test.assertTrue
@@ -43,7 +42,7 @@ class OutgoingPaymentsTest {
}
}
-@Ignore // enable after having modified the bouncing logic in Kotlin
+// @Ignore // enable after having modified the bouncing logic in Kotlin
class IncomingPaymentsTest {
// Tests creating and bouncing incoming payments in one DB transaction.
@Test
@@ -51,17 +50,21 @@ class IncomingPaymentsTest {
val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
runBlocking {
// creating and bouncing one incoming transaction.
- db.incomingPaymentCreateBounced(
+ assertTrue(db.incomingPaymentCreateBounced(
genIncPay("incoming and bounced"),
"UID"
- )
+ ))
db.runConn {
- // check the bounced flaag is true
+ // Checking one incoming got created
+ val checkIncoming = it.prepareStatement("""
+ SELECT 1 FROM incoming_transactions WHERE
incoming_transaction_id = 1;
+ """).executeQuery()
+ assertTrue(checkIncoming.next())
+ // Checking the bounced table got its row.
val checkBounced = it.prepareStatement("""
- SELECT bounced FROM incoming_transactions WHERE
incoming_transaction_id = 1;
+ SELECT 1 FROM bounced_transactions WHERE
incoming_transaction_id = 1;
""").executeQuery()
assertTrue(checkBounced.next())
- assertTrue(checkBounced.getBoolean("bounced"))
// check the related initiated payment exists.
val checkInitiated = it.prepareStatement("""
SELECT
@@ -74,55 +77,22 @@ class IncomingPaymentsTest {
}
}
- // Tests the function that flags incoming payments as bounced.
+ // Tests the creation of a talerable incoming payment.
@Test
- fun incomingPaymentBounce() {
+ fun incomingTalerableCreation() {
val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
- runBlocking {
- // creating one incoming payment.
- assertTrue(db.incomingPaymentCreate(genIncPay("to be bounced")))
// row ID == 1.
- db.runConn {
- val bouncedSql = """
- SELECT bounced
- FROM incoming_transactions
- WHERE incoming_transaction_id = 1"""
- // asserting is NOT bounced.
- val expectNotBounced = it.execSQLQuery(bouncedSql)
- assertTrue(expectNotBounced.next())
- assertFalse(expectNotBounced.getBoolean("bounced"))
- // now bouncing it.
- assertTrue(db.incomingPaymentSetAsBounced(1, "unique 0"))
- // asserting it got flagged as bounced.
- val expectBounced = it.execSQLQuery(bouncedSql)
- assertTrue(expectBounced.next())
- assertTrue(expectBounced.getBoolean("bounced"))
- // Trying to bounce a non-existing payment.
- assertFalse(db.incomingPaymentSetAsBounced(5, "unique 1"))
- }
- }
- }
+ val reservePub = ByteArray(32)
+ Random.nextBytes(reservePub)
- // Tests the creation of an incoming payment.
- @Test
- fun incomingPaymentCreation() {
- val db = prepDb(TalerConfig(NEXUS_CONFIG_SOURCE))
- val countRows = "SELECT count(*) AS how_many FROM
incoming_transactions"
runBlocking {
- // Asserting the table is empty.
- db.runConn {
- val res = it.execSQLQuery(countRows)
- assertTrue(res.next())
- assertEquals(0, res.getInt("how_many"))
- }
- assertTrue(db.incomingPaymentCreate(genIncPay("singleton")))
- // Asserting the table has one.
- db.runConn {
- val res = it.execSQLQuery(countRows)
- assertTrue(res.next())
- assertEquals(1, res.getInt("how_many"))
- }
- // Checking insertion of null (allowed) subjects.
- assertTrue(db.incomingPaymentCreate(genIncPay()))
+ // Checking the reserve is not found.
+ assertFalse(db.isReservePubFound(reservePub))
+ assertTrue(db.incomingTalerablePaymentCreate(
+ genIncPay("reserve-pub"),
+ reservePub
+ ))
+ // Checking the reserve is not found.
+ assertTrue(db.isReservePubFound(reservePub))
}
}
}
diff --git a/nexus/src/test/kotlin/Parsing.kt b/nexus/src/test/kotlin/Parsing.kt
new file mode 100644
index 00000000..441b05aa
--- /dev/null
+++ b/nexus/src/test/kotlin/Parsing.kt
@@ -0,0 +1,72 @@
+import org.junit.Test
+import org.junit.jupiter.api.assertThrows
+import tech.libeufin.nexus.getTalerAmount
+import tech.libeufin.nexus.isReservePub
+import java.lang.StringBuilder
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+class Parsing {
+ @Test // parses amounts as found in the camt.05x documents.
+ fun parseCurrencyAgnosticAmount() {
+ assertTrue {
+ getTalerAmount("1.00", "KUDOS").run {
+ this.value == 1L && this.fraction == 0 && this.currency ==
"KUDOS"
+ }
+ }
+ assertTrue {
+ getTalerAmount("1", "KUDOS").run {
+ this.value == 1L && this.fraction == 0 && this.currency ==
"KUDOS"
+ }
+ }
+ assertTrue {
+ getTalerAmount("0.99", "KUDOS").run {
+ this.value == 0L && this.fraction == 99000000 && this.currency
== "KUDOS"
+ }
+ }
+ assertTrue {
+ getTalerAmount("0.01", "KUDOS").run {
+ this.value == 0L && this.fraction == 1000000 && this.currency
== "KUDOS"
+ }
+ }
+ assertThrows<Exception> {
+ getTalerAmount("", "")
+ }
+ assertThrows<Exception> {
+ getTalerAmount(".1", "KUDOS")
+ }
+ assertThrows<Exception> {
+ getTalerAmount("1.", "KUDOS")
+ }
+ assertThrows<Exception> {
+ getTalerAmount("0.123456789", "KUDOS")
+ }
+ assertThrows<Exception> {
+ getTalerAmount("noise", "KUDOS")
+ }
+ assertThrows<Exception> {
+ getTalerAmount("1.noise", "KUDOS")
+ }
+ assertThrows<Exception> {
+ getTalerAmount("5", "")
+ }
+ }
+
+ // Checks that the input decodes to a 32-bytes value.
+ @Test
+ fun validateReservePub() {
+ val valid = "4MZT6RS3RVB3B0E2RDMYW0YRA3Y0VPHYV0CYDE6XBB0YMPFXCEG0"
+ val validBytes = isReservePub(valid)
+ assertNotNull(validBytes)
+ assertEquals(32, validBytes.size)
+ assertNull(isReservePub("noise"))
+ val trimmedInput = valid.dropLast(10)
+ assertNull(isReservePub(trimmedInput))
+ val invalidChar = StringBuilder(valid)
+ invalidChar.setCharAt(10, '*')
+ assertNull(isReservePub(invalidChar.toString()))
+ // assertNull(isReservePub(valid.dropLast(1))) // FIXME: this fails
now because the decoder is buggy.
+ }
+}
\ No newline at end of file
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
index c0b85171..dc0ebb5a 100644
--- a/util/src/main/kotlin/time.kt
+++ b/util/src/main/kotlin/time.kt
@@ -20,8 +20,8 @@
package tech.libeufin.util
import java.time.*
+import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
-import java.util.concurrent.TimeUnit
/**
* Converts the 'this' Instant to the number of nanoseconds
@@ -79,4 +79,16 @@ fun Long.microsToJavaInstant(): Instant? {
logger.error(e.message)
return null
}
+}
+
+/**
+ * Parses one timestamp from the ISO 8601 format.
+ *
+ * @param timeFromXml input time string from the XML
+ * @return [Instant] in the UTC timezone
+ */
+fun parseGregorianTime(timeFromXml: String): Instant {
+ val formatter = DateTimeFormatter.ISO_DATE_TIME.parse(timeFromXml)
+ return Instant.from(formatter)
+
}
\ No newline at end of file
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: nexus fetch, adding:,
gnunet <=