gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (71aa5ab6 -> 4f152510)


From: gnunet
Subject: [libeufin] branch master updated (71aa5ab6 -> 4f152510)
Date: Fri, 31 Mar 2023 14:28:04 +0200

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

ms pushed a change to branch master
in repository libeufin.

    from 71aa5ab6 sanity checks
     new 3be48ded matching /cashout/estimates to the API
     new 9fa7dbec Polishing Sandbox.
     new 84a5889d tests
     new 2d45653e Postgres notifications.
     new 9832b269 Constants definition.
     new 59a20971 Taler facade.
     new 4f152510 Nexus x-libeufin-bank connection.

The 7 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .idea/modules.xml                                  |   8 +
 cli/tests/launch_services.sh                       |  13 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt  |   7 +
 .../tech/libeufin/nexus/BankConnectionProtocol.kt  |  26 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt    |  61 +++-
 .../main/kotlin/tech/libeufin/nexus/FacadeUtil.kt  |  75 ++--
 .../main/kotlin/tech/libeufin/nexus/Scheduling.kt  |  10 +-
 nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt | 108 ++----
 .../tech/libeufin/nexus/bankaccount/BankAccount.kt | 362 ++++++++++---------
 .../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 117 +++---
 .../tech/libeufin/nexus/iso20022/Iso20022.kt       | 236 ++++++++++--
 .../kotlin/tech/libeufin/nexus/server/Helpers.kt   |  85 +++++
 .../main/kotlin/tech/libeufin/nexus/server/JSON.kt |  23 +-
 .../tech/libeufin/nexus/server/NexusServer.kt      |   3 +-
 .../nexus/xlibeufinbank/XLibeufinBankNexus.kt      | 395 +++++++++++++++++++++
 nexus/src/test/kotlin/DownloadAndSubmit.kt         |   8 +-
 nexus/src/test/kotlin/MakeEnv.kt                   | 140 ++++++--
 nexus/src/test/kotlin/NexusApiTest.kt              |   4 +-
 nexus/src/test/kotlin/SandboxCircuitApiTest.kt     |  26 +-
 nexus/src/test/kotlin/TalerTest.kt                 | 106 ++++--
 nexus/src/test/kotlin/XLibeufinBankTest.kt         | 111 ++++++
 .../kotlin/tech/libeufin/sandbox/CircuitApi.kt     | 101 ++++--
 .../src/main/kotlin/tech/libeufin/sandbox/DB.kt    |   1 +
 .../tech/libeufin/sandbox/EbicsProtocolBackend.kt  |  12 +-
 .../main/kotlin/tech/libeufin/sandbox/Helpers.kt   |   9 +-
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  |   8 +-
 .../kotlin/tech/libeufin/sandbox/bankAccount.kt    |   2 +
 util/src/main/kotlin/DB.kt                         |  97 ++---
 util/src/main/kotlin/JSON.kt                       |  49 ++-
 util/src/main/kotlin/LibeufinErrorCodes.kt         |   2 +-
 30 files changed, 1661 insertions(+), 544 deletions(-)
 create mode 100644 .idea/modules.xml
 create mode 100644 nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
 create mode 100644 
nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
 create mode 100644 nexus/src/test/kotlin/XLibeufinBankTest.kt

diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 00000000..dbca1434
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/.idea/libeufin.iml" 
filepath="$PROJECT_DIR$/.idea/libeufin.iml" />
+    </modules>
+  </component>
+</project>
\ No newline at end of file
diff --git a/cli/tests/launch_services.sh b/cli/tests/launch_services.sh
index b3fcc4eb..2bee7df7 100755
--- a/cli/tests/launch_services.sh
+++ b/cli/tests/launch_services.sh
@@ -4,8 +4,8 @@
 # EBICS pair, in order to try CLI commands.
 set -eu
 
-WITH_TASKS=1
-# WITH_TASKS=0
+# WITH_TASKS=1
+WITH_TASKS=0
 function exit_cleanup()
 {
   echo "Running exit-cleanup"
@@ -25,13 +25,13 @@ curl --version &> /dev/null || (echo "'curl' command not 
found"; exit 77)
 SQLITE_FILE_PATH=/tmp/libeufin-cli-test.sqlite3
 getDbConn () {
   if test withPostgres == "${1:-}"; then
-    echo "jdbc:postgresql://localhost:5432/taler?user=$(whoami)"
+    echo "jdbc:postgresql://localhost:5432/libeufincheck?user=$(whoami)"
     return
   fi
   echo "jdbc:sqlite:${SQLITE_FILE_PATH}"
 }
 
-DB_CONN=`getDbConn`
+DB_CONN=`getDbConn withPostgres`
 export LIBEUFIN_SANDBOX_DB_CONNECTION=$DB_CONN
 export LIBEUFIN_NEXUS_DB_CONNECTION=$DB_CONN
 
@@ -139,6 +139,9 @@ if test 1 = $WITH_TASKS; then
     www-nexus || true
   echo OK
 else
-  echo NOT creating backound tasks!
+  echo NOT creating background tasks!
 fi
+echo "Requesting Taler history with 90 seconds timeout..."
+curl -u test-user:x 
"http://localhost:5001/facades/test-facade/taler-wire-gateway/history/incoming?delta=5&long_poll_ms=90000";
+
 read -p "Press Enter to terminate..."
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
index bdaa0ec3..2048813e 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Auth.kt
@@ -9,6 +9,13 @@ import tech.libeufin.nexus.server.Permission
 import tech.libeufin.nexus.server.PermissionQuery
 import tech.libeufin.util.*
 
+fun getNexusUser(username: String): NexusUserEntity =
+    transaction {
+        NexusUserEntity.find {
+            NexusUsersTable.username eq username
+        }.firstOrNull() ?: throw notFound("User $username not found.")
+    }
+
 /**
  * HTTP basic auth.  Throws error if password is wrong,
  * and makes sure that the user exists in the system.
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
index 6a18db21..ac52095c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt
@@ -23,11 +23,12 @@ import com.fasterxml.jackson.databind.JsonNode
 import io.ktor.client.HttpClient
 import io.ktor.http.HttpStatusCode
 import tech.libeufin.nexus.ebics.*
+import tech.libeufin.nexus.server.BankConnectionType
 import tech.libeufin.nexus.server.FetchSpecJson
 
 // 'const' allows only primitive types.
-val bankConnectionRegistry: Map<String, BankConnectionProtocol> = mapOf(
-    "ebics" to EbicsBankConnectionProtocol()
+val bankConnectionRegistry: Map<BankConnectionType, BankConnectionProtocol> = 
mapOf(
+    BankConnectionType.EBICS to EbicsBankConnectionProtocol()
 )
 
 interface BankConnectionProtocol {
@@ -64,9 +65,13 @@ interface BankConnectionProtocol {
      *
      * This function returns a possibly empty list of exceptions.
      * That helps not to stop fetching if ONE operation fails.  Notably,
-     * C52 and C53 may be asked along one invocation of this function,
+     * C52 _and_ C53 may be asked along one invocation of this function,
      * therefore storing the exception on C52 allows the C53 to still
      * take place.  The caller then decides how to handle the exceptions.
+     *
+     * More on multi requests: C52 and C53, or more generally 'reports'
+     * and 'statements' are tried to be downloaded together when the fetch
+     * level is set to ALL.
      */
     suspend fun fetchTransactions(
         fetchSpec: FetchSpecJson,
@@ -76,9 +81,18 @@ interface BankConnectionProtocol {
     ): List<Exception>?
 }
 
-fun getConnectionPlugin(connId: String): BankConnectionProtocol {
-    return bankConnectionRegistry.get(connId) ?: throw NexusError(
+fun getConnectionPlugin(connType: BankConnectionType): BankConnectionProtocol {
+    return bankConnectionRegistry[connType] ?: throw NexusError(
         HttpStatusCode.NotFound,
-        "Connection type '${connId}' not available"
+        "Connection type '${connType}' not available"
     )
+}
+
+/**
+ * Adaptor helper to keep until all the connection type mentions will
+ * be passed as BankConnectionType instead of arbitrary easy-to-break
+ * string.
+ */
+fun getConnectionPlugin(connType: String): BankConnectionProtocol {
+    return 
getConnectionPlugin(BankConnectionType.parseBankConnectionType(connType))
 }
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
index f0af4499..38fb7bd3 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt
@@ -19,17 +19,28 @@
 
 package tech.libeufin.nexus
 
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import org.jetbrains.exposed.dao.*
 import org.jetbrains.exposed.dao.id.EntityID
 import org.jetbrains.exposed.dao.id.LongIdTable
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import tech.libeufin.nexus.iso20022.EntryStatus
 import tech.libeufin.util.EbicsInitState
 import java.sql.Connection
+import kotlin.reflect.typeOf
 
 
+enum class EntryStatus {
+    // Booked
+    BOOK,
+    // Pending
+    PDNG,
+    // Informational
+    INFO,
+}
+
 /**
  * This table holds the values that exchange gave to issue a payment,
  * plus a reference to the prepared pain.001 version of.  Note that
@@ -134,15 +145,36 @@ class NexusBankBalanceEntity(id: EntityID<Long>) : 
LongEntity(id) {
     var date by NexusBankBalancesTable.date
 }
 
+// This table holds the data to talk to Sandbox
+// via the x-libeufin-bank protocol supplier.
+object XLibeufinBankUsersTable : LongIdTable() {
+    val username = text("username")
+    val password = text("password")
+    val baseUrl = text("baseUrl")
+    val nexusBankConnection = reference("nexusBankConnection", 
NexusBankConnectionsTable)
+}
+
+class XLibeufinBankUserEntity(id: EntityID<Long>) : LongEntity(id) {
+    companion object : 
LongEntityClass<XLibeufinBankUserEntity>(XLibeufinBankUsersTable)
+    var username by XLibeufinBankUsersTable.username
+    var password by XLibeufinBankUsersTable.password
+    var baseUrl by XLibeufinBankUsersTable.baseUrl
+    var nexusBankConnection by NexusBankConnectionEntity referencedOn 
XLibeufinBankUsersTable.nexusBankConnection
+}
+
 /**
  * Table that stores all messages we receive from the bank.
+ * The nullable fields were introduced along the x-libeufin-bank
+ * connection, as those messages are plain JSON object unlike
+ * the more structured CaMt.
  */
 object NexusBankMessagesTable : LongIdTable() {
     val bankConnection = reference("bankConnection", NexusBankConnectionsTable)
-    val messageId = text("messageId")
-    val code = text("code")
     val message = blob("message")
-    val errors = bool("errors").default(false) // true when the parser could 
not ingest one message.
+    val messageId = text("messageId").nullable()
+    val code = text("code").nullable()
+    // true when the parser could not ingest one message:
+    val errors = bool("errors").default(false)
 }
 
 class NexusBankMessageEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -188,6 +220,21 @@ class NexusBankTransactionEntity(id: EntityID<Long>) : 
LongEntity(id) {
     var transactionJson by NexusBankTransactionsTable.transactionJson
     var accountTransactionId by NexusBankTransactionsTable.accountTransactionId
     val updatedBy by NexusBankTransactionEntity optionalReferencedOn 
NexusBankTransactionsTable.updatedBy
+
+    /**
+     * It is responsibility of the caller to insert only valid
+     * JSON into the database, and therefore provide error management
+     * when calling the two helpers below.
+     */
+
+    inline fun <reified T> parseDetailsIntoObject(): T {
+        val mapper = jacksonObjectMapper()
+        return mapper.readValue(this.transactionJson, T::class.java)
+    }
+    fun parseDetailsIntoObject(): JsonNode {
+        val mapper = jacksonObjectMapper()
+        return mapper.readTree(this.transactionJson)
+    }
 }
 
 /**
@@ -459,9 +506,7 @@ object NexusPermissionsTable : LongIdTable() {
     val subjectId = text("subjectName")
     val permissionName = text("permissionName")
 
-    init {
-        uniqueIndex(resourceType, resourceId, subjectType, subjectId, 
permissionName)
-    }
+    init { uniqueIndex(resourceType, resourceId, subjectType, subjectId, 
permissionName) }
 }
 
 class NexusPermissionEntity(id: EntityID<Long>) : LongEntity(id) {
@@ -479,6 +524,7 @@ fun dbDropTables(dbConnectionString: String) {
     transaction {
         SchemaUtils.drop(
             NexusUsersTable,
+            XLibeufinBankUsersTable,
             PaymentInitiationsTable,
             NexusEbicsSubscribersTable,
             NexusBankAccountsTable,
@@ -504,6 +550,7 @@ fun dbCreateTables(dbConnectionString: String) {
     TransactionManager.manager.defaultIsolationLevel = 
Connection.TRANSACTION_SERIALIZABLE
     transaction {
         SchemaUtils.create(
+            XLibeufinBankUsersTable,
             NexusScheduledTasksTable,
             NexusUsersTable,
             PaymentInitiationsTable,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
index 7fdd2c26..c908a828 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/FacadeUtil.kt
@@ -2,47 +2,60 @@ package tech.libeufin.nexus
 
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.http.*
+import org.jetbrains.exposed.dao.flushCache
 import org.jetbrains.exposed.sql.SortOrder
 import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
 import tech.libeufin.nexus.iso20022.CreditDebitIndicator
-import tech.libeufin.nexus.iso20022.EntryStatus
 import tech.libeufin.nexus.iso20022.TransactionDetails
+import tech.libeufin.nexus.server.NexusFacadeType
 
-
-/**
- * Mainly used to resort the last processed transaction ID.
- */
+// Mainly used to resort the last processed transaction ID.
 fun getFacadeState(fcid: String): FacadeStateEntity {
-    val facade = FacadeEntity.find { FacadesTable.facadeName eq fcid 
}.firstOrNull() ?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Could not find facade '${fcid}'"
-    )
-    return FacadeStateEntity.find {
-        FacadeStateTable.facade eq facade.id.value
-    }.firstOrNull() ?: throw NexusError(
-        HttpStatusCode.NotFound,
-        "Could not find any state for facade: $fcid"
-    )
+    return transaction {
+        val facade = FacadeEntity.find {
+            FacadesTable.facadeName eq fcid
+        }.firstOrNull() ?: throw NexusError(
+            HttpStatusCode.NotFound,
+            "Could not find facade '${fcid}'"
+        )
+        FacadeStateEntity.find {
+            FacadeStateTable.facade eq facade.id.value
+        }.firstOrNull() ?: throw NexusError(
+            HttpStatusCode.NotFound,
+            "Could not find any state for facade: $fcid"
+        )
+    }
 }
 
 fun getFacadeBankAccount(fcid: String): NexusBankAccountEntity {
-    val facadeState = getFacadeState(fcid)
-    return NexusBankAccountEntity.findByName(facadeState.bankAccount) ?: throw 
NexusError(
-        HttpStatusCode.NotFound,
-        "The facade: $fcid doesn't manage bank account: 
${facadeState.bankAccount}"
-    )
+    return transaction {
+        val facadeState = getFacadeState(fcid)
+        NexusBankAccountEntity.findByName(facadeState.bankAccount) ?: throw 
NexusError(
+            HttpStatusCode.NotFound,
+            "The facade: $fcid doesn't manage bank account: 
${facadeState.bankAccount}"
+        )
+    }
 }
 
 /**
  * Ingests transactions for those facades accounting for bankAccountId.
+ * 'incomingFilterCb' decides whether the facade accepts the payment;
+ * if not, refundCb prepares a refund.  The 'txStatus' parameter decides
+ * at which state one transaction deserve to fuel Taler transactions. BOOK
+ * is conservative, and with some banks the delay can be significant.  PNDG
+ * instead reacts faster, but risks that one transaction gets undone by the
+ * bank and never reach the BOOK state; this would mean a loss and/or admin
+ * burden.
  */
 fun ingestFacadeTransactions(
     bankAccountId: String,
-    facadeType: String,
+    facadeType: NexusFacadeType,
     incomingFilterCb: ((NexusBankTransactionEntity, TransactionDetails) -> 
Unit)?,
-    refundCb: ((NexusBankAccountEntity, Long) -> Unit)?
+    refundCb: ((NexusBankAccountEntity, Long) -> Unit)?,
+    txStatus: EntryStatus = EntryStatus.BOOK
 ) {
     fun ingest(bankAccount: NexusBankAccountEntity, facade: FacadeEntity) {
         logger.debug(
@@ -55,18 +68,21 @@ fun ingestFacadeTransactions(
             /** Those with "our" bank account involved */
             NexusBankTransactionsTable.bankAccount eq bankAccount.id.value and
                     /** Those that are booked */
-                    (NexusBankTransactionsTable.status eq EntryStatus.BOOK) and
+                    (NexusBankTransactionsTable.status eq txStatus) and
                     /** Those that came later than the latest processed 
payment */
                     (NexusBankTransactionsTable.id.greater(lastId))
         }.orderBy(Pair(NexusBankTransactionsTable.id, SortOrder.ASC)).forEach {
             // Incoming payment.
-            logger.debug("Facade checks payment: ${it.transactionJson}")
             val tx = jacksonObjectMapper().readValue(
-                it.transactionJson, CamtBankAccountEntry::class.java
+                it.transactionJson,
+                CamtBankAccountEntry::class.java
             )
-            val details = 
tx.batches?.get(0)?.batchTransactions?.get(0)?.details
+            /**
+             * Need transformer from "JSON tx" to TransactionDetails?.
+             */
+            val details: TransactionDetails? = 
tx.batches?.get(0)?.batchTransactions?.get(0)?.details
             if (details == null) {
-                logger.warn("A void money movement made it through the 
ingestion: VERY strange")
+                logger.warn("A void money movement (${tx.accountServicerRef}) 
made it through the ingestion: VERY strange")
                 return@forEach
             }
             when (tx.creditDebitIndicator) {
@@ -90,16 +106,17 @@ fun ingestFacadeTransactions(
                 )
             }
         } catch (e: Exception) {
-            logger.warn("sending refund payment failed", e)
+            logger.warn("Sending refund payment failed: ${e.message}")
         }
         facadeState.highestSeenMessageSerialId = lastId
     }
     // invoke ingestion for all the facades
     transaction {
-        FacadeEntity.find { FacadesTable.type eq facadeType }.forEach {
+        FacadeEntity.find { FacadesTable.type eq facadeType.facadeType 
}.forEach {
             val facadeBankAccount = getFacadeBankAccount(it.facadeName)
             if (facadeBankAccount.bankAccountName == bankAccountId)
                 ingest(facadeBankAccount, it)
+            flushCache()
         }
     }
 }
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
index 86c86a37..8d6062d1 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Scheduling.kt
@@ -59,7 +59,15 @@ private suspend fun runTask(client: HttpClient, sched: 
TaskSchedule) {
                     "fetch" -> {
                         @Suppress("BlockingMethodInNonBlockingContext")
                         val fetchSpec = 
jacksonObjectMapper().readValue(sched.params, FetchSpecJson::class.java)
-                        fetchBankAccountTransactions(client, fetchSpec, 
sched.resourceId)
+                        val outcome = fetchBankAccountTransactions(client, 
fetchSpec, sched.resourceId)
+                        if (outcome.errors != null && 
outcome.errors!!.isNotEmpty()) {
+                            /**
+                             * Communication with the bank had at least one 
error.  All of
+                             * them get logged when this 'outcome.errors' list 
was defined,
+                             * so not logged twice here.  Failing to bring the 
problem(s) up.
+                             */
+                            exitProcess(1)
+                        }
                     }
                     /**
                      * Submits the payment preparations that are found in the 
database.
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
index 4b325def..365a4ea5 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt
@@ -19,7 +19,6 @@
 
 package tech.libeufin.nexus
 
-import UtilError
 import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.server.application.ApplicationCall
 import io.ktor.server.application.call
@@ -34,15 +33,15 @@ import io.ktor.server.routing.Route
 import io.ktor.server.routing.get
 import io.ktor.server.routing.post
 import io.ktor.server.util.*
-import io.ktor.util.*
-import kotlinx.coroutines.*
-import net.taler.wallet.crypto.Base32Crockford
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.currentCoroutineContext
 import org.jetbrains.exposed.dao.Entity
 import org.jetbrains.exposed.dao.id.IdTable
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import org.postgresql.jdbc.PgConnection
 import tech.libeufin.nexus.bankaccount.addPaymentInitiation
 import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
 import tech.libeufin.nexus.bankaccount.getBankAccount
@@ -50,8 +49,6 @@ import tech.libeufin.nexus.iso20022.*
 import tech.libeufin.nexus.server.*
 import tech.libeufin.util.*
 import java.net.URL
-import java.util.concurrent.atomic.AtomicReference
-import javax.xml.crypto.Data
 import kotlin.math.abs
 import kotlin.math.min
 
@@ -245,10 +242,12 @@ private suspend fun talerTransfer(call: ApplicationCall) {
     )
 }
 
+// Processes new transactions and stores TWG-specific data in
 fun talerFilter(
     payment: NexusBankTransactionEntity,
     txDtls: TransactionDetails
 ) {
+    val channelsToNotify = mutableListOf<String>()
     var isInvalid = false // True when pub is invalid or duplicate.
     val subject = txDtls.unstructuredRemittanceInformation
     val debtorName = txDtls.debtor?.name
@@ -324,11 +323,6 @@ fun talerFilter(
         HttpStatusCode.InternalServerError,
         "talerFilter(): unexpected execution out of a DB transaction"
     )
-    /**
-     * Without COMMIT here, the woken up LISTENer won't
-     * find the record in the database.
-     */
-    dbTx.commit()
     // Only supporting Postgres' NOTIFY.
     if (dbTx.isPostgres()) {
         val channelName = buildChannelName(
@@ -339,11 +333,7 @@ fun talerFilter(
                 " ${NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING}" +
                 " for IBAN: ${payment.bankAccount.iban}.  Resulting channel" +
                 " name: $channelName.")
-        val notifyHandle = PostgresListenNotify(
-            dbTx.getPgConnection(),
-            channelName
-        )
-        notifyHandle.postgresNotify()
+        dbTx.postgresNotify(channelName)
     }
 }
 
@@ -505,75 +495,53 @@ private suspend fun historyIncoming(call: 
ApplicationCall) {
     val start: Long = 
handleStartArgument(call.request.queryParameters["start"], delta)
     val history = TalerIncomingHistory()
     val startCmpOp = getComparisonOperator(delta, start, 
TalerIncomingPaymentsTable)
+    val listenHandle: PostgresListenHandle? = if (isPostgres() && 
longPollTimeout != null) {
+        val notificationChannelName = buildChannelName(
+            NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING,
+            getFacadeBankAccount(facadeId).iban
+        )
+        val handle = PostgresListenHandle(channelName = 
notificationChannelName)
+        handle.postgresListen()
+        handle
+    } else null
+
     /**
-     * The following block checks first for results, and then LISTEN
-     * _only if_ the client gave the long_poll_ms parameter.
+     * NOTE: the LISTEN command MAY also go inside this transaction,
+     * but that uses a connection other than the one provided by the
+     * transaction block.  More facts on the consequences are needed.
      */
-    var resultOrWait: Pair<
-            List<TalerIncomingPaymentEntity>,
-            PostgresListenNotify?
-            > = transaction {
-        val res = TalerIncomingPaymentEntity.find { startCmpOp 
}.orderTaler(delta)
-        // Register to Postgres notifications, if no results arrived.
-        if (res.isEmpty() && this.isPostgres() && longPollTimeout != null) {
-            // Getting the IBAN to build the unique channel name.
-            val f = FacadeEntity.find { FacadesTable.facadeName eq facadeId 
}.firstOrNull()
-            if (f == null) throw internalServerError(
-                "Handling request for facade '$facadeId', but that's not found 
in the database."
-            )
-            val fState = FacadeStateEntity.find {
-                FacadeStateTable.facade eq f.id.value
-            }.firstOrNull()
-            if (fState == null) throw internalServerError(
-                "Facade '$facadeId' exist but has no state."
-            )
-            val bankAccount = getBankAccount(fState.bankAccount)
-            val channelName = buildChannelName(
-                NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING,
-                bankAccount.iban
-            )
-            logger.debug("LISTENing on domain " +
-                    "${NotificationsChannelDomains.LIBEUFIN_TALER_INCOMING}" +
-                    " for IBAN: ${bankAccount.iban} with timeout: 
$longPollTimeoutPar." +
-                    " Resulting channel name: $channelName"
-            )
-            val listenHandle = PostgresListenNotify(
-                this.getPgConnection(),
-                channelName
-            )
-            listenHandle.postrgesListen()
-            return@transaction Pair(res, listenHandle)
-        }
-        Pair(res, null)
+    var result: List<TalerIncomingPaymentEntity> = transaction {
+        TalerIncomingPaymentEntity.find { startCmpOp }.orderTaler(delta)
     }
-    /**
-     * Wait here by releasing the execution, or proceed to response if didn't 
sleep.
-     * The right condition only silences the compiler, because when the 
timeout is null
-     * the left condition is always false (no listen-notify object.)
-     */
-    if (resultOrWait.second != null && longPollTimeout != null) {
-        logger.debug("Waiting for NOTIFY, with timeout: $longPollTimeoutPar 
ms")
-        val listenHandle = resultOrWait.second!!
-        val notificationArrived = 
listenHandle.postgresWaitNotification(longPollTimeout)
+    if (result.isNotEmpty() && listenHandle != null)
+        listenHandle.postgresUnlisten()
+
+    if (result.isEmpty() && listenHandle != null && longPollTimeout != null) {
+        logger.debug("Waiting for NOTIFY on channel 
${listenHandle.channelName}," +
+                " with timeout: $longPollTimeoutPar ms")
+        val notificationArrived = coroutineScope {
+            async(Dispatchers.IO) {
+                listenHandle.postgresGetNotifications(longPollTimeout)
+            }.await()
+        }
         if (notificationArrived) {
-            val likelyNewPayments = transaction {
-                // addLogger(StdOutSqlLogger)
-                TalerIncomingPaymentEntity.find { startCmpOp 
}.orderTaler(delta)
-            }
             /**
              * NOTE: the query can still have zero results despite the
              * notification.  That happens when the 'start' URI param is
              * higher than the ID of the new row in the database.  Not
              * an error.
              */
-            resultOrWait = Pair(likelyNewPayments, null)
+            result = transaction {
+                // addLogger(StdOutSqlLogger)
+                TalerIncomingPaymentEntity.find { startCmpOp 
}.orderTaler(delta)
+            }
         }
     }
     /**
      * Whether because of a timeout or a notification or of never slept, here 
it
      * proceeds to the response (== resultOrWait.first IS EFFECTIVE).
      */
-    val maybeNewPayments = resultOrWait.first
+    val maybeNewPayments = result
     if (maybeNewPayments.isNotEmpty()) {
         transaction {
             maybeNewPayments.subList(
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index c71e5531..7843345f 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -24,20 +24,37 @@ import io.ktor.server.application.ApplicationCall
 import io.ktor.client.HttpClient
 import io.ktor.http.HttpStatusCode
 import org.jetbrains.exposed.sql.*
+import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
 import org.jetbrains.exposed.sql.transactions.transaction
-import org.w3c.dom.Document
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.iso20022.*
-import tech.libeufin.nexus.server.FetchSpecJson
-import tech.libeufin.nexus.server.Pain001Data
-import tech.libeufin.nexus.server.requireBankConnection
-import tech.libeufin.nexus.server.toPlainString
+import tech.libeufin.nexus.server.*
+import tech.libeufin.nexus.xlibeufinbank.processXLibeufinBankMessage
 import tech.libeufin.util.XMLUtil
+import tech.libeufin.util.internalServerError
 import java.time.Instant
+import java.time.ZoneOffset
 import java.time.ZonedDateTime
-import java.time.format.DateTimeFormatter
 
 private val keepBankMessages: String? = 
System.getenv("LIBEUFIN_NEXUS_KEEP_BANK_MESSAGES")
+
+/**
+ * Gets a prepared payment starting from its 'payment information id'.
+ * Note: although the terminology comes from CaMt, a 'payment information id'
+ * is indeed any UID that identifies the payment.  For this reason, also
+ * the x-libeufin-bank logic uses this helper.
+ *
+ * Returns the prepared payment, or null if that's not found.  Not throwing
+ * any exception because the null case is common: not every transaction being
+ * processed by Neuxs was prepared/initiated here; incoming transactions are
+ * one example.
+ */
+fun getPaymentInitiation(pmtInfId: String): PaymentInitiationEntity? =
+    transaction {
+        PaymentInitiationEntity.find(
+            PaymentInitiationsTable.paymentInformationId.eq(pmtInfId)
+        ).firstOrNull()
+    }
 fun requireBankAccount(call: ApplicationCall, parameterKey: String): 
NexusBankAccountEntity {
     val name = call.parameters[parameterKey]
     if (name == null)
@@ -63,6 +80,7 @@ suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId:
             val submitted = paymentInitiation.submitted
         }
     }
+    // Skips, if the payment was sent once already.
     if (r.submitted) {
         return
     }
@@ -75,10 +93,11 @@ suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId:
 /**
  * Submit all pending prepared payments.
  */
-suspend fun submitAllPaymentInitiations(httpClient: HttpClient, accountid: 
String) {
-    data class Submission(
-        val id: Long
-    )
+suspend fun submitAllPaymentInitiations(
+    httpClient: HttpClient,
+    accountid: String
+) {
+    data class Submission(val id: Long)
     val workQueue = mutableListOf<Submission>()
     transaction {
         val account = NexusBankAccountEntity.findByName(accountid) ?: throw 
NexusError(
@@ -119,25 +138,11 @@ suspend fun submitAllPaymentInitiations(httpClient: 
HttpClient, accountid: Strin
     }
 }
 
-/**
- * Check if the transaction is already found in the database.
- */
-private fun findDuplicate(bankAccountId: String, acctSvcrRef: String): 
NexusBankTransactionEntity? {
-    // FIXME: make this generic depending on transaction identification scheme
-    val ati = "AcctSvcrRef:$acctSvcrRef"
-    return transaction {
-        val account = NexusBankAccountEntity.findByName((bankAccountId)) ?: 
return@transaction null
-        NexusBankTransactionEntity.find {
-            (NexusBankTransactionsTable.accountTransactionId eq ati) and 
(NexusBankTransactionsTable.bankAccount eq account.id)
-        }.firstOrNull()
-    }
-}
-
 /**
  * NOTE: this type can be used BOTH for one Camt document OR
  * for a set of those.
  */
-data class CamtTransactionsCount(
+data class IngestedTransactionsCount(
     /**
      * Number of transactions that are new to the database.
      * Note that transaction T can be downloaded multiple times;
@@ -155,154 +160,31 @@ data class CamtTransactionsCount(
     /**
      * Exceptions occurred while fetching transactions.  Fetching
      * transactions can be done via multiple EBICS messages, therefore
-     * a failing one should not prevent other messages to be sent.
-     * This list collects all the exceptions that happened during the
-     * execution of a batch of messages.
+     * a failing one should not prevent other messages to be fetched.
+     * This list collects all the exceptions that happened while fetching
+     * multiple messages.
      */
     var errors: List<Exception>? = null
 )
 
 /**
- * Get the Camt parsed by a helper function, discards duplicates
- * and stores new transactions.
- */
-fun processCamtMessage(
-    bankAccountId: String, camtDoc: Document, code: String
-): CamtTransactionsCount {
-    var newTransactions = 0
-    var downloadedTransactions = 0
-    transaction {
-        val acct = NexusBankAccountEntity.findByName(bankAccountId)
-        if (acct == null) {
-            throw NexusError(HttpStatusCode.NotFound, "user not found")
-        }
-        val res = try {
-            parseCamtMessage(camtDoc)
-        } catch (e: CamtParsingError) {
-            logger.warn("Invalid CAMT received from bank: $e")
-            newTransactions = -1
-            return@transaction
-        }
-        res.reports.forEach {
-            NexusAssert(
-                it.account.iban == acct.iban,
-                "Nexus hit a report or statement of a wrong IBAN!"
-            )
-            it.balances.forEach { b ->
-                if (b.type == "CLBD") {
-                    val lastBalance = NexusBankBalanceEntity.all().lastOrNull()
-                    /**
-                     * Store balances different from the one that came from 
the bank,
-                     * or the very first balance.  This approach has the 
following inconvenience:
-                     * the 'balance' held at Nexus does not differentiate 
between one
-                     * coming from a statement and one coming from a report.  
As a consequence,
-                     * the two types of balances may override each other 
without notice.
-                     */
-                    if ((lastBalance == null) ||
-                        (b.amount.toPlainString() != lastBalance.balance)) {
-                        NexusBankBalanceEntity.new {
-                            bankAccount = acct
-                            balance = b.amount.toPlainString()
-                            creditDebitIndicator = b.creditDebitIndicator.name
-                            date = b.date
-                        }
-                    }
-                }
-            }
-        }
-        /**
-         * Why is the report/statement creation timestamp important,
-         * rather than each individual payment identification value?
-         */
-        val stamp =
-            ZonedDateTime.parse(res.creationDateTime, 
DateTimeFormatter.ISO_DATE_TIME).toInstant().toEpochMilli()
-        when (code) {
-            "C52" -> {
-                val s = acct.lastReportCreationTimestamp
-                if (s != null && stamp > s) {
-                    acct.lastReportCreationTimestamp = stamp
-                }
-            }
-            "C53" -> {
-                val s = acct.lastStatementCreationTimestamp
-                if (s != null && stamp > s) {
-                    acct.lastStatementCreationTimestamp = stamp
-                }
-            }
-        }
-        val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries 
}.flatten()
-        var newPaymentsLog = ""
-        downloadedTransactions = entries.size
-        txloop@ for (entry in entries) {
-            val singletonBatchedTransaction = 
entry.batches?.get(0)?.batchTransactions?.get(0)
-                ?: throw NexusError(
-                    HttpStatusCode.InternalServerError,
-                    "Singleton money movements policy wasn't respected"
-                )
-            val acctSvcrRef = entry.accountServicerRef
-            if (acctSvcrRef == null) {
-                // FIXME(dold): Report this!
-                logger.error("missing account servicer reference in 
transaction")
-                continue
-            }
-            val duplicate = findDuplicate(bankAccountId, acctSvcrRef)
-            if (duplicate != null) {
-                logger.info("Found a duplicate (acctSvcrRef): $acctSvcrRef")
-                // FIXME(dold): See if an old transaction needs to be 
superseded by this one
-                // https://bugs.gnunet.org/view.php?id=6381
-                continue@txloop
-            }
-            val rawEntity = NexusBankTransactionEntity.new {
-                bankAccount = acct
-                accountTransactionId = "AcctSvcrRef:$acctSvcrRef"
-                amount = singletonBatchedTransaction.amount.value
-                currency = singletonBatchedTransaction.amount.currency
-                transactionJson = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
-                creditDebitIndicator = 
singletonBatchedTransaction.creditDebitIndicator.name
-                status = entry.status
-            }
-            rawEntity.flush()
-            newTransactions++
-            newPaymentsLog += "\n- " + 
entry.batches[0].batchTransactions[0].details.unstructuredRemittanceInformation
-            // This block tries to acknowledge a former outgoing payment as 
booked.
-            if (singletonBatchedTransaction.creditDebitIndicator == 
CreditDebitIndicator.DBIT) {
-                val t0 = singletonBatchedTransaction.details
-                val pmtInfId = t0.paymentInformationId
-                if (pmtInfId != null) {
-                    val paymentInitiation = PaymentInitiationEntity.find {
-                        PaymentInitiationsTable.bankAccount eq acct.id and (
-                                PaymentInitiationsTable.paymentInformationId 
eq pmtInfId)
-
-                    }.firstOrNull()
-                    if (paymentInitiation != null) {
-                        logger.info("Could confirm one initiated payment: 
$pmtInfId")
-                        paymentInitiation.confirmationTransaction = rawEntity
-                    }
-                }
-            }
-        }
-        if (newTransactions > 0)
-            logger.debug("Camt $code '${res.messageId}' has new 
payments:${newPaymentsLog}")
-    }
-    return CamtTransactionsCount(
-        newTransactions = newTransactions,
-        downloadedTransactions = downloadedTransactions
-    )
-}
-
-/**
- * Create new transactions for an account based on bank messages it
- * did not see before.
+ * Causes new Nexus transactions to be stored into the database.  Note:
+ * this function does NOT parse itself the banking data but relies on the
+ * dedicated helpers.  This function is mostly responsible for _iterating_
+ * over the new downloaded messages and update the local bank account about
+ * the new data.
  */
 fun ingestBankMessagesIntoAccount(
     bankConnectionId: String,
     bankAccountId: String
-): CamtTransactionsCount {
+): IngestedTransactionsCount {
     var totalNew = 0
     var downloadedTransactions = 0
     transaction {
         val conn =
-            NexusBankConnectionEntity.find { 
NexusBankConnectionsTable.connectionId eq bankConnectionId }.firstOrNull()
+            NexusBankConnectionEntity.find {
+                NexusBankConnectionsTable.connectionId eq bankConnectionId
+            }.firstOrNull()
         if (conn == null) {
             throw NexusError(HttpStatusCode.InternalServerError, "connection 
not found")
         }
@@ -311,15 +193,55 @@ fun ingestBankMessagesIntoAccount(
             throw NexusError(HttpStatusCode.InternalServerError, "account not 
found")
         }
         var lastId = acct.highestSeenBankMessageSerialId
+        /**
+         * This block picks all the new messages that were downloaded
+         * from the bank and passes them to the deeper banking data handlers
+         * according to the connection type.  Such handlers are then 
responsible
+         * to extract the interesting values and insert them into the database.
+         */
         NexusBankMessageEntity.find {
             (NexusBankMessagesTable.bankConnection eq conn.id) and
                     (NexusBankMessagesTable.id greater 
acct.highestSeenBankMessageSerialId) and
-                    // Wrong messages got already skipped by the
-                    // index check above.  Below is a extra check.
                     not(NexusBankMessagesTable.errors)
-        }.orderBy(Pair(NexusBankMessagesTable.id, SortOrder.ASC)).forEach {
-            val doc = 
XMLUtil.parseStringIntoDom(it.message.bytes.toString(Charsets.UTF_8))
-            val processingResult = processCamtMessage(bankAccountId, doc, 
it.code)
+        }.orderBy(
+            Pair(NexusBankMessagesTable.id, SortOrder.ASC)
+        ).forEach {
+            val processingResult: IngestedTransactionsCount = 
when(BankConnectionType.parseBankConnectionType(conn.type)) {
+                BankConnectionType.EBICS -> {
+                    val doc = 
XMLUtil.parseStringIntoDom(it.message.bytes.toString(Charsets.UTF_8))
+                    /**
+                     * Calling the CaMt handler.  After its return, all the 
Neuxs-meaningful
+                     * payment data got stored into the database and is ready 
to being further
+                     * processed by any facade OR simply be communicated to 
the CLI via JSON.
+                     */
+                    processCamtMessage(
+                        bankAccountId,
+                        doc,
+                        it.code ?: throw internalServerError(
+                            "Bank message with ID ${it.id.value} in DB table" +
+                                    " NexusBankMessagesTable has no code, but 
one is expected."
+                        )
+                    )
+                }
+                BankConnectionType.X_LIBEUFIN_BANK -> {
+                    val jMessage = try { 
jacksonObjectMapper().readTree(it.message.bytes) }
+                    catch (e: Exception) {
+                        logger.error("Bank message ${it.id}/${it.messageId} 
could not" +
+                                " be parsed into JSON by the x-libeufin-bank 
ingestion.")
+                        throw internalServerError("Could not ingest 
x-libeufin-bank messages.")
+                    }
+                    processXLibeufinBankMessage(
+                        bankAccountId,
+                        jMessage
+                    )
+                }
+            }
+            /**
+             * Checking for errors.  Note: errors do NOT stop this loop as
+             * they mean that ONE message has errors.  Erroneous messages gets
+             * (1) flagged, (2) skipped when this function will run again, and 
(3)
+             * NEVER deleted from the database.
+             */
             if (processingResult.newTransactions == -1) {
                 it.errors = true
                 lastId = it.id.value
@@ -336,12 +258,18 @@ fun ingestBankMessagesIntoAccount(
                 it.delete()
                 return@forEach
             }
+            /**
+             * Updating the highest seen message ID with the serial ID of
+             * the row that's being currently iterated over.  Note: this
+             * number is ever-growing REGARDLESS of the row being kept into
+             * the database.
+             */
             lastId = it.id.value
         }
+        // Causing the lastId to be stored into the database:
         acct.highestSeenBankMessageSerialId = lastId
     }
-    // return totalNew
-    return CamtTransactionsCount(
+    return IngestedTransactionsCount(
         newTransactions = totalNew,
         downloadedTransactions = downloadedTransactions
     )
@@ -358,6 +286,31 @@ fun getPaymentInitiation(uuid: Long): 
PaymentInitiationEntity {
         "Payment '$uuid' not found"
     )
 }
+
+data class LastMessagesTimes(
+    val lastStatement: ZonedDateTime?,
+    val lastReport: ZonedDateTime?
+)
+/**
+ * Get the last timestamps where a report and
+ * a statement were received for the bank account
+ * given as argument.
+ */
+fun getLastMessagesTimes(bankAccountId: String): LastMessagesTimes {
+    val acct = getBankAccount(bankAccountId)
+    return getLastMessagesTimes(acct)
+}
+
+fun getLastMessagesTimes(acct: NexusBankAccountEntity): LastMessagesTimes {
+    return LastMessagesTimes(
+        lastReport = acct.lastReportCreationTimestamp?.let {
+            ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
+        },
+        lastStatement = acct.lastStatementCreationTimestamp?.let {
+            ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), ZoneOffset.UTC)
+        }
+    )
+}
 fun getBankAccount(label: String): NexusBankAccountEntity {
     val maybeBankAccount = transaction {
         NexusBankAccountEntity.findByName(label)
@@ -405,9 +358,11 @@ fun addPaymentInitiation(paymentData: Pain001Data, 
debtorAccount: NexusBankAccou
 }
 
 suspend fun fetchBankAccountTransactions(
-    client: HttpClient, fetchSpec: FetchSpecJson, accountId: String
-): CamtTransactionsCount {
-    val res = transaction {
+    client: HttpClient,
+    fetchSpec: FetchSpecJson,
+    accountId: String
+): IngestedTransactionsCount {
+    val connectionDetails = transaction {
         val acct = NexusBankAccountEntity.findByName(accountId)
         if (acct == null) {
             throw NexusError(
@@ -423,7 +378,12 @@ suspend fun fetchBankAccountTransactions(
             )
         }
         return@transaction object {
-            val connectionType = conn.type
+            /**
+             * The connection type _as enum_ should eventually come
+             * directly from the database, instead of being parsed by
+             * parseBankConnectionType().
+             */
+            val connectionType = 
BankConnectionType.parseBankConnectionType(conn.type)
             val connectionName = conn.connectionId
         }
     }
@@ -432,16 +392,39 @@ suspend fun fetchBankAccountTransactions(
      * document into the database.  This function tries to download
      * both reports AND statements even if the first one fails.
      */
-    val errors: List<Exception>? = 
getConnectionPlugin(res.connectionType).fetchTransactions(
+    val errors: List<Exception>? = 
getConnectionPlugin(connectionDetails.connectionType).fetchTransactions(
         fetchSpec,
         client,
-        res.connectionName,
+        connectionDetails.connectionName,
         accountId
     )
 
-    val ingestionResult = ingestBankMessagesIntoAccount(res.connectionName, 
accountId)
-    ingestFacadeTransactions(accountId, "taler-wire-gateway", ::talerFilter, 
::maybeTalerRefunds)
-    ingestFacadeTransactions(accountId, "anastasis", ::anastasisFilter, null)
+    /**
+     * This block causes new NexusBankAccountTransactions rows to be
+     * INSERTed into the database, according to the banking data that
+     * was recently downloaded.
+     */
+    val ingestionResult: IngestedTransactionsCount = 
ingestBankMessagesIntoAccount(
+        connectionDetails.connectionName,
+        accountId
+    )
+    /**
+     * The following two functions further processe the banking data
+     * that was recently downloaded, according to the particular facade
+     * being honored.
+     */
+    ingestFacadeTransactions(
+        bankAccountId = accountId,
+        facadeType = NexusFacadeType.TALER,
+        incomingFilterCb = ::talerFilter,
+        refundCb = ::maybeTalerRefunds
+    )
+    ingestFacadeTransactions(
+        bankAccountId = accountId,
+        facadeType = NexusFacadeType.ANASTASIS,
+        incomingFilterCb = ::anastasisFilter,
+        refundCb = null
+    )
 
     ingestionResult.errors = errors
     return ingestionResult
@@ -498,4 +481,29 @@ fun importBankAccount(call: ApplicationCall, 
offeredBankAccountId: String, nexus
             }
         }
     }
-}
\ No newline at end of file
+}
+
+
+/**
+ * Check if the transaction is already found in the database.
+ * This function works as long as the caller provides the appropriate
+ * 'uid' parameter.  For CaMt messages this value is carried along
+ * the AcctSvcrRef node, whereas for x-libeufin-bank connections
+ * that's the 'uid' field of the XLibeufinBankTransaction type.
+ *
+ * Returns the transaction that's already in the database, in case
+ * the 'uid' is from a duplicate.
+ */
+fun findDuplicate(
+    bankAccountId: String,
+    uid: String
+): NexusBankTransactionEntity? {
+    return transaction {
+        val account = NexusBankAccountEntity.findByName((bankAccountId)) ?:
+        return@transaction null
+        NexusBankTransactionEntity.find {
+            (NexusBankTransactionsTable.accountTransactionId eq uid) and
+                    (NexusBankTransactionsTable.bankAccount eq account.id)
+        }.firstOrNull()
+    }
+}
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index 209c9384..f41a3729 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -44,9 +44,9 @@ import org.jetbrains.exposed.sql.and
 import org.jetbrains.exposed.sql.insert
 import org.jetbrains.exposed.sql.select
 import org.jetbrains.exposed.sql.statements.api.ExposedBlob
-import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.nexus.*
+import tech.libeufin.nexus.bankaccount.getLastMessagesTimes
 import tech.libeufin.nexus.bankaccount.getPaymentInitiation
 import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData
 import tech.libeufin.nexus.iso20022.createPain001document
@@ -149,6 +149,10 @@ private suspend fun fetchEbicsC5x(
     }
 }
 
+/**
+ * Prepares key material and other EBICS details and
+ * returns them along a convenient object.
+ */
 private fun getEbicsSubscriberDetailsInternal(subscriber: 
EbicsSubscriberEntity): EbicsClientSubscriberDetails {
     var bankAuthPubValue: RSAPublicKey? = null
     if (subscriber.bankAuthenticationPublicKey != null) {
@@ -178,18 +182,19 @@ private fun getEbicsSubscriberDetailsInternal(subscriber: 
EbicsSubscriberEntity)
         ebicsHiaState = subscriber.ebicsHiaState
     )
 }
-
+private fun getSubscriberFromConnection(connectionEntity: 
NexusBankConnectionEntity): EbicsSubscriberEntity =
+    transaction {
+        EbicsSubscriberEntity.find {
+            NexusEbicsSubscribersTable.nexusBankConnection eq 
connectionEntity.id
+        }.firstOrNull() ?: throw internalServerError("ebics bank connection 
'${connectionEntity.connectionId}' has no subscriber.")
+    }
 /**
  * Retrieve Ebics subscriber details given a bank connection.
  */
 fun getEbicsSubscriberDetails(bankConnectionId: String): 
EbicsClientSubscriberDetails {
-    val transport = NexusBankConnectionEntity.findByName(bankConnectionId)
-    if (transport == null) {
-        throw NexusError(HttpStatusCode.NotFound, "transport not found")
-    }
-    val subscriber = EbicsSubscriberEntity.find {
-        NexusEbicsSubscribersTable.nexusBankConnection eq transport.id
-    }.first()
+    val transport = getBankConnection(bankConnectionId)
+    val subscriber = getSubscriberFromConnection(transport)
+
     // transport exists and belongs to caller.
     return getEbicsSubscriberDetailsInternal(subscriber)
 }
@@ -417,23 +422,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
         accountId: String
     ): List<Exception>? {
         val subscriberDetails = transaction { 
getEbicsSubscriberDetails(bankConnectionId) }
-        val lastTimes = transaction {
-            val acct = NexusBankAccountEntity.findByName(accountId)
-            if (acct == null) {
-                throw NexusError(
-                    HttpStatusCode.NotFound,
-                    "Account '$accountId' not found"
-                )
-            }
-            object {
-                val lastStatement = acct.lastStatementCreationTimestamp?.let {
-                    ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), 
ZoneOffset.UTC)
-                }
-                val lastReport = acct.lastReportCreationTimestamp?.let {
-                    ZonedDateTime.ofInstant(Instant.ofEpochMilli(it), 
ZoneOffset.UTC)
-                }
-            }
-        }
+        val lastTimes = getLastMessagesTimes(accountId)
         /**
          * Will be filled with fetch instructions, according
          * to the parameters received from the client.
@@ -533,61 +522,53 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol 
{
             return errors
         return null
     }
-    /**
-     * Submit one Pain.001 for one payment initiations.
-     */
+    // Submit one Pain.001 for one payment initiations.
     override suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId: Long) {
-        val r = transaction {
-            val paymentInitiation = 
PaymentInitiationEntity.findById(paymentInitiationId)
-                ?: throw NexusError(HttpStatusCode.NotFound, "payment 
initiation not found")
-            val conn = paymentInitiation.bankAccount.defaultBankConnection
-                ?: throw NexusError(HttpStatusCode.NotFound, "no default bank 
connection available for submission")
+        val dbData = transaction {
+            val preparedPayment = getPaymentInitiation(paymentInitiationId)
+            val conn = preparedPayment.bankAccount.defaultBankConnection ?: 
throw NexusError(HttpStatusCode.NotFound, "no default bank connection available 
for submission")
             val subscriberDetails = 
getEbicsSubscriberDetails(conn.connectionId)
             val painMessage = createPain001document(
                 NexusPaymentInitiationData(
-                    debtorIban = paymentInitiation.bankAccount.iban,
-                    debtorBic = paymentInitiation.bankAccount.bankCode,
-                    debtorName = paymentInitiation.bankAccount.accountHolder,
-                    currency = paymentInitiation.currency,
-                    amount = paymentInitiation.sum.toString(),
-                    creditorIban = paymentInitiation.creditorIban,
-                    creditorName = paymentInitiation.creditorName,
-                    creditorBic = paymentInitiation.creditorBic,
-                    paymentInformationId = 
paymentInitiation.paymentInformationId,
-                    preparationTimestamp = paymentInitiation.preparationDate,
-                    subject = paymentInitiation.subject,
-                    instructionId = paymentInitiation.instructionId,
-                    endToEndId = paymentInitiation.endToEndId,
-                    messageId = paymentInitiation.messageId
+                    debtorIban = preparedPayment.bankAccount.iban,
+                    debtorBic = preparedPayment.bankAccount.bankCode,
+                    debtorName = preparedPayment.bankAccount.accountHolder,
+                    currency = preparedPayment.currency,
+                    amount = preparedPayment.sum,
+                    creditorIban = preparedPayment.creditorIban,
+                    creditorName = preparedPayment.creditorName,
+                    creditorBic = preparedPayment.creditorBic,
+                    paymentInformationId = 
preparedPayment.paymentInformationId,
+                    preparationTimestamp = preparedPayment.preparationDate,
+                    subject = preparedPayment.subject,
+                    instructionId = preparedPayment.instructionId,
+                    endToEndId = preparedPayment.endToEndId,
+                    messageId = preparedPayment.messageId
                 )
             )
-            logger.debug("Sending Pain.001: 
${paymentInitiation.paymentInformationId}," +
-                    " for payment: '${paymentInitiation.subject}'")
-            if (!XMLUtil.validateFromString(painMessage)) {
-                logger.error("Pain.001 
${paymentInitiation.paymentInformationId}" +
-                        " is invalid, not submitting it and flag as invalid.")
-                val payment = getPaymentInitiation(paymentInitiationId)
-                payment.invalid = true
-                // The following commit prevents the thrown error
-                // to lose the database transaction data.
-                TransactionManager.current().commit()
-                throw NexusError(
-                    HttpStatusCode.InternalServerError,
-                    "Attempted Pain.001 
(${paymentInitiation.paymentInformationId})" +
-                            " message is invalid.  Not sent to the bank.",
-                    LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE
-                )
-            }
             object {
+                val painXml = painMessage
                 val subscriberDetails = subscriberDetails
-                val painMessage = painMessage
             }
         }
+        if (!XMLUtil.validateFromString(dbData.painXml)) {
+            val pmtInfId = transaction {
+                val payment = getPaymentInitiation(paymentInitiationId)
+                logger.error("Pain.001 ${payment.paymentInformationId}" +
+                        " is invalid, not submitting it and flag as invalid.")
+                payment.invalid = true
+            }
+            throw NexusError(
+                HttpStatusCode.InternalServerError,
+                "pain document: $pmtInfId document is invalid.  Not sent to 
the bank.",
+                LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE
+            )
+        }
         doEbicsUploadTransaction(
             httpClient,
-            r.subscriberDetails,
+            dbData.subscriberDetails,
             "CCT",
-            r.painMessage.toByteArray(Charsets.UTF_8),
+            dbData.painXml.toByteArray(Charsets.UTF_8),
             EbicsStandardOrderParams()
         )
         transaction {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
index 98e92a46..52627b66 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/iso20022/Iso20022.kt
@@ -22,15 +22,21 @@
  */
 package tech.libeufin.nexus.iso20022
 
+import com.fasterxml.jackson.annotation.JsonIgnore
 import com.fasterxml.jackson.annotation.JsonInclude
 import com.fasterxml.jackson.annotation.JsonValue
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
 import io.ktor.http.*
+import io.ktor.util.reflect.*
+import org.jetbrains.exposed.sql.and
+import org.jetbrains.exposed.sql.transactions.transaction
 import org.w3c.dom.Document
-import tech.libeufin.nexus.NexusAssert
-import tech.libeufin.nexus.NexusError
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.bankaccount.IngestedTransactionsCount
+import tech.libeufin.nexus.bankaccount.findDuplicate
 import tech.libeufin.nexus.server.CurrencyAmount
+import tech.libeufin.nexus.server.toPlainString
 import tech.libeufin.util.*
-import java.math.BigDecimal
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -40,23 +46,6 @@ enum class CreditDebitIndicator {
     DBIT, CRDT
 }
 
-enum class EntryStatus {
-    /**
-     * Booked
-     */
-    BOOK,
-
-    /**
-     * Pending
-     */
-    PDNG,
-
-    /**
-     * Informational
-     */
-    INFO,
-}
-
 enum class CashManagementResponseType(@get:JsonValue val jsonName: String) {
     Report("report"), Statement("statement"), Notification("notification")
 }
@@ -268,7 +257,7 @@ data class ReturnInfo(
 )
 
 data class BatchTransaction(
-    val amount: CurrencyAmount,
+    val amount: CurrencyAmount, // Fuels Taler withdrawal amount.
     val creditDebitIndicator: CreditDebitIndicator,
     val details: TransactionDetails
 )
@@ -329,7 +318,53 @@ data class CamtBankAccountEntry(
 
     // list of sub-transactions participating in this money movement.
     val batches: List<Batch>?
-)
+) {
+    /**
+     * This function returns the subject of the unique transaction
+     * accounted in this object.  If the transaction is not unique,
+     * it throws an exception.  NOTE: the caller has the responsibility
+     * of not passing an empty report; those usually should be discarded
+     * and never participate in the application logic.
+     */
+    @JsonIgnore
+    fun getSingletonSubject(): String {
+        // Checks that the given list contains only one element and returns it.
+        fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T {
+            if (maybeTxs == null || maybeTxs.size > 1) throw 
internalServerError(
+                "Only a singleton transaction is " +
+                        "allowed inside ${this.javaClass}."
+            )
+            return maybeTxs[0]
+        }
+        /**
+         * Types breakdown until the last payment information is reached.
+         *
+         * CamtBankAccountEntry contains:
+         * - Batch 0
+         * - Batch 1
+         * - Batch N
+         *
+         * Batch X contains:
+         * - BatchTransaction 0
+         * - BatchTransaction 1
+         * - BatchTransaction N
+         *
+         * BatchTransaction X contains:
+         * - TransactionDetails
+         *
+         * TransactionDetails contains the involved parties
+         * and the payment subject but MAY NOT contain the amount.
+         * In this model, the amount is held in the BatchTransaction
+         * type, that is also -- so far -- required to be a singleton
+         * inside Batch.
+         */
+        checkAndGetSingleton<Batch>(this.batches)
+        val batchTransactions = this.batches?.get(0)?.batchTransactions
+        val tx = checkAndGetSingleton<BatchTransaction>(batchTransactions)
+        val details: TransactionDetails = tx.details
+        return details.unstructuredRemittanceInformation
+    }
+}
 
 class CamtParsingError(msg: String) : Exception(msg)
 
@@ -861,7 +896,10 @@ private fun 
XmlElementDestructor.extractInnerTransactions(): CamtReport {
             instructedAmount = instructedAmount,
             creditDebitIndicator = creditDebitIndicator,
             bankTransactionCode = btc,
-            batches = extractBatches(amount, creditDebitIndicator, acctSvcrRef 
?: "AcctSvcrRef not given/found"),
+            batches = extractBatches(
+                amount,
+                creditDebitIndicator,
+                acctSvcrRef ?: "AcctSvcrRef not given/found"),
             bookingDate = maybeUniqueChildNamed("BookgDt") { 
extractDateOrDateTime() },
             valueDate = maybeUniqueChildNamed("ValDt") { 
extractDateOrDateTime() },
             accountServicerRef = acctSvcrRef,
@@ -936,3 +974,155 @@ fun parseCamtMessage(doc: Document): CamtParseResult {
         }
     }
 }
+
+/**
+ * Given that every CaMt is a collection of reports/statements
+ * where each of them carries the bank account balance and a list
+ * of transactions, this function:
+ *
+ * - extracts the balance (storing a NexusBankBalanceEntity)
+ * - updates timestamps in NexusBankAccountEntity to the last seen
+ *   report/statement.
+ * - finds which transactions were already downloaded.
+ * - stores a new NexusBankTransactionEntity for each new tx
+accounted in the report/statement.
+ * - tries to link the new transaction with a submitted one, in
+ *   case of DBIT transaction.
+ * - returns a IngestedTransactionCount object.
+ */
+fun processCamtMessage(
+    bankAccountId: String,
+    camtDoc: Document,
+    /**
+     * FIXME: should NOT be C52/C53 but "report" or "statement".
+     * The reason is that C52/C53 are NOT CaMt, they are EBICS names.
+     */
+    code: String
+): IngestedTransactionsCount {
+    var newTransactions = 0
+    var downloadedTransactions = 0
+    transaction {
+        val acct = NexusBankAccountEntity.findByName(bankAccountId)
+        if (acct == null) {
+            throw NexusError(HttpStatusCode.NotFound, "user not found")
+        }
+        val res = try { parseCamtMessage(camtDoc) } catch (e: 
CamtParsingError) {
+            logger.warn("Invalid CAMT received from bank: $e")
+            newTransactions = -1
+            return@transaction
+        }
+        res.reports.forEach {
+            NexusAssert(
+                it.account.iban == acct.iban,
+                "Nexus hit a report or statement of a wrong IBAN!"
+            )
+            it.balances.forEach { b ->
+                if (b.type == "CLBD") {
+                    val lastBalance = NexusBankBalanceEntity.all().lastOrNull()
+                    /**
+                     * Store balances different from the one that came from 
the bank,
+                     * or the very first balance.  This approach has the 
following inconvenience:
+                     * the 'balance' held at Nexus does not differentiate 
between one
+                     * coming from a statement and one coming from a report.  
As a consequence,
+                     * the two types of balances may override each other 
without notice.
+                     */
+                    if ((lastBalance == null) ||
+                        (b.amount.toPlainString() != lastBalance.balance)) {
+                        NexusBankBalanceEntity.new {
+                            bankAccount = acct
+                            balance = b.amount.toPlainString()
+                            creditDebitIndicator = b.creditDebitIndicator.name
+                            date = b.date
+                        }
+                    }
+                }
+            }
+        }
+        // Updating the local bank account state timestamps according to the 
current document.
+        val stamp = ZonedDateTime.parse(
+            res.creationDateTime,
+            DateTimeFormatter.ISO_DATE_TIME
+        ).toInstant().toEpochMilli()
+        when (code) {
+            "C52" -> {
+                val s = acct.lastReportCreationTimestamp
+                /**
+                 * FIXME.
+                 * The following check seems broken, as it ONLY sets the value 
when
+                 * s is non-null BUT s gets never set; not even with a default 
value.
+                 * That didn't break so far because the timestamp gets only 
used when
+                 * the fetch specification has "since-last" for the time 
range.  Never
+                 * used.
+                 */
+                if (s != null && stamp > s) {
+                    acct.lastReportCreationTimestamp = stamp
+                }
+            }
+            "C53" -> {
+                val s = acct.lastStatementCreationTimestamp
+                if (s != null && stamp > s) {
+                    acct.lastStatementCreationTimestamp = stamp
+                }
+            }
+        }
+        val entries: List<CamtBankAccountEntry> = res.reports.map { it.entries 
}.flatten()
+        var newPaymentsLog = ""
+        downloadedTransactions = entries.size
+        txloop@ for (entry: CamtBankAccountEntry in entries) {
+            val singletonBatchedTransaction: BatchTransaction = 
entry.batches?.get(0)?.batchTransactions?.get(0)
+                ?: throw NexusError(
+                    HttpStatusCode.InternalServerError,
+                    "Singleton money movements policy wasn't respected"
+                )
+            val acctSvcrRef = entry.accountServicerRef
+            if (acctSvcrRef == null) {
+                // FIXME(dold): Report this!
+                logger.error("missing account servicer reference in 
transaction")
+                continue
+            }
+            val duplicate = findDuplicate(bankAccountId, acctSvcrRef)
+            if (duplicate != null) {
+                logger.info("Found a duplicate (acctSvcrRef): $acctSvcrRef")
+                // FIXME(dold): See if an old transaction needs to be 
superseded by this one
+                // https://bugs.gnunet.org/view.php?id=6381
+                continue@txloop
+            }
+            val rawEntity = NexusBankTransactionEntity.new {
+                bankAccount = acct
+                accountTransactionId = acctSvcrRef
+                amount = singletonBatchedTransaction.amount.value
+                currency = singletonBatchedTransaction.amount.currency
+                transactionJson = 
jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString(entry)
+                creditDebitIndicator = 
singletonBatchedTransaction.creditDebitIndicator.name
+                status = entry.status
+            }
+            rawEntity.flush()
+            newTransactions++
+            newPaymentsLog += "\n- " + 
entry.batches[0].batchTransactions[0].details.unstructuredRemittanceInformation
+            // This block tries to acknowledge a former outgoing payment as 
booked.
+            if (singletonBatchedTransaction.creditDebitIndicator == 
CreditDebitIndicator.DBIT) {
+                val t0 = singletonBatchedTransaction.details
+                val pmtInfId = t0.paymentInformationId
+                if (pmtInfId != null) {
+                    val paymentInitiation = PaymentInitiationEntity.find {
+                        PaymentInitiationsTable.bankAccount eq acct.id and (
+                                // pmtInfId is a value that the payment 
submitter
+                                // asked the bank to associate with the 
payment to be made.
+                                PaymentInitiationsTable.paymentInformationId 
eq pmtInfId)
+
+                    }.firstOrNull()
+                    if (paymentInitiation != null) {
+                        logger.info("Could confirm one initiated payment: 
$pmtInfId")
+                        paymentInitiation.confirmationTransaction = rawEntity
+                    }
+                }
+            }
+        }
+        if (newTransactions > 0)
+            logger.debug("Camt $code '${res.messageId}' has new 
payments:${newPaymentsLog}")
+    }
+    return IngestedTransactionsCount(
+        newTransactions = newTransactions,
+        downloadedTransactions = downloadedTransactions
+    )
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
new file mode 100644
index 00000000..8c01705a
--- /dev/null
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/Helpers.kt
@@ -0,0 +1,85 @@
+package tech.libeufin.nexus.server
+
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.NexusBankConnectionEntity
+import tech.libeufin.nexus.NexusBankConnectionsTable
+import tech.libeufin.util.internalServerError
+import tech.libeufin.util.notFound
+
+/**
+ * FIXME:
+ * enum type names were introduced after 0.9.2 and need to
+ * be employed wherever now type names are passed as plain
+ * strings.
+ */
+
+// Valid connection types.
+enum class BankConnectionType(val typeName: String) {
+    EBICS("ebics"),
+    X_LIBEUFIN_BANK("x-taler-bank");
+    companion object {
+        /**
+         * This method takes legacy bank connection type names as input
+         * and _tries_ to return the correspondent enum type.  This
+         * fixes the cases where bank connection types are passed as
+         * easy-to-break arbitrary strings; eventually this method should
+         * be discarded and only enum types be passed as connection type names.
+         */
+        fun parseBankConnectionType(typeName: String): BankConnectionType {
+            return when(typeName) {
+                "ebics" -> EBICS
+                "x-libeufin-bank" -> X_LIBEUFIN_BANK
+                else -> throw internalServerError(
+                    "Cannot extract ${this::class.java.typeName}' instance 
from name: $typeName'"
+                )
+            }
+        }
+    }
+}
+// Valid facade types
+enum class NexusFacadeType(val facadeType: String) {
+    TALER("taler-wire-gateway"),
+    ANASTASIS("anastasis")
+}
+
+/**
+ * These types point at the _content_ brought by bank connections.
+ * The following stack depicts the layering of banking communication
+ * as modeled here in Nexus.  On top the most inner layer.
+ *
+ * --------------------
+ * Banking data type
+ * --------------------
+ * Bank connection type
+ * --------------------
+ * HTTP
+ * --------------------
+ *
+ * Once the banking data type arrives to the local database, facades
+ * types MAY apply further processing to it.
+ *
+ * For example, a Taler facade WILL look for Taler-meaningful wire
+ * subjects and act accordingly.  Even without a facade, the Nexus
+ * native HTTP API picks instances of banking data and extracts its
+ * details to serve to the client.
+ *
+ * NOTE: this type MAY help but is NOT essential, as each connection
+ * is USUALLY tied with the same banking data type.  For example, EBICS
+ * brings CaMt, and x-libeufin-bank bring its own (same-named x-libeufin-bank)
+ * banking data type.
+ */
+enum class BankingDataType {
+    X_LIBEUFIN_BANK,
+    CAMT
+}
+
+// Gets connection or throws.
+fun getBankConnection(connId: String): NexusBankConnectionEntity {
+    val maybeConn = transaction {
+        NexusBankConnectionEntity.find {
+            NexusBankConnectionsTable.connectionId eq connId
+        }.firstOrNull()
+    }
+    if (maybeConn == null) throw notFound("Bank connection $connId not found")
+    return maybeConn
+}
\ No newline at end of file
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
index 38ab4236..7fdfd526 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/JSON.kt
@@ -32,10 +32,9 @@ import 
com.fasterxml.jackson.databind.annotation.JsonDeserialize
 import com.fasterxml.jackson.databind.annotation.JsonSerialize
 import com.fasterxml.jackson.databind.deser.std.StdDeserializer
 import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import tech.libeufin.nexus.EntryStatus
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
-import tech.libeufin.nexus.iso20022.EntryStatus
 import tech.libeufin.util.*
-import java.math.BigDecimal
 import java.time.Instant
 import java.time.ZoneId
 import java.time.ZonedDateTime
@@ -251,6 +250,17 @@ data class EbicsNewTransport(
     val systemID: String?
 )
 
+/**
+ * Credentials and URL to access Sandbox and talk JSON to it.
+ * See 
https://docs.taler.net/design-documents/038-demobanks-protocol-suppliers.html#static-x-libeufin-bank-with-dynamic-demobank
+ * for an introduction on x-libeufin-bank.
+ */
+data class XLibeufinBankTransport(
+    val username: String,
+    val password: String,
+    val baseUrl: String
+)
+
 /** Response type of "GET /prepared-payments/{uuid}" */
 data class PaymentStatus(
     val paymentInitiationId: String,
@@ -262,10 +272,6 @@ data class PaymentStatus(
     val subject: String,
     val submissionDate: String?,
     val preparationDate: String,
-    // null when the payment was never acknowledged by
-    // the bank.  For example, it was submitted but never
-    // seen in any report; or only created and not even
-    // submitted.
     val status: EntryStatus?
 )
 
@@ -346,8 +352,9 @@ data class BankMessageList(
 )
 
 data class BankMessageInfo(
-    val messageId: String,
-    val code: String,
+    // x-libeufin-bank messages do not have any ID or code.
+    val messageId: String?,
+    val code: String?,
     val length: Long
 )
 
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
index ebe002fc..fb864d3b 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/server/NexusServer.kt
@@ -53,6 +53,7 @@ import tech.libeufin.nexus.*
 import tech.libeufin.nexus.bankaccount.*
 import tech.libeufin.nexus.ebics.*
 import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
+import tech.libeufin.nexus.iso20022.processCamtMessage
 import tech.libeufin.util.*
 import java.net.BindException
 import java.net.URLEncoder
@@ -739,7 +740,7 @@ val nexusApp: Application.() -> Unit = {
             var statusCode = HttpStatusCode.OK
             /**
              * Client errors are unlikely here, because authentication
-             * and JSON validity fail earlier.  Hence either Nexus or the
+             * and JSON validity fail earlier.  Hence, either Nexus or the
              * bank had a problem.  NOTE: because this handler triggers 
multiple
              * fetches, it is ALSO possible that although one error is 
reported,
              * SOME transactions made it to the database!
diff --git 
a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt 
b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
new file mode 100644
index 00000000..b819163a
--- /dev/null
+++ 
b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt
@@ -0,0 +1,395 @@
+package tech.libeufin.nexus.xlibeufinbank
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.client.*
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.server.util.*
+import io.ktor.util.*
+import org.jetbrains.exposed.sql.statements.api.ExposedBlob
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.bankaccount.*
+import tech.libeufin.nexus.iso20022.*
+import tech.libeufin.nexus.server.*
+import tech.libeufin.util.XLibeufinBankDirection
+import tech.libeufin.util.XLibeufinBankTransaction
+import tech.libeufin.util.badRequest
+import tech.libeufin.util.internalServerError
+import java.net.MalformedURLException
+import java.net.URL
+
+// Gets Sandbox URL and credentials, taking the connection name as input.
+fun getXLibeufinBankCredentials(conn: NexusBankConnectionEntity): 
XLibeufinBankTransport {
+    val maybeCredentials = transaction {
+        XLibeufinBankUserEntity.find {
+            XLibeufinBankUsersTable.nexusBankConnection eq conn.id
+        }.firstOrNull()
+    }
+    if (maybeCredentials == null) throw internalServerError(
+        "Existing connection ${conn.connectionId} has no transport details"
+    )
+    return XLibeufinBankTransport(
+        username = maybeCredentials.username,
+        password = maybeCredentials.password,
+        baseUrl = maybeCredentials.baseUrl
+    )
+}
+fun getXLibeufinBankCredentials(connId: String): XLibeufinBankTransport {
+    val conn = getBankConnection(connId)
+    return getXLibeufinBankCredentials(conn)
+
+}
+
+class XlibeufinBankConnectionProtocol : BankConnectionProtocol {
+    override suspend fun connect(client: HttpClient, connId: String) {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun fetchAccounts(client: HttpClient, connId: String) {
+        throw NotImplementedError("x-libeufin-bank does not need to fetch 
accounts")
+    }
+
+    override fun createConnectionFromBackup(
+        connId: String,
+        user: NexusUserEntity,
+        passphrase: String?,
+        backup: JsonNode
+    ) {
+        TODO("Not yet implemented")
+    }
+
+    override fun createConnection(
+        connId: String,
+        user: NexusUserEntity,
+        data: JsonNode) {
+
+        val bankConn = transaction {
+            NexusBankConnectionEntity.new {
+                this.connectionId = connId
+                owner = user
+                type = "x-libeufin-bank"
+            }
+        }
+        val newTransportData = jacksonObjectMapper().treeToValue(
+            data, XLibeufinBankTransport::class.java
+        ) ?: throw badRequest("x-libeufin-bank details not found in the 
request")
+        // Validate the base URL
+        try { URL(newTransportData.baseUrl).toURI() }
+        catch (e: MalformedURLException) {
+            throw badRequest("Base URL (${newTransportData.baseUrl}) is 
invalid.")
+        }
+        transaction {
+            XLibeufinBankUserEntity.new {
+                username = newTransportData.username
+                password = newTransportData.password
+                // Only addressing mild cases where ONE slash ends the base 
URL.
+                baseUrl = newTransportData.baseUrl.dropLastWhile { it == '/' }
+                nexusBankConnection = bankConn
+            }
+        }
+    }
+
+    override fun getConnectionDetails(conn: NexusBankConnectionEntity): 
JsonNode {
+        TODO("Not yet implemented")
+    }
+
+    override fun exportBackup(bankConnectionId: String, passphrase: String): 
JsonNode {
+        TODO("Not yet implemented")
+    }
+
+    override fun exportAnalogDetails(conn: NexusBankConnectionEntity): 
ByteArray {
+        throw NotImplementedError("x-libeufin-bank does not need analog 
details")
+    }
+
+    override suspend fun submitPaymentInitiation(httpClient: HttpClient, 
paymentInitiationId: Long) {
+        TODO("Not yet implemented")
+    }
+
+    override suspend fun fetchTransactions(
+        fetchSpec: FetchSpecJson,
+        client: HttpClient,
+        bankConnectionId: String,
+        accountId: String
+    ): List<Exception>? {
+        val conn = getBankConnection(bankConnectionId)
+        /**
+         * Note: fetchSpec.level is ignored because Sandbox does not
+         * differentiate between booked and non-booked transactions.
+         * Just logging if the unaware client specified non-REPORT for
+         * the level.  FIXME: docs have to mention this.
+         */
+        if (fetchSpec.level == FetchLevel.REPORT || fetchSpec.level == 
FetchLevel.ALL)
+            throw badRequest("level '${fetchSpec.level}' on x-libeufin-bank" +
+                    "connection (${conn.connectionId}) is not supported:" +
+                    " bank has only 'booked' state."
+            )
+        // Get credentials
+        val credentials = getXLibeufinBankCredentials(conn)
+        /**
+         * Now builds the URL to ask the transactions, according to the
+         * FetchSpec gotten in the args.  Level 'statement' and time range
+         * 'previous-dayes' are NOT implemented.
+         */
+        val baseUrl = URL(credentials.baseUrl)
+        val fetchUrl = url {
+            protocol = URLProtocol(name = baseUrl.protocol, defaultPort = -1)
+            appendPathSegments(
+                baseUrl.path.dropLastWhile { it == '/' },
+                "accounts/${credentials.username}/transactions")
+            when (fetchSpec) {
+                // Gets the last 5 transactions
+                is FetchSpecLatestJson -> {
+                    // Do nothing, the bare endpoint gets the last 5 txs by 
default.
+                }
+                /* Defines the from_ms URI param. according to the last 
transaction
+                 * timestamp that was seen in this connection */
+                is FetchSpecSinceLastJson -> {
+                    val localBankAccount = getBankAccount(accountId)
+                    val lastMessagesTimes = 
getLastMessagesTimes(localBankAccount)
+                    // Sandbox doesn't have report vs. statement, defaulting 
to report time
+                    // and so does the ingestion routine when storing the last 
message time.
+                    this.parameters["from_ms"] = 
"${lastMessagesTimes.lastStatement ?: 0}"
+                }
+                // This wants ALL the transactions, hence it sets the from_ms 
to zero.
+                is FetchSpecAllJson -> {
+                    this.parameters["from_ms"] = "0"
+                }
+                else -> throw NexusError(
+                    HttpStatusCode.NotImplemented,
+                    "FetchSpec ${fetchSpec::class} not supported"
+                )
+            }
+        }
+        logger.debug("Requesting x-libeufin-bank transactions to: $fetchUrl")
+        val resp: HttpResponse = try {
+            client.get(fetchUrl) {
+                expectSuccess = true
+                contentType(ContentType.Application.Json)
+                basicAuth(credentials.username, credentials.password)
+            }
+        } catch (e: Exception) {
+            e.printStackTrace()
+            logger.error(e.message)
+            return listOf(e)
+        }
+        val respBlob = resp.bodyAsChannel().toByteArray()
+        transaction {
+            NexusBankMessageEntity.new {
+                bankConnection = conn
+                message = ExposedBlob(respBlob)
+            }
+        }
+        return null
+    }
+}
+
+/**
+ * Parses one x-libeufin-bank message and INSERTs Nexus local
+ * transaction records into the database.  After this function
+ * returns, the transactions are ready to both being communicated
+ * to the CLI via the native JSON interface OR being further processed
+ * by ANY facade.
+ *
+ * This function:
+ * - updates the local timestamps related to the latest report.
+ * - inserts a new NexusBankTransactionEntity.  To achieve that, it extracts 
the:
+ * -- amount
+ * -- credit/debit indicator
+ * -- currency
+ *
+ * Note: in contrast to what the CaMt handler does, here there's NO
+ * status, since Sandbox has only one (unnamed) transaction state and
+ * all transactions are asked as reports.
+ */
+fun processXLibeufinBankMessage(
+    bankAccountId: String,
+    data: JsonNode
+): IngestedTransactionsCount {
+    data class XLibeufinBankTransactions(
+        val transactions: List<XLibeufinBankTransaction>
+    )
+    val txs = try {
+        jacksonObjectMapper().treeToValue(
+            data,
+            XLibeufinBankTransactions::class.java
+        )
+    } catch (e: Exception) {
+        throw NexusError(
+            HttpStatusCode.BadGateway,
+            "The bank sent invalid x-libeufin-bank transactions."
+        )
+    }
+    val bankAccount = getBankAccount(bankAccountId)
+    var newTxs = 0 // Counts how many transactions are new.
+    txs.transactions.forEach {
+        val maybeTimestamp = try {
+            it.date.toLong()
+        } catch (e: Exception) {
+            throw NexusError(
+                HttpStatusCode.BadGateway,
+                "The bank gave an invalid timestamp " +
+                        "for x-libeufin-bank message: ${it.uid}"
+            )
+        }
+        // Searching for duplicates.
+        if (findDuplicate(bankAccountId, it.uid) != null) {
+            logger.debug(
+                "x-libeufin-bank ingestion: transaction ${it.uid} is a 
duplicate, skipping."
+            )
+            return@forEach
+        }
+        val direction = if (it.debtorIban == bankAccount.iban)
+            XLibeufinBankDirection.DEBIT else XLibeufinBankDirection.CREDIT
+        // New tx, storing it.
+        transaction {
+            val localTx = NexusBankTransactionEntity.new {
+                this.bankAccount = bankAccount
+                this.amount = it.amount
+                this.currency = it.currency
+                /**
+                 * Sandbox has only booked state for its transactions: as soon 
as
+                 * one payment makes it to the database, that is the final 
(booked)
+                 * state.
+                 */
+                this.status = EntryStatus.BOOK
+                this.accountTransactionId = it.uid
+                this.transactionJson = jacksonObjectMapper(
+                ).writeValueAsString(it.exportAsCamtModel())
+                this.creditDebitIndicator = direction.direction
+                newTxs++
+                logger.debug("x-libeufin-bank transaction with subject 
'${it.subject}' ingested.")
+            }
+            /**
+             * The following block tries to reconcile a previous prepared
+             * (outgoing) payment with the one being iterated over.
+             */
+            if (direction == XLibeufinBankDirection.DEBIT) {
+                val maybePrepared = getPaymentInitiation(pmtInfId = it.uid)
+                if (maybePrepared != null) 
maybePrepared.confirmationTransaction = localTx
+            }
+            // x-libeufin-bank transactions are ALWAYS modeled as reports
+            // in Nexus, because such bank protocol supplier doesn't have
+            // the report vs. statement distinction.  Therefore, we only
+            // consider the last report timestamp.
+            if ((bankAccount.lastStatementCreationTimestamp ?: 0L) < 
maybeTimestamp)
+                bankAccount.lastStatementCreationTimestamp = maybeTimestamp
+        }
+    }
+    return IngestedTransactionsCount(
+        newTransactions = newTxs,
+        downloadedTransactions = txs.transactions.size
+    )
+}
+
+fun XLibeufinBankTransaction.exportCamtDirectionIndicator(): 
CreditDebitIndicator =
+    if (this.direction == XLibeufinBankDirection.CREDIT)
+        CreditDebitIndicator.CRDT else CreditDebitIndicator.DBIT
+
+/**
+ * This function transforms an x-libeufin-bank transaction
+ * into the JSON representation of CaMt used by Nexus along
+ * its processing.  Notably, this helps to stick to one unified
+ * type when facades process transactions.
+ */
+fun XLibeufinBankTransaction.exportAsCamtModel(): CamtBankAccountEntry =
+    CamtBankAccountEntry(
+        /**
+         * Amount obtained by summing all the transactions accounted
+         * in this report/statement.  Here this field equals the amount of the
+         * _unique_ transaction accounted.
+         */
+        amount = CurrencyAmount(currency = this.currency, value = this.amount),
+        accountServicerRef = this.uid,
+        bankTransactionCode = "Not given",
+        bookingDate = this.date,
+        counterValueAmount = null,
+        creditDebitIndicator = this.exportCamtDirectionIndicator(),
+        currencyExchange = null,
+        entryRef = null,
+        instructedAmount = null,
+        valueDate = null,
+        status = EntryStatus.BOOK, // x-libeufin-bank always/only BOOK.
+        /**
+         * This field accounts for the _unique_ transaction that this
+         * object represents.
+         */
+        batches = listOf(
+            Batch(
+                messageId = null,
+                paymentInformationId = this.uid,
+                batchTransactions = listOf(
+                    BatchTransaction(
+                        amount = CurrencyAmount(
+                            currency = this.currency,
+                            value = this.amount
+                        ),
+                        creditDebitIndicator = 
this.exportCamtDirectionIndicator(),
+                        details = TransactionDetails(
+                            debtor = PartyIdentification(
+                                name = this.debtorName,
+                                countryOfResidence = null,
+                                organizationId = null,
+                                otherId = null,
+                                postalAddress = null,
+                                privateId = null
+                                ),
+                            debtorAccount = CashAccount(
+                                name = null,
+                                currency = this.currency,
+                                iban = this.debtorIban,
+                                otherId = null
+                            ),
+                            debtorAgent = AgentIdentification(
+                                name = null,
+                                bic = this.debtorBic,
+                                clearingSystemCode = null,
+                                clearingSystemMemberId = null,
+                                lei = null,
+                                otherId = null,
+                                postalAddress = null,
+                                proprietaryClearingSystemCode = null
+                            ),
+                            counterValueAmount = null,
+                            currencyExchange = null,
+                            interBankSettlementAmount = null,
+                            proprietaryPurpose = null,
+                            purpose = null,
+                            returnInfo = null,
+                            ultimateCreditor = null,
+                            ultimateDebtor = null,
+                            unstructuredRemittanceInformation = this.subject,
+                            instructedAmount = null,
+                            creditor = PartyIdentification(
+                                name = this.creditorName,
+                                countryOfResidence = null,
+                                organizationId = null,
+                                otherId = null,
+                                postalAddress = null,
+                                privateId = null
+                            ),
+                            creditorAccount = CashAccount(
+                                name = null,
+                                currency = this.currency,
+                                iban = this.creditorIban,
+                                otherId = null
+                            ),
+                            creditorAgent = AgentIdentification(
+                                name = null,
+                                bic = this.creditorBic,
+                                clearingSystemCode = null,
+                                clearingSystemMemberId = null,
+                                lei = null,
+                                otherId = null,
+                                postalAddress = null,
+                                proprietaryClearingSystemCode = null
+                            )
+                        )
+                    )
+                )
+            )
+        )
+    )
\ No newline at end of file
diff --git a/nexus/src/test/kotlin/DownloadAndSubmit.kt 
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
index 0ac5b0c7..622ff928 100644
--- a/nexus/src/test/kotlin/DownloadAndSubmit.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -114,9 +114,9 @@ class DownloadAndSubmit {
                         client,
                         fetchSpec = FetchSpecAllJson(
                             level = FetchLevel.REPORT,
-                            "foo"
+                            bankConnection = "foo"
                         ),
-                        "foo"
+                        accountId = "foo"
                     )
                 }
                 transaction {
@@ -223,7 +223,7 @@ class DownloadAndSubmit {
     }
 
     /**
-     * Submit one payment instruction with a invalid Pain.001
+     * Submit one payment instruction with an invalid Pain.001
      * document, and check that it was marked as invalid.  Hence,
      * the error is expected only by the first submission, since
      * the second won't pick the invalid payment.
@@ -238,7 +238,7 @@ class DownloadAndSubmit {
                     addPaymentInitiation(
                         Pain001Data(
                             creditorIban = getIban(),
-                            creditorBic = "not-a-BIC",
+                            creditorBic = "not-a-BIC", // this value causes 
the expected error.
                             creditorName = "Tester",
                             subject = "test payment",
                             sum = "1",
diff --git a/nexus/src/test/kotlin/MakeEnv.kt b/nexus/src/test/kotlin/MakeEnv.kt
index 9f8f5249..596b2c95 100644
--- a/nexus/src/test/kotlin/MakeEnv.kt
+++ b/nexus/src/test/kotlin/MakeEnv.kt
@@ -3,19 +3,16 @@ import org.jetbrains.exposed.sql.Database
 import org.jetbrains.exposed.sql.statements.api.ExposedBlob
 import org.jetbrains.exposed.sql.transactions.TransactionManager
 import org.jetbrains.exposed.sql.transactions.transaction
-import org.jetbrains.exposed.sql.transactions.transactionManager
 import tech.libeufin.nexus.*
 import tech.libeufin.nexus.dbCreateTables
 import tech.libeufin.nexus.dbDropTables
 import tech.libeufin.nexus.iso20022.*
+import tech.libeufin.nexus.server.BankConnectionType
 import tech.libeufin.nexus.server.CurrencyAmount
 import tech.libeufin.nexus.server.FetchLevel
 import tech.libeufin.nexus.server.FetchSpecAllJson
 import tech.libeufin.sandbox.*
-import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.EbicsInitState
-import java.io.File
-import tech.libeufin.util.getIban
+import tech.libeufin.util.*
 
 data class EbicsKeys(
     val auth: CryptoUtil.RsaCrtKeyPair,
@@ -40,6 +37,15 @@ val userKeys = EbicsKeys(
     sig = CryptoUtil.generateRsaKeyPair(2048)
 )
 
+fun assertWithPrint(cond: Boolean, msg: String) {
+    try {
+        assert(cond)
+    } catch (e: AssertionError) {
+        System.err.println(msg)
+        throw e
+    }
+}
+
 // New versions of JUnit provide this!
 inline fun <reified ExceptionType> assertException(
     block: () -> Unit,
@@ -85,11 +91,28 @@ fun prepNexusDb() {
             passwordHash = CryptoUtil.hashpw("foo")
             superuser = true
         }
+        val b = NexusUserEntity.new {
+            username = "bar"
+            passwordHash = CryptoUtil.hashpw("bar")
+            superuser = true
+        }
         val c = NexusBankConnectionEntity.new {
+            connectionId = "bar"
+            owner = b
+            type = "x-libeufin-bank"
+        }
+        val d = NexusBankConnectionEntity.new {
             connectionId = "foo"
-            owner = u
+            owner = b
             type = "ebics"
         }
+        XLibeufinBankUserEntity.new {
+            username = "bar"
+            password = "bar"
+            // Only addressing mild cases where ONE slash ends the base URL.
+            baseUrl = "http://localhost/demobanks/default/access-api";
+            nexusBankConnection = c
+        }
         tech.libeufin.nexus.EbicsSubscriberEntity.new {
             // ebicsURL = "http://localhost:5000/ebicsweb";
             ebicsURL = "http://localhost/ebicsweb";
@@ -100,7 +123,7 @@ fun prepNexusDb() {
             signaturePrivateKey = ExposedBlob(userKeys.sig.private.encoded)
             encryptionPrivateKey = ExposedBlob(userKeys.enc.private.encoded)
             authenticationPrivateKey = 
ExposedBlob(userKeys.auth.private.encoded)
-            nexusBankConnection = c
+            nexusBankConnection = d
             ebicsIniState = EbicsInitState.NOT_SENT
             ebicsHiaState = EbicsInitState.NOT_SENT
             bankEncryptionPublicKey = ExposedBlob(bankKeys.enc.public.encoded)
@@ -110,7 +133,7 @@ fun prepNexusDb() {
             bankAccountName = "foo"
             iban = FOO_USER_IBAN
             bankCode = "SANDBOXX"
-            defaultBankConnection = c
+            defaultBankConnection = d
             highestSeenBankMessageSerialId = 0
             accountHolder = "foo"
         }
@@ -140,7 +163,7 @@ fun prepNexusDb() {
         }
         // Giving 'foo' a Taler facade.
         val f = FacadeEntity.new {
-            facadeName = "taler"
+            facadeName = "foo-facade"
             type = "taler-wire-gateway"
             creator = u
         }
@@ -152,6 +175,20 @@ fun prepNexusDb() {
             facade = f
             highestSeenMessageSerialId = 0
         }
+        // Giving 'bar' a Taler facade
+        val g = FacadeEntity.new {
+            facadeName = "bar-facade"
+            type = "taler-wire-gateway"
+            creator = b
+        }
+        FacadeStateEntity.new {
+            bankAccount = "bar"
+            bankConnection = "bar" // uses x-libeufin-bank connection.
+            currency = "TESTKUDOS"
+            reserveTransferLevel = "report"
+            facade = g
+            highestSeenMessageSerialId = 0
+        }
     }
 }
 
@@ -287,35 +324,82 @@ fun withSandboxTestDatabase(f: () -> Unit) {
     }
 }
 
-fun newNexusBankTransaction(currency: String, value: String, subject: String) {
+fun newNexusBankTransaction(
+    currency: String,
+    value: String,
+    subject: String,
+    creditorAcct: String = "foo",
+    connType: BankConnectionType = BankConnectionType.EBICS
+) {
+    val jDetails: String = when(connType) {
+        BankConnectionType.EBICS -> {
+            jacksonObjectMapper(
+            ).writerWithDefaultPrettyPrinter(
+            ).writeValueAsString(
+                genNexusIncomingCamt(
+                    amount = CurrencyAmount(currency,value),
+                    subject = subject
+                )
+            )
+        }
+        /**
+         * Note: x-libeufin-bank ALSO stores the transactions in the
+         * CaMt representation, hence this branch should be removed.
+         */
+        BankConnectionType.X_LIBEUFIN_BANK -> {
+            jacksonObjectMapper(
+            ).writerWithDefaultPrettyPrinter(
+            ).writeValueAsString(genNexusIncomingCamt(
+                amount = CurrencyAmount(currency, value),
+                subject = subject
+            ))
+        }
+        else -> throw Exception("Unsupported connection type: 
${connType.typeName}")
+    }
     transaction {
         NexusBankTransactionEntity.new {
-            bankAccount = NexusBankAccountEntity.findByName("foo")!!
+            bankAccount = NexusBankAccountEntity.findByName(creditorAcct)!!
             accountTransactionId = "mock"
             creditDebitIndicator = "CRDT"
             this.currency = currency
             this.amount = value
             status = EntryStatus.BOOK
-            transactionJson = jacksonObjectMapper(
-            ).writerWithDefaultPrettyPrinter(
-            ).writeValueAsString(
-                genNexusIncomingPayment(
-                    amount = CurrencyAmount(currency,value),
-                    subject = subject
-                )
-            )
+            transactionJson = jDetails
         }
-        /*TalerIncomingPaymentEntity.new {
-            payment = inc
-            reservePublicKey = "mock"
-            timestampMs = 0L
-            debtorPaytoUri = "mock"
-        }*/
     }
 }
 
-
-fun genNexusIncomingPayment(
+/**
+ * This function generates the Nexus JSON model of one transaction
+ * as if it got downloaded via one x-libeufin-bank connection.  The
+ * non given values are either resorted from other sources by Nexus,
+ * or actually not useful so far.
+ */
+private fun genNexusIncomingXLibeufinBank(
+    amount: CurrencyAmount,
+    subject: String
+): XLibeufinBankTransaction =
+    XLibeufinBankTransaction(
+        creditorIban = "NOTUSED",
+        creditorBic =  null,
+        creditorName = "Not Used",
+        debtorIban =  "NOTUSED",
+        debtorBic = null,
+        debtorName = "Not Used",
+        amount = amount.value,
+        currency =  amount.currency,
+        subject =  subject,
+        date = "0",
+        uid =  "not-used",
+        direction = XLibeufinBankDirection.CREDIT
+    )
+/**
+ * This function generates the Nexus JSON model of one transaction
+ * as if it got downloaded via one Ebics connection.  The non given
+ * values are either resorted from other sources by Nexus, or actually
+ * not useful so far.
+ */
+private fun genNexusIncomingCamt(
     amount: CurrencyAmount,
     subject: String,
 ): CamtBankAccountEntry =
@@ -382,4 +466,4 @@ fun genNexusIncomingPayment(
                 )
             )
         )
-    )
\ No newline at end of file
+    )
diff --git a/nexus/src/test/kotlin/NexusApiTest.kt 
b/nexus/src/test/kotlin/NexusApiTest.kt
index 30763005..e4fcc6d0 100644
--- a/nexus/src/test/kotlin/NexusApiTest.kt
+++ b/nexus/src/test/kotlin/NexusApiTest.kt
@@ -4,13 +4,13 @@ import io.ktor.http.*
 import io.ktor.server.testing.*
 import org.junit.Test
 import tech.libeufin.nexus.server.nexusApp
+import tech.libeufin.sandbox.sandboxApp
 
 /**
  * This class tests the API offered by Nexus,
  * documented here: https://docs.taler.net/libeufin/api-nexus.html
  */
 class NexusApiTest {
-
     // Testing basic operations on facades.
     @Test
     fun facades() {
@@ -19,7 +19,7 @@ class NexusApiTest {
             prepNexusDb()
             testApplication {
                 application(nexusApp)
-                client.delete("/facades/taler") {
+                client.delete("/facades/foo-facade") {
                     basicAuth("foo", "foo")
                     expectSuccess = true
                 }
diff --git a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt 
b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
index 8979fef9..d9ff3d51 100644
--- a/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
+++ b/nexus/src/test/kotlin/SandboxCircuitApiTest.kt
@@ -38,18 +38,40 @@ class SandboxCircuitApiTest {
             prepSandboxDb()
             testApplication {
                 application(sandboxApp)
-                val R = client.get(
+                var R = client.get(
                     
"/demobanks/default/circuit-api/cashouts/estimates?amount_debit=TESTKUDOS:2"
                 ) {
                     expectSuccess = true
                     basicAuth("foo", "foo")
                 }
                 val mapper = ObjectMapper()
-                val respJson = mapper.readTree(R.bodyAsText())
+                var respJson = mapper.readTree(R.bodyAsText())
                 val creditAmount = respJson.get("amount_credit").asText()
                 // sell ratio and fee are the following constants: 0.95 and 0.
                 // expected credit amount = 2 * 0.95 - 0 = 1.90
                 assert("CHF:1.90" == creditAmount || "CHF:1.9" == creditAmount)
+                R = client.get(
+                    
"/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1.9"
+                ) {
+                    expectSuccess = true
+                    basicAuth("foo", "foo")
+                }
+                respJson = mapper.readTree(R.bodyAsText())
+                val debitAmount = respJson.get("amount_debit").asText()
+                assertWithPrint(
+                    "TESTKUDOS:2" == debitAmount || "TESTKUDOS:2.0" == 
debitAmount,
+                    "'debit_amount' was $debitAmount for a 'credit_amount' of 
CHF:1.9"
+                )
+                R = client.get(
+                    
"/demobanks/default/circuit-api/cashouts/estimates?amount_credit=CHF:1&amount_debit=TESTKUDOS=1"
+                ) {
+                    expectSuccess = false
+                    basicAuth("foo", "foo")
+                }
+                assertWithPrint(
+                    R.status.value == HttpStatusCode.BadRequest.value,
+                    "Expected status code was 400, but got '${R.status.value}' 
instead."
+                )
             }
         }
     }
diff --git a/nexus/src/test/kotlin/TalerTest.kt 
b/nexus/src/test/kotlin/TalerTest.kt
index c433284a..f877203b 100644
--- a/nexus/src/test/kotlin/TalerTest.kt
+++ b/nexus/src/test/kotlin/TalerTest.kt
@@ -30,7 +30,7 @@ class TalerTest {
         withNexusAndSandboxUser {
             testApplication {
                 application(nexusApp)
-                client.post("/facades/taler/taler-wire-gateway/transfer") {
+                client.post("/facades/foo-facade/taler-wire-gateway/transfer") 
{
                     contentType(ContentType.Application.Json)
                     basicAuth("foo", "foo") // exchange's credentials
                     expectSuccess = true
@@ -73,7 +73,7 @@ class TalerTest {
              */
             testApplication {
                 application(nexusApp)
-                val r = 
client.get("/facades/taler/taler-wire-gateway/history/outgoing?delta=5") {
+                val r = 
client.get("/facades/foo-facade/taler-wire-gateway/history/outgoing?delta=5") {
                     expectSuccess = true
                     contentType(ContentType.Application.Json)
                     basicAuth("foo", "foo")
@@ -85,38 +85,102 @@ class TalerTest {
         }
     }
 
-    // Checking that a correct wire transfer (with Taler-compatible subject)
-    // is responded by the Taler facade.
+    // Tests that incoming Taler txs arrive via EBICS.
     @Test
-    fun historyIncomingTest() {
+    fun historyIncomingTestEbics() {
+        historyIncomingTest(
+            testedAccount = "foo",
+            connType = BankConnectionType.EBICS
+        )
+    }
+
+    // Tests that incoming Taler txs arrive via x-libeufin-bank.
+    @Test
+    fun historyIncomingTestXLibeufinBank() {
+        historyIncomingTest(
+            testedAccount = "bar",
+            connType = BankConnectionType.X_LIBEUFIN_BANK
+        )
+    }
+
+    // Tests that even if one call is long-polling, other calls
+    @Test
+    fun servingTest() {
+        withTestDatabase {
+            prepNexusDb()
+            testApplication {
+                application(nexusApp)
+                // This call blocks for 90 seconds
+                val currentTime = System.currentTimeMillis()
+                runBlocking {
+                    launch {
+                        val r = 
client.get("/facades/foo-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=5000")
 {
+                            expectSuccess = true
+                            contentType(ContentType.Application.Json)
+                            basicAuth("foo", "foo") // user & pw always equal.
+                        }
+                        assert(r.status.value == 
HttpStatusCode.NoContent.value)
+                    }
+                    val R = client.get("/") {
+                        expectSuccess = true
+                    }
+                    val latestTime = System.currentTimeMillis()
+                    assert(R.status.value == HttpStatusCode.OK.value
+                            && (latestTime - currentTime) < 2000
+                    )
+                }
+            }
+        }
+    }
+
+    // Downloads Taler txs via the default connection of 'testedAccount'.
+    // This allows to test the Taler logic on different connection types.
+    fun historyIncomingTest(testedAccount: String, connType: 
BankConnectionType) {
         val reservePub = "GX5H5RME193FDRCM1HZKERXXQ2K21KH7788CKQM8X6MYKYRBP8F0"
         withNexusAndSandboxUser {
             testApplication {
                 application(nexusApp)
                 runBlocking {
+                    /**
+                     * This block issues the request by long-polling and
+                     * lets the execution proceed where the actions to unblock
+                     * the polling are taken.
+                     */
                     launch {
-                        val r = 
client.get("/facades/taler/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=3000")
 {
-                            expectSuccess = false
+                        val r = 
client.get("/facades/${testedAccount}-facade/taler-wire-gateway/history/incoming?delta=5&start=0&long_poll_ms=30000")
 {
+                            expectSuccess = true
                             contentType(ContentType.Application.Json)
-                            basicAuth("foo", "foo")
+                            basicAuth(testedAccount, testedAccount) // user & 
pw always equal.
                         }
-                        println("maybe response body: ${r.bodyAsText()}")
-                        assert(r.status.value == HttpStatusCode.OK.value)
+                        assertWithPrint(
+                            r.status.value == HttpStatusCode.OK.value,
+                            "Long-polling history had status: 
${r.status.value} and" +
+                                    " body: ${r.bodyAsText()}"
+                        )
                         val j = mapper.readTree(r.readBytes())
                         val reservePubFromTwg = 
j.get("incoming_transactions").get(0).get("reserve_pub").asText()
                         assert(reservePubFromTwg == reservePub)
                     }
-                    newNexusBankTransaction(
-                        "KUDOS",
-                        "10",
-                        reservePub
-                    )
-                    ingestFacadeTransactions(
-                        "foo", // bank account local to Nexus.
-                        "taler-wire-gateway",
-                        ::talerFilter,
-                        ::maybeTalerRefunds
-                    )
+                    launch {
+                        delay(500)
+                        /**
+                         * FIXME: this test never gets the server to wait 
notifications from the DBMS.
+                         * Somehow, the wire transfer arrives always before 
the blocking await on the DBMS.
+                         */
+                        newNexusBankTransaction(
+                            currency = "KUDOS",
+                            value = "10",
+                            subject = reservePub,
+                            creditorAcct = testedAccount,
+                            connType = connType
+                        )
+                        ingestFacadeTransactions(
+                            bankAccountId = testedAccount, // bank account 
local to Nexus.
+                            facadeType = NexusFacadeType.TALER,
+                            incomingFilterCb = ::talerFilter,
+                            refundCb = ::maybeTalerRefunds
+                        )
+                    }
                 }
             }
         }
diff --git a/nexus/src/test/kotlin/XLibeufinBankTest.kt 
b/nexus/src/test/kotlin/XLibeufinBankTest.kt
new file mode 100644
index 00000000..9af9133c
--- /dev/null
+++ b/nexus/src/test/kotlin/XLibeufinBankTest.kt
@@ -0,0 +1,111 @@
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.server.testing.*
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.junit.Test
+import tech.libeufin.nexus.BankConnectionProtocol
+import tech.libeufin.nexus.NexusBankTransactionEntity
+import tech.libeufin.nexus.NexusBankTransactionsTable
+import tech.libeufin.nexus.bankaccount.ingestBankMessagesIntoAccount
+import tech.libeufin.nexus.getNexusUser
+import tech.libeufin.nexus.iso20022.CamtBankAccountEntry
+import tech.libeufin.nexus.server.*
+import tech.libeufin.nexus.xlibeufinbank.XlibeufinBankConnectionProtocol
+import tech.libeufin.sandbox.sandboxApp
+import tech.libeufin.sandbox.wireTransfer
+import tech.libeufin.util.XLibeufinBankTransaction
+import java.net.URL
+
+// Testing the x-libeufin-bank communication
+
+class XLibeufinBankTest {
+    private val mapper = jacksonObjectMapper()
+    @Test
+    fun urlParse() {
+        val u = URL("http://localhost";)
+        println(u.authority)
+    }
+
+    /**
+     * This test tries to submit a transaction to Sandbox
+     * via the x-libeufin-bank connection and later - after
+     * having downloaded its transactions - tries to reconcile
+     * it as sent.
+     */
+    @Test
+    fun submitTransaction() {
+
+    }
+
+    /**
+     * Testing that Nexus downloads one transaction from
+     * Sandbox via the x-libeufin-bank protocol supplier
+     * and stores it in the Nexus internal transactions
+     * table.
+     *
+     * NOTE: the test should be extended by checking that
+     * downloading twice the transaction doesn't lead to asset
+     * duplication locally in Nexus.
+     */
+    @Test
+    fun fetchTransaction() {
+        withTestDatabase {
+            prepSandboxDb()
+            prepNexusDb()
+            testApplication {
+                // Creating the Sandbox transaction that's expected to be 
ingested.
+                wireTransfer(
+                    debitAccount = "bar",
+                    creditAccount = "foo",
+                    demobank = "default",
+                    subject = "x-libeufin-bank test transaction",
+                    amount = "TESTKUDOS:333"
+                )
+                val fooUser = getNexusUser("foo")
+                // Creating the x-libeufin-bank connection to interact with 
Sandbox.
+                val conn = XlibeufinBankConnectionProtocol()
+                val jDetails = """{
+                    "username": "foo",
+                    "password": "foo",
+                    "baseUrl": "http://localhost/demobanks/default/access-api";
+                    }""".trimIndent()
+                conn.createConnection(
+                    connId = "x",
+                    user = fooUser,
+                    data = mapper.readTree(jDetails)
+                )
+                // Starting _Sandbox_ to check how it reacts to Nexus request.
+                application(sandboxApp)
+                /**
+                 * Doing two rounds of download: the first is expected to
+                 * record the payment as new, and the second is expected to
+                 * ignore it because it has already it in the database.
+                 */
+                repeat(2) {
+                    // Invoke transaction fetcher offered by the 
x-libeufin-bank connection.
+                    conn.fetchTransactions(
+                        fetchSpec = FetchSpecAllJson(
+                            FetchLevel.STATEMENT,
+                            null
+                        ),
+                        accountId = "foo",
+                        bankConnectionId = "x",
+                        client = client
+                    )
+                }
+                // The messages are in the database now, invoke the
+                // ingestion routine to parse them into the Nexus internal
+                // format.
+                ingestBankMessagesIntoAccount("x", "foo")
+                // Asserting that the payment made it to the database in the 
Nexus format.
+                transaction {
+                    val maybeTx = NexusBankTransactionEntity.all()
+                    // This assertion checks that the payment is not doubled 
in the database:
+                    assert(maybeTx.count() == 1L)
+                    val tx = 
maybeTx.first().parseDetailsIntoObject<CamtBankAccountEntry>()
+                    assert(tx.getSingletonSubject() == "x-libeufin-bank test 
transaction")
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
index 8cd2750e..773d6450 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/CircuitApi.kt
@@ -145,17 +145,33 @@ fun generateCashoutSubject(
             " to ${amountCredit.currency}:${amountCredit.amount}"
 }
 
-/* Takes one amount value as input, applies cash-out rates
-* and fees to it, and returns the result.  Typically, the input
-* comes from a regional currency amount and the output will be
-* the fiat currency amount that the customer will get in their
-* fiat bank account. */
+fun BigDecimal.roundToTwoDigits(): BigDecimal {
+    val twoDigitsRounding = MathContext(2)
+    return this.round(twoDigitsRounding)
+}
+
+/**
+ * By default, it takes the amount in the regional currency
+ * and applies ratio and fees to convert it to fiat.  If the
+ * 'fromCredit' parameter is true, then it does the inverse
+ * operation: returns the regional amount that would lead to
+ * such fiat amount given in the 'amount' parameter.
+ */
 fun applyCashoutRatioAndFee(
-    regioAmount: BigDecimal,
-    ratiosAndFees: RatioAndFees
-): BigDecimal =
-    (regioAmount * ratiosAndFees.sell_at_ratio.toBigDecimal()) -
-            ratiosAndFees.sell_out_fee.toBigDecimal()
+    amount: BigDecimal,
+    ratiosAndFees: RatioAndFees,
+    fromCredit: Boolean = false
+): BigDecimal {
+    // Normal case, when the calculation starts from the regional amount.
+    if (!fromCredit) {
+        return ((amount * ratiosAndFees.sell_at_ratio.toBigDecimal()) -
+                ratiosAndFees.sell_out_fee.toBigDecimal()).roundToTwoDigits()
+    }
+    // UI convenient case, when the calculation start from the
+    // desired fiat amount that the user wants eventually be paid.
+    return ((amount + ratiosAndFees.sell_out_fee.toBigDecimal()) /
+            ratiosAndFees.sell_at_ratio.toBigDecimal()).roundToTwoDigits()
+}
 
 /**
  * NOTE: future versions take the supported TAN method from
@@ -379,20 +395,56 @@ fun circuitApi(circuitRoute: Route) {
     }
     circuitRoute.get("/cashouts/estimates") {
         call.request.basicAuth()
-        val maybeAmountDebit: String? = 
call.request.queryParameters["amount_debit"]
-        if (maybeAmountDebit == null) throw badRequest("Missing 'amount_debit' 
URI parameter.")
-        val amountDebit = parseAmount(maybeAmountDebit)
         val demobank = ensureDemobank(call)
-        if (amountDebit.currency != demobank.config.currency)
-            throw badRequest("POSTed debit amount has wrong currency 
(${amountDebit.currency}).  Give '${demobank.config.currency}' instead.")
-        val amountDebitValue = try {
-            amountDebit.amount.toBigDecimal()
-        } catch (e: Exception) { throw badRequest("POSTed debit amount has 
invalid number.") }
-        val estimate = applyCashoutRatioAndFee(amountDebitValue, ratiosAndFees)
-        val twoDigitsRounding = MathContext(2)
-        val estimateRounded = estimate.round(twoDigitsRounding)
-        call.respond(object { val amount_credit = 
"$FIAT_CURRENCY:$estimateRounded" })
+        // Optionally parsing param 'amount_debit' into number and checking 
its currency
+        val maybeAmountDebit: String? = 
call.request.queryParameters["amount_debit"]
+        val amountDebit: BigDecimal? = if (maybeAmountDebit != null) {
+            val amount = parseAmount(maybeAmountDebit)
+            if (amount.currency != demobank.config.currency) throw badRequest(
+                "parameter 'amount_debit' has the wrong currency: 
${amount.currency}"
+            )
+            try { amount.amount.toBigDecimal() } catch (e: Exception) {
+                throw badRequest("Cannot extract a number from 'amount_debit'")
+            }
+        } else null
+        // Optionally parsing param 'amount_credit' into number and checking 
its currency
+        val maybeAmountCredit: String? = 
call.request.queryParameters["amount_credit"]
+        val amountCredit: BigDecimal? = if (maybeAmountCredit != null) {
+            val amount = parseAmount(maybeAmountCredit)
+            if (amount.currency != FIAT_CURRENCY) throw badRequest(
+                "parameter 'amount_credit' has the wrong currency: 
${amount.currency}"
+            )
+            try { amount.amount.toBigDecimal() } catch (e: Exception) {
+                throw badRequest("Cannot extract a number from 
'amount_credit'")
+            }
+        } else null
+        val respAmountCredit = if (amountDebit != null) {
+            val estimate = applyCashoutRatioAndFee(amountDebit, ratiosAndFees)
+            if (amountCredit != null && estimate != amountCredit) throw 
badRequest(
+                "Wrong calculation found in 'amount_credit', bank estimates: 
$estimate"
+            )
+            estimate
+        } else null
+        if (amountDebit == null && amountCredit == null) throw badRequest(
+            "Both 'amount_credit' and 'amount_debit' are missing"
+        )
+        val respAmountDebit = if (amountCredit != null) {
+            val estimate = applyCashoutRatioAndFee(
+                amountCredit,
+                ratiosAndFees,
+                fromCredit = true
+            )
+            if (amountDebit != null && estimate != amountDebit) throw 
badRequest(
+                "Wrong calculation found in 'amount_credit', bank estimates: 
$estimate"
+            )
+            estimate
+        } else null
+        call.respond(object {
+            val amount_credit = "$FIAT_CURRENCY:$respAmountCredit"
+            val amount_debit = "${demobank.config.currency}:$respAmountDebit"
+        })
     }
+
     // Create a cash-out operation.
     circuitRoute.post("/cashouts") {
         val user = call.request.basicAuth()
@@ -441,9 +493,8 @@ fun circuitApi(circuitRoute: Route) {
         // check rates correctness
         val amountDebitAsNumber = BigDecimal(amountDebit.amount)
         val expectedAmountCredit = 
applyCashoutRatioAndFee(amountDebitAsNumber, ratiosAndFees)
-        val commonRounding = MathContext(2) // ensures both amounts end with 
".XY"
-        val amountCreditAsNumber = BigDecimal(amountCredit.amount)
-        if (expectedAmountCredit.round(commonRounding) != 
amountCreditAsNumber.round(commonRounding)) {
+        val amountCreditAsNumber = 
BigDecimal(amountCredit.amount).roundToTwoDigits()
+        if (expectedAmountCredit != amountCreditAsNumber) {
             throw badRequest("Rates application are incorrect." +
                     "  The expected amount to credit is: 
${expectedAmountCredit}," +
                     " but ${amountCredit.amount} was specified.")
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
index 00688082..b0654950 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt
@@ -423,6 +423,7 @@ object BankAccountTransactionsTable : LongIdTable() {
     // Amount is a BigDecimal in String form.
     val amount = text("amount")
     val currency = text("currency")
+    // Milliseconds since the Epoch.
     val date = long("date")
     // Unique ID for this payment within the bank account.
     val accountServicerReference = text("accountServicerReference")
diff --git 
a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
index 051d1a09..d02ff7c3 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -235,7 +235,7 @@ fun <T> expectNonNull(x: T?): T {
     return x;
 }
 
-private fun getRelatedParty(branch: XmlElementBuilder, payment: RawPayment) {
+private fun getRelatedParty(branch: XmlElementBuilder, payment: 
XLibeufinBankTransaction) {
     val otherParty = object {
         var ibanPath = "CdtrAcct/Id/IBAN"
         var namePath = "Cdtr/Nm"
@@ -244,7 +244,7 @@ private fun getRelatedParty(branch: XmlElementBuilder, 
payment: RawPayment) {
         var bicPath = "CdtrAgt/FinInstnId/BIC"
         var bic = payment.creditorBic
     }
-    if (payment.direction == "CRDT") {
+    if (payment.direction == XLibeufinBankDirection.CREDIT) {
         otherParty.iban = payment.debtorIban
         otherParty.ibanPath = "DbtrAcct/Id/IBAN"
         otherParty.namePath = "Dbtr/Nm"
@@ -279,7 +279,7 @@ private fun getCreditDebitInd(balance: BigDecimal): String {
 fun buildCamtString(
     type: Int,
     subscriberIban: String,
-    history: MutableList<RawPayment>,
+    history: MutableList<XLibeufinBankTransaction>,
     balancePrcd: BigDecimal, // Balance up to freshHistory (excluded).
     balanceClbd: BigDecimal,
     currency: String
@@ -521,7 +521,7 @@ private fun constructCamtResponse(
     if (type == 52) {
         if (dateRange != null)
             throw EbicsOrderParamsIgnored("C52 does not support date ranges.")
-        val history = mutableListOf<RawPayment>()
+        val history = mutableListOf<XLibeufinBankTransaction>()
         transaction {
             BankAccountFreshTransactionEntity.all().forEach {
                 if (it.transactionRef.account.label == bankAccount.label) {
@@ -545,8 +545,8 @@ private fun constructCamtResponse(
             var base = prcdBalance
             history.forEach { tx ->
                 when (tx.direction) {
-                    "DBIT" -> base -= parseDecimal(tx.amount)
-                    "CRDT" -> base += parseDecimal(tx.amount)
+                    XLibeufinBankDirection.DEBIT -> base -= 
parseDecimal(tx.amount)
+                    XLibeufinBankDirection.CREDIT -> base += 
parseDecimal(tx.amount)
                     else -> {
                         logger.error("Transaction with subject '${tx.subject}' 
is " +
                                 "inconsistent: neither DBIT nor CRDT")
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index 8fe70541..5d492914 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -215,8 +215,8 @@ fun getOrderTypeFromTransactionId(transactionID: String): 
String {
     return uploadTransaction.orderType
 }
 
-fun getHistoryElementFromTransactionRow(dbRow: BankAccountTransactionEntity): 
RawPayment {
-    return RawPayment(
+fun getHistoryElementFromTransactionRow(dbRow: BankAccountTransactionEntity): 
XLibeufinBankTransaction {
+    return XLibeufinBankTransaction(
         subject = dbRow.subject,
         creditorIban = dbRow.creditorIban,
         creditorBic = dbRow.creditorBic,
@@ -231,7 +231,8 @@ fun getHistoryElementFromTransactionRow(dbRow: 
BankAccountTransactionEntity): Ra
         // and dbRow makes the document invalid!
         // uid = "${dbRow.pmtInfId}-${it.msgId}"
         uid = dbRow.accountServicerReference,
-        direction = dbRow.direction,
+        // Eventually, the _database_ should contain the direction enum:
+        direction = 
XLibeufinBankDirection.convertCamtDirectionToXLibeufin(dbRow.direction),
         pmtInfId = dbRow.pmtInfId
     )
 }
@@ -256,7 +257,7 @@ fun printConfig(demobank: DemobankConfigEntity) {
 
 fun getHistoryElementFromTransactionRow(
     dbRow: BankAccountFreshTransactionEntity
-): RawPayment {
+): XLibeufinBankTransaction {
     return getHistoryElementFromTransactionRow(dbRow.transactionRef)
 }
 
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 0d2a80d0..a1a4d70b 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -225,7 +225,7 @@ class Camt053Tick : CliktCommand(
         val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
         Database.connect(dbConnString)
         dbCreateTables(dbConnString)
-        val newStatements = mutableMapOf<String, MutableList<RawPayment>>()
+        val newStatements = mutableMapOf<String, 
MutableList<XLibeufinBankTransaction>>()
         /**
          * For each bank account, extract the latest statement and
          * include all the later transactions in a new statement.
@@ -1336,8 +1336,8 @@ val sandboxApp: Application.() -> Unit = {
                     val baseUrl = URL(call.request.getBaseUrl())
                     val withdrawUri = url {
                         protocol = URLProtocol(
-                            "taler".plus(if (baseUrl.protocol.lowercase() == 
"http") "+http" else ""),
-                            -1
+                            name = "taler".plus(if 
(baseUrl.protocol.lowercase() == "http") "+http" else ""),
+                            defaultPort = -1
                         )
                         host = "withdraw"
                         val pathSegments = mutableListOf(
@@ -1492,7 +1492,7 @@ val sandboxApp: Application.() -> Unit = {
                     if (fromMs < 0) throw badRequest("'from_ms' param is less 
than 0")
                     val untilMs = 
expectLong(call.request.queryParameters["until_ms"] ?: 
Long.MAX_VALUE.toString())
                     if (untilMs < 0) throw badRequest("'until_ms' param is 
less than 0")
-                    val ret = mutableListOf<RawPayment>()
+                    val ret = mutableListOf<XLibeufinBankTransaction>()
                     /**
                      * Case where page number wasn't given,
                      * therefore the results starts from the last transaction.
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
index 2361b876..2ebe5fc2 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt
@@ -6,6 +6,8 @@ import org.jetbrains.exposed.sql.transactions.transaction
 import tech.libeufin.util.*
 import java.math.BigDecimal
 
+
+
 /**
  * Check whether the given bank account would surpass the
  * debit threshold, in case the potential amount gets transferred.
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
index beb5d12f..90189257 100644
--- a/util/src/main/kotlin/DB.kt
+++ b/util/src/main/kotlin/DB.kt
@@ -20,24 +20,28 @@
 package tech.libeufin.util
 import UtilError
 import io.ktor.http.*
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
 import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.withContext
 import logger
 import net.taler.wallet.crypto.Base32Crockford
+import org.jetbrains.exposed.sql.Database
 import org.jetbrains.exposed.sql.Transaction
+import org.jetbrains.exposed.sql.transactions.TransactionManager
+import org.jetbrains.exposed.sql.transactions.transaction
+import org.jetbrains.exposed.sql.transactions.transactionManager
+import org.postgresql.PGNotification
 import org.postgresql.jdbc.PgConnection
 
 fun Transaction.isPostgres(): Boolean {
     return this.db.vendor == "postgresql"
 }
 
-fun Transaction.getPgConnection(): PgConnection {
-    if (!this.isPostgres()) throw UtilError(
-        HttpStatusCode.InternalServerError,
-        "Unexpected non-postgresql connection: ${this.db.vendor}"
+fun isPostgres(): Boolean {
+    val db = TransactionManager.defaultDatabase ?: throw internalServerError(
+        "Could not find the default database, can't check if that's Postgres."
     )
-    return this.db.connector().connection as PgConnection
+    return db.vendor == "postgresql"
+
 }
 
 // Check GANA (https://docs.gnunet.org/gana/index.html) for numbers allowance.
@@ -52,55 +56,68 @@ fun buildChannelName(
     separator: String = "_"
 ): String {
     val channelElements = "${domain.value}$separator$iban"
-    return 
"X${Base32Crockford.encode(CryptoUtil.hashStringSHA256(channelElements))}"
+    val ret = 
"X${Base32Crockford.encode(CryptoUtil.hashStringSHA256(channelElements))}"
+    logger.debug("Defining db channel name for IBAN: $iban, domain: 
${domain.name}, resulting in: $ret")
+    return ret
 }
 
-// This class abstracts Postgres' LISTEN/NOTIFY.
-// FIXME: find facts where Exposed provides always a live 'conn'.
-class PostgresListenNotify(val conn: PgConnection, val channel: String) {
-    fun postrgesListen() {
-        val stmt = conn.createStatement()
-        stmt.execute("LISTEN $channel")
-        stmt.close()
-    }
-    fun postgresNotify() {
+fun Transaction.postgresNotify(channel: String) {
+    this.exec("NOTIFY $channel")
+}
+
+/**
+ * postgresListen() and postgresGetNotifications() appear to have
+ * to use the same connection, in order for the notifications to
+ * arrive.  Therefore, calling LISTEN inside one "transaction {}"
+ * and postgresGetNotifications() outside of it did NOT work because
+ * Exposed _closes_ the connection as soon as the transaction block
+ * completes. OTOH, calling postgresGetNotifications() _inside_ the
+ * same transaction block as LISTEN's would lead to keep the database
+ * locked for the timeout duration.
+ *
+ * For this reason, opening and keeping one connection open for the
+ * lifetime of this object and only executing postgresListen() and
+ * postgresGetNotifications() _on that connection_ makes the event
+ * delivery more reliable.
+ */
+class PostgresListenHandle(val channelName: String) {
+    private val db = TransactionManager.defaultDatabase ?: throw 
internalServerError(
+        "Could not find the default database, won't get Postgres 
notifications."
+    )
+    private val conn = db.connector().connection as PgConnection
+
+    fun postgresListen() {
         val stmt = conn.createStatement()
-        stmt.execute("NOTIFY $channel")
+        stmt.execute("LISTEN $channelName")
         stmt.close()
+        logger.debug("LISTENing on channel: $channelName")
     }
-
     fun postgresUnlisten() {
         val stmt = conn.createStatement()
-        stmt.execute("UNLISTEN $channel")
+        stmt.execute("UNLISTEN $channelName")
         stmt.close()
+        logger.debug("UNLISTENing on channel: $channelName")
+        conn.close()
     }
 
-    /**
-     * Asks Postgres for notifications with a timeout.  Returns
-     * true when there have been, false otherwise.
-     */
-    fun postgresWaitNotification(timeoutMs: Long): Boolean {
+    fun postgresGetNotifications(timeoutMs: Long): Boolean {
         if (timeoutMs == 0L)
             logger.warn("Database notification checker has timeout == 0," +
                     " that waits FOREVER until a notification arrives."
             )
-        val maybeNotifications = conn.getNotifications(timeoutMs.toInt())
-
-        /**
-         * This check works around the apparent API inconsistency
-         * where instead of returning null, a empty array is given
-         * back when there have been no notifications.
-         */
-        val noResultWorkaround = maybeNotifications.isEmpty()
-        /*if (noResultWorkaround) {
-            logger.warn("JDBC+Postgres: empty array from getNotifications() 
despite docs suggest null.")
-        }*/
-        if (maybeNotifications == null || noResultWorkaround) return false
-
+        logger.debug("Waiting Postgres notifications on channel " +
+                "'$channelName' for $timeoutMs millis.")
+        val maybeNotifications = this.conn.getNotifications(timeoutMs.toInt())
+        if (maybeNotifications == null || maybeNotifications.isEmpty()) {
+            logger.debug("DB notification channel $channelName was found 
empty.")
+            return false
+        }
         for (n in maybeNotifications) {
-            if (n.name.lowercase() != this.channel.lowercase())
-                throw internalServerError("Channel ${this.channel} got 
notified from ${n.name}!")
+            if (n.name.lowercase() != channelName.lowercase()) {
+                throw internalServerError("Channel $channelName got notified 
from ${n.name}!")
+            }
         }
+        logger.debug("Found DB notifications on channel $channelName")
         return true
     }
 }
\ No newline at end of file
diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt
index e54cdd77..96ee1e05 100644
--- a/util/src/main/kotlin/JSON.kt
+++ b/util/src/main/kotlin/JSON.kt
@@ -19,14 +19,40 @@
 
 package tech.libeufin.util
 
-/**
- * (Very) generic information about one payment.  Can be
- * derived from a CAMT response, or from a prepared PAIN
- * document.
- *
- * Note:
- */
-data class RawPayment(
+enum class XLibeufinBankDirection(val direction: String) {
+    DEBIT("debit"),
+    CREDIT("credit");
+    companion object {
+        fun parseXLibeufinDirection(direction: String): XLibeufinBankDirection 
{
+            return when(direction) {
+                "credit" -> CREDIT
+                "debit" -> DEBIT
+                else -> throw internalServerError(
+                    "Cannot extract ${this::class.java.typeName}' instance 
from value: $direction'"
+                )
+            }
+        }
+
+        /**
+         * Sandbox uses _some_ CaMt terminology even for its internal
+         * data model.  This function helps to bridge such CaMt terminology
+         * to the Sandbox simplified JSON format (XLibeufinBankTransaction).
+         *
+         * Ideally, the terminology should be made more abstract to serve
+         * both (and probably more) data formats.
+         */
+        fun convertCamtDirectionToXLibeufin(camtDirection: String): 
XLibeufinBankDirection {
+            return when(camtDirection) {
+                "CRDT" -> CREDIT
+                "DBIT" -> DEBIT
+                else -> throw internalServerError(
+                    "Cannot extract ${this::class.java.typeName}' instance 
from value: $camtDirection'"
+                )
+            }
+        }
+    }
+}
+data class XLibeufinBankTransaction(
     val creditorIban: String,
     val creditorBic: String?,
     val creditorName: String,
@@ -36,17 +62,16 @@ data class RawPayment(
     val amount: String,
     val currency: String,
     val subject: String,
+    // Milliseconds since the Epoch.
     val date: String,
-    val uid: String, // FIXME: explain this value.
-    val direction: String, // FIXME: this following value should be restricted 
to only DBIT/CRDT.
-
+    val uid: String,
+    val direction: XLibeufinBankDirection,
     // The following two values are rather CAMT/PAIN
     // specific, therefore do not need to be returned
     // along every API call using this object.
     val pmtInfId: String? = null,
     val msgId: String? = null
 )
-
 data class IncomingPaymentInfo(
     val debtorIban: String,
     val debtorBic: String?,
diff --git a/util/src/main/kotlin/LibeufinErrorCodes.kt 
b/util/src/main/kotlin/LibeufinErrorCodes.kt
index 758292e3..e60b4015 100644
--- a/util/src/main/kotlin/LibeufinErrorCodes.kt
+++ b/util/src/main/kotlin/LibeufinErrorCodes.kt
@@ -51,7 +51,7 @@ enum class LibeufinErrorCode(val code: Int) {
     LIBEUFIN_EC_INCONSISTENT_STATE(3),
 
     /**
-     * An access was forbidden due to wrong credentials.
+     * Access was forbidden due to wrong credentials.
      */
     LIBEUFIN_EC_AUTHENTICATION_FAILED(4),
 

-- 
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]