gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] branch master updated (fe31400 -> 6144c4a)


From: gnunet
Subject: [libeufin] branch master updated (fe31400 -> 6144c4a)
Date: Wed, 06 Oct 2021 16:17:51 +0200

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

ms pushed a change to branch master
in repository libeufin.

    from fe31400  demobank notes and skeleton
     new ffef8c7  Unix domain socket server.
     new 3ab168c  Unix domain sockets dependencies
     new 3552c78  adapt test to new API
     new e1c7fdf  test for Unix domain socket
     new 2b788ab  test dep
     new b4942a1  Unix domain socket proxy: fix payload read/write.
     new 6144c4a  Serve Sandbox via Unix domain socket.

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:
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  | 1248 ++++++++++----------
 sandbox/src/test/kotlin/CamtTest.kt                |   34 -
 sandbox/src/test/kotlin/DBTest.kt                  |    2 +-
 util/build.gradle                                  |    6 +
 util/src/main/kotlin/UnixDomainSocket.kt           |   97 ++
 util/src/test/kotlin/DomainSocketTest.kt           |   30 +
 6 files changed, 763 insertions(+), 654 deletions(-)
 delete mode 100644 sandbox/src/test/kotlin/CamtTest.kt
 create mode 100644 util/src/main/kotlin/UnixDomainSocket.kt
 create mode 100644 util/src/test/kotlin/DomainSocketTest.kt

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index f70f7ed..fbde053 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -38,35 +38,21 @@ package tech.libeufin.sandbox
 
 import UtilError
 import com.fasterxml.jackson.core.JsonParseException
-import io.ktor.application.ApplicationCallPipeline
-import io.ktor.application.call
-import io.ktor.application.install
-import io.ktor.features.CallLogging
-import io.ktor.features.ContentNegotiation
-import io.ktor.features.StatusPages
-import io.ktor.response.respond
-import io.ktor.response.respondText
 import io.ktor.server.engine.embeddedServer
 import org.jetbrains.exposed.sql.*
 import org.jetbrains.exposed.sql.transactions.transaction
-import io.ktor.jackson.jackson
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
-import org.slf4j.event.Level
 import org.w3c.dom.Document
+import io.ktor.jackson.*
 import tech.libeufin.util.CryptoUtil
 import tech.libeufin.util.RawPayment
 import java.lang.ArithmeticException
 import java.math.BigDecimal
 import java.security.interfaces.RSAPublicKey
 import javax.xml.bind.JAXBContext
-import com.fasterxml.jackson.core.util.DefaultIndenter
-import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
-import com.fasterxml.jackson.databind.SerializationFeature
 import com.fasterxml.jackson.databind.exc.MismatchedInputException
-import com.fasterxml.jackson.module.kotlin.KotlinModule
 import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
-import org.jetbrains.exposed.sql.statements.api.ExposedBlob
 import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.core.ProgramResult
 import com.github.ajalt.clikt.core.context
@@ -77,21 +63,33 @@ import com.github.ajalt.clikt.parameters.options.*
 import com.github.ajalt.clikt.parameters.types.int
 import execThrowableOrTerminate
 import io.ktor.application.ApplicationCall
+import io.ktor.application.ApplicationCallPipeline
+import io.ktor.application.call
+import io.ktor.application.install
+import io.ktor.features.CallLogging
+import io.ktor.features.ContentNegotiation
+import org.jetbrains.exposed.sql.statements.api.ExposedBlob
+import com.fasterxml.jackson.core.util.DefaultIndenter
+import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
+import com.fasterxml.jackson.module.kotlin.KotlinModule
+import com.fasterxml.jackson.databind.SerializationFeature
+import 
org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
+import io.ktor.features.StatusPages
+import io.ktor.response.respond
+import io.ktor.response.respondText
 import io.ktor.auth.*
 import io.ktor.http.*
 import io.ktor.request.*
 import io.ktor.routing.*
 import io.ktor.server.netty.*
 import io.ktor.util.date.*
+import io.ktor.application.*
 import kotlinx.coroutines.newSingleThreadContext
-import 
org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction
+import startServer
 import tech.libeufin.util.*
-import tech.libeufin.util.ebics_h004.EbicsResponse
-import tech.libeufin.util.ebics_h004.EbicsTypes
 import validatePlainAmount
 import java.net.BindException
 import java.util.*
-import kotlin.random.Random
 import kotlin.system.exitProcess
 
 private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox")
@@ -290,9 +288,22 @@ class Serve : CliktCommand("Run sandbox HTTP server") {
 
     private val logLevel by option()
     private val port by option().int().default(5000)
+    private val withUnixSocket by option(
+        help = "Bind the Sandbox to the Unix domain socket at PATH.  
Overrides" +
+                "--port, when both are given", metavar = "PATH"
+    )
     override fun run() {
         setLogLevel(logLevel)
-        serverMain(getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME), port)
+        val dbName = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME)
+        if (withUnixSocket != null) {
+            execThrowableOrTerminate { dbCreateTables(dbName) }
+            startServer(
+                withUnixSocket ?: throw Exception("Could not use the Unix 
domain socket path value!"),
+                app = sandboxApp
+            )
+            exitProcess(0)
+        }
+        serverMain(dbName, port)
     }
 }
 
@@ -384,703 +395,702 @@ suspend inline fun <reified T : Any> 
ApplicationCall.receiveJson(): T {
 }
 
 val singleThreadContext = newSingleThreadContext("DB")
-
-fun serverMain(dbName: String, port: Int) {
-    execThrowableOrTerminate { dbCreateTables(dbName) }
-    val myLogger = logger
-    val server = embeddedServer(Netty, port = port) {
-        install(CallLogging) {
-            this.level = Level.DEBUG
-            this.logger = myLogger
-        }
-        install(Authentication) {
-            // Web-based authentication for Bank customers.
-            form("auth-form") {
-                userParamName = "username"
-                passwordParamName = "password"
-                validate { credentials ->
-                    if (credentials.name == "test") {
-                        UserIdPrincipal(credentials.name)
-                    } else {
-                        null
-                    }
+val sandboxApp: Application.() -> Unit = {
+    install(io.ktor.features.CallLogging) {
+        this.level = org.slf4j.event.Level.DEBUG
+        this.logger = logger
+    }
+    install(Authentication) {
+        // Web-based authentication for Bank customers.
+        form("auth-form") {
+            userParamName = "username"
+            passwordParamName = "password"
+            validate { credentials ->
+                if (credentials.name == "test") {
+                    UserIdPrincipal(credentials.name)
+                } else {
+                    null
                 }
             }
         }
-        install(ContentNegotiation) {
-            jackson {
-                enable(SerializationFeature.INDENT_OUTPUT)
-                setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
-                    
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
-                    indentObjectsWith(DefaultIndenter("  ", "\n"))
-                })
-                registerModule(KotlinModule(nullisSameAsDefault = true))
-                //registerModule(JavaTimeModule())
-            }
+    }
+    install(io.ktor.features.ContentNegotiation) {
+        jackson {
+            
enable(com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT)
+            setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
+                
indentArraysWith(com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter.instance)
+                indentObjectsWith(DefaultIndenter("  ", "\n"))
+            })
+            registerModule(KotlinModule(nullisSameAsDefault = true))
+            //registerModule(JavaTimeModule())
         }
-        install(StatusPages) {
-            exception<ArithmeticException> { cause ->
-                logger.error("Exception while handling '${call.request.uri}'", 
cause)
-                call.respondText(
-                    "Invalid arithmetic attempted.",
-                    ContentType.Text.Plain,
-                    // here is always the bank's fault, as it should always 
check
-                    // the operands.
-                    HttpStatusCode.InternalServerError
+    }
+    install(StatusPages) {
+        exception<ArithmeticException> { cause ->
+            logger.error("Exception while handling '${call.request.uri}'", 
cause)
+            call.respondText(
+                "Invalid arithmetic attempted.",
+                io.ktor.http.ContentType.Text.Plain,
+                // here is always the bank's fault, as it should always check
+                // the operands.
+                io.ktor.http.HttpStatusCode.InternalServerError
+            )
+        }
+        exception<EbicsRequestError> { cause ->
+            val resp = 
tech.libeufin.util.ebics_h004.EbicsResponse.createForUploadWithError(
+                cause.errorText,
+                cause.errorCode,
+                // assuming that the phase is always transfer,
+                // as errors during initialization should have
+                // already been caught by the chunking logic.
+                
tech.libeufin.util.ebics_h004.EbicsTypes.TransactionPhaseType.TRANSFER
+            )
+
+            val hostAuthPriv = transaction {
+                val host = tech.libeufin.sandbox.EbicsHostEntity.find {
+                    tech.libeufin.sandbox.EbicsHostsTable.hostID.upperCase() 
eq call.attributes.get(tech.libeufin.sandbox.EbicsHostIdAttribute).uppercase()
+                }.firstOrNull() ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.InternalServerError,
+                    "Requested Ebics host ID not found."
                 )
+                
tech.libeufin.util.CryptoUtil.loadRsaPrivateKey(host.authenticationPrivateKey.bytes)
             }
-            exception<EbicsRequestError> { cause ->
-                val resp = EbicsResponse.createForUploadWithError(
-                    cause.errorText,
-                    cause.errorCode,
-                    // assuming that the phase is always transfer,
-                    // as errors during initialization should have
-                    // already been caught by the chunking logic.
-                    EbicsTypes.TransactionPhaseType.TRANSFER
-                )
-
-                val hostAuthPriv = transaction {
-                    val host = EbicsHostEntity.find {
-                        EbicsHostsTable.hostID.upperCase() eq 
call.attributes.get(EbicsHostIdAttribute).uppercase()
-                    }.firstOrNull() ?: throw SandboxError(
-                        HttpStatusCode.InternalServerError,
-                        "Requested Ebics host ID not found."
+            call.respondText(
+                tech.libeufin.util.XMLUtil.signEbicsResponse(resp, 
hostAuthPriv),
+                io.ktor.http.ContentType.Application.Xml,
+                io.ktor.http.HttpStatusCode.OK
+            )
+        }
+        exception<SandboxError> { cause ->
+            logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
+            call.respond(
+                cause.statusCode,
+                SandboxErrorJson(
+                    error = SandboxErrorDetailJson(
+                        type = "sandbox-error",
+                        description = cause.reason
                     )
-                    
CryptoUtil.loadRsaPrivateKey(host.authenticationPrivateKey.bytes)
-                }
-                call.respondText(
-                    XMLUtil.signEbicsResponse(resp, hostAuthPriv),
-                    ContentType.Application.Xml,
-                    HttpStatusCode.OK
                 )
-            }
-            exception<SandboxError> { cause ->
-                logger.error("Exception while handling '${call.request.uri}', 
${cause.reason}")
-                call.respond(
-                    cause.statusCode,
-                    SandboxErrorJson(
-                        error = SandboxErrorDetailJson(
-                            type = "sandbox-error",
-                            description = cause.reason
-                        )
+            )
+        }
+        exception<UtilError> { cause ->
+            logger.error("Exception while handling '${call.request.uri}'", 
cause)
+            call.respond(
+                cause.statusCode,
+                SandboxErrorJson(
+                    error = SandboxErrorDetailJson(
+                        type = "util-error",
+                        description = cause.reason
                     )
                 )
-            }
-            exception<UtilError> { cause ->
-                logger.error("Exception while handling '${call.request.uri}'", 
cause)
-                call.respond(
-                    cause.statusCode,
-                    SandboxErrorJson(
-                        error = SandboxErrorDetailJson(
-                            type = "util-error",
-                            description = cause.reason
-                        )
-                    )
+            )
+        }
+        exception<Throwable> { cause ->
+            logger.error("Exception while handling '${call.request.uri}'", 
cause)
+            call.respondText("Internal server error.", 
io.ktor.http.ContentType.Text.Plain, 
io.ktor.http.HttpStatusCode.InternalServerError)
+        }
+    }
+    intercept(io.ktor.application.ApplicationCallPipeline.Fallback) {
+        if (this.call.response.status() == null) {
+            call.respondText("Not found (no route matched).\n", 
io.ktor.http.ContentType.Text.Plain, io.ktor.http.HttpStatusCode.NotFound)
+            return@intercept finish()
+        }
+    }
+    routing {
+
+        get("/") {
+            call.respondText("Hello, this is Sandbox\n", 
io.ktor.http.ContentType.Text.Plain)
+        }
+        get("/config") {
+            call.respond(object {
+                val name = "libeufin-sandbox"
+
+                // FIXME: use actual version here!
+                val version = "0.0.0-dev.0"
+            })
+        }
+        /**
+         * For now, only returns the last statement of the
+         * requesting account.
+         */
+        post("/admin/payments/camt") {
+            requireSuperuser(call.request)
+            val body = call.receiveJson<CamtParams>()
+            val bankaccount = getAccountFromLabel(body.bankaccount)
+            if (body.type != 53) throw SandboxError(
+                io.ktor.http.HttpStatusCode.NotFound,
+                "Only Camt.053 documents can be generated."
+            )
+            val camtMessage = transaction {
+                tech.libeufin.sandbox.BankAccountStatementEntity.find {
+                    
tech.libeufin.sandbox.BankAccountStatementsTable.bankAccount eq bankaccount.id
+                }.lastOrNull()?.xmlMessage ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.NotFound,
+                    "Could not find any statements; please wait next tick"
                 )
             }
-            exception<Throwable> { cause ->
-                logger.error("Exception while handling '${call.request.uri}'", 
cause)
-                call.respondText("Internal server error.", 
ContentType.Text.Plain, HttpStatusCode.InternalServerError)
-            }
+            call.respondText(
+                camtMessage, io.ktor.http.ContentType.Text.Xml, 
io.ktor.http.HttpStatusCode.OK
+            )
+            return@post
         }
-        intercept(ApplicationCallPipeline.Fallback) {
-            if (this.call.response.status() == null) {
-                call.respondText("Not found (no route matched).\n", 
ContentType.Text.Plain, HttpStatusCode.NotFound)
-                return@intercept finish()
+
+        post("/admin/bank-accounts/{label}") {
+            requireSuperuser(call.request)
+            val body = call.receiveJson<BankAccountInfo>()
+            transaction {
+                tech.libeufin.sandbox.BankAccountEntity.new {
+                    iban = body.iban
+                    bic = body.bic
+                    name = body.name
+                    label = body.label
+                    currency = body.currency ?: "EUR"
+                }
             }
+            call.respond(object {})
+            return@post
         }
-        routing {
 
-            get("/") {
-                call.respondText("Hello, this is Sandbox\n", 
ContentType.Text.Plain)
+        get("/admin/bank-accounts/{label}") {
+            requireSuperuser(call.request)
+            val label = ensureNonNull(call.parameters["label"])
+            val ret = transaction {
+                val bankAccount = tech.libeufin.sandbox.BankAccountEntity.find 
{
+                    tech.libeufin.sandbox.BankAccountsTable.label eq label
+                }.firstOrNull() ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.NotFound,
+                    "Account '$label' not found"
+                )
+                val balance = balanceForAccount(bankAccount)
+                object {
+                    val balance = "${bankAccount.currency}:${balance}"
+                    val iban = bankAccount.iban
+                    val bic = bankAccount.bic
+                    val name = bankAccount.name
+                    val label = bankAccount.label
+                }
             }
-            get("/config") {
-                call.respond(object {
-                    val name = "libeufin-sandbox"
+            call.respond(ret)
+            return@get
+        }
 
-                    // FIXME: use actual version here!
-                    val version = "0.0.0-dev.0"
-                })
+        post("/admin/bank-accounts/{label}/simulate-incoming-transaction") {
+            requireSuperuser(call.request)
+            val body = call.receiveJson<IncomingPaymentInfo>()
+            // FIXME: generate nicer UUID!
+            val accountLabel = ensureNonNull(call.parameters["label"])
+            if (!validatePlainAmount(body.amount)) {
+                throw SandboxError(
+                    io.ktor.http.HttpStatusCode.BadRequest,
+                    "invalid amount (should be plain amount without currency)"
+                )
             }
-            /**
-             * For now, only returns the last statement of the
-             * requesting account.
-             */
-            post("/admin/payments/camt") {
-                requireSuperuser(call.request)
-                val body = call.receiveJson<CamtParams>()
-                val bankaccount = getAccountFromLabel(body.bankaccount)
-                if (body.type != 53) throw SandboxError(
-                    HttpStatusCode.NotFound,
-                    "Only Camt.053 documents can be generated."
+            val reqDebtorBic = body.debtorBic
+            if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
+                throw SandboxError(
+                    io.ktor.http.HttpStatusCode.BadRequest,
+                    "invalid BIC"
                 )
-                val camtMessage = transaction {
-                    BankAccountStatementEntity.find {
-                        BankAccountStatementsTable.bankAccount eq 
bankaccount.id
-                    }.lastOrNull()?.xmlMessage ?: throw SandboxError(
-                        HttpStatusCode.NotFound,
-                        "Could not find any statements; please wait next tick"
-                    )
+            }
+            transaction {
+                val account = getBankAccountFromLabel(accountLabel)
+                val randId = getRandomString(16)
+                tech.libeufin.sandbox.BankAccountTransactionEntity.new {
+                    creditorIban = account.iban
+                    creditorBic = account.bic
+                    creditorName = account.name
+                    debtorIban = body.debtorIban
+                    debtorBic = reqDebtorBic
+                    debtorName = body.debtorName
+                    subject = body.subject
+                    amount = body.amount
+                    currency = account.currency
+                    date = getUTCnow().toInstant().toEpochMilli()
+                    accountServicerReference = "sandbox-$randId"
+                    this.account = account
+                    direction = "CRDT"
                 }
-                call.respondText(
-                    camtMessage, ContentType.Text.Xml, HttpStatusCode.OK
-                )
-                return@post
             }
+            call.respond(object {})
+        }
 
-            post("/admin/bank-accounts/{label}") {
-                requireSuperuser(call.request)
-                val body = call.receiveJson<BankAccountInfo>()
-                transaction {
-                    BankAccountEntity.new {
-                        iban = body.iban
-                        bic = body.bic
-                        name = body.name
-                        label = body.label
-                        currency = body.currency ?: "EUR"
-                    }
+        /**
+         * Associates a new bank account with an existing Ebics subscriber.
+         */
+        post("/admin/ebics/bank-accounts") {
+            requireSuperuser(call.request)
+            val body = call.receiveJson<BankAccountRequest>()
+            if (!validateBic(body.bic)) {
+                throw SandboxError(io.ktor.http.HttpStatusCode.BadRequest, 
"invalid BIC (${body.bic})")
+            }
+            transaction {
+                val subscriber = getEbicsSubscriberFromDetails(
+                    body.subscriber.userID,
+                    body.subscriber.partnerID,
+                    body.subscriber.hostID
+                )
+                val check = tech.libeufin.sandbox.BankAccountEntity.find {
+                    tech.libeufin.sandbox.BankAccountsTable.iban eq body.iban 
or (tech.libeufin.sandbox.BankAccountsTable.label eq body.label)
+                }.count()
+                if (check > 0) throw SandboxError(
+                    io.ktor.http.HttpStatusCode.BadRequest,
+                    "Either IBAN or account label were already taken; please 
choose fresh ones"
+                )
+                subscriber.bankAccount = 
tech.libeufin.sandbox.BankAccountEntity.new {
+                    iban = body.iban
+                    bic = body.bic
+                    name = body.name
+                    label = body.label
+                    currency = body.currency.uppercase(java.util.Locale.ROOT)
                 }
-                call.respond(object {})
-                return@post
             }
-
-            get("/admin/bank-accounts/{label}") {
-                requireSuperuser(call.request)
-                val label = ensureNonNull(call.parameters["label"])
-                val ret = transaction {
-                    val bankAccount = BankAccountEntity.find {
-                        BankAccountsTable.label eq label
-                    }.firstOrNull() ?: throw SandboxError(
-                        HttpStatusCode.NotFound,
-                        "Account '$label' not found"
+            call.respondText("Bank account created")
+            return@post
+        }
+        get("/admin/bank-accounts") {
+            requireSuperuser(call.request)
+            val accounts = mutableListOf<BankAccountInfo>()
+            transaction {
+                tech.libeufin.sandbox.BankAccountEntity.all().forEach {
+                    accounts.add(
+                        BankAccountInfo(
+                            label = it.label,
+                            name = it.name,
+                            bic = it.bic,
+                            iban = it.iban,
+                            currency = it.currency
+                        )
                     )
-                    val balance = balanceForAccount(bankAccount)
-                    object {
-                        val balance = "${bankAccount.currency}:${balance}"
-                        val iban = bankAccount.iban
-                        val bic = bankAccount.bic
-                        val name = bankAccount.name
-                        val label = bankAccount.label
-                    }
                 }
-                call.respond(ret)
-                return@get
             }
-
-            post("/admin/bank-accounts/{label}/simulate-incoming-transaction") 
{
-                requireSuperuser(call.request)
-                val body = call.receiveJson<IncomingPaymentInfo>()
-                // FIXME: generate nicer UUID!
+            call.respond(accounts)
+        }
+        get("/admin/bank-accounts/{label}/transactions") {
+            requireSuperuser(call.request)
+            val ret = AccountTransactions()
+            transaction {
                 val accountLabel = ensureNonNull(call.parameters["label"])
-                if (!validatePlainAmount(body.amount)) {
-                    throw SandboxError(
-                        HttpStatusCode.BadRequest,
-                        "invalid amount (should be plain amount without 
currency)"
-                    )
-                }
-                val reqDebtorBic = body.debtorBic
-                if (reqDebtorBic != null && !validateBic(reqDebtorBic)) {
-                    throw SandboxError(
-                        HttpStatusCode.BadRequest,
-                        "invalid BIC"
-                    )
-                }
                 transaction {
                     val account = getBankAccountFromLabel(accountLabel)
-                    val randId = getRandomString(16)
-                    BankAccountTransactionEntity.new {
+                    tech.libeufin.sandbox.BankAccountTransactionEntity.find {
+                        
tech.libeufin.sandbox.BankAccountTransactionsTable.account eq account.id
+                    }.forEach {
+                        ret.payments.add(
+                            PaymentInfo(
+                                accountLabel = account.label,
+                                creditorIban = it.creditorIban,
+                                // FIXME: We need to modify the transactions 
table to have an actual
+                                // account servicer reference here.
+                                accountServicerReference = 
it.accountServicerReference,
+                                paymentInformationId = it.pmtInfId,
+                                debtorIban = it.debtorIban,
+                                subject = it.subject,
+                                date = GMTDate(it.date).toHttpDate(),
+                                amount = it.amount,
+                                creditorBic = it.creditorBic,
+                                creditorName = it.creditorName,
+                                debtorBic = it.debtorBic,
+                                debtorName = it.debtorName,
+                                currency = it.currency,
+                                creditDebitIndicator = when (it.direction) {
+                                    "CRDT" -> "credit"
+                                    "DBIT" -> "debit"
+                                    else -> throw Error("invalid direction")
+                                }
+                            )
+                        )
+                    }
+                }
+            }
+            call.respond(ret)
+        }
+        post("/admin/bank-accounts/{label}/generate-transactions") {
+            requireSuperuser(call.request)
+            transaction {
+                val accountLabel = ensureNonNull(call.parameters["label"])
+                val account = getBankAccountFromLabel(accountLabel)
+                val transactionReferenceCrdt = getRandomString(8)
+                val transactionReferenceDbit = getRandomString(8)
+
+                run {
+                    val amount = kotlin.random.Random.nextLong(5, 25)
+                    tech.libeufin.sandbox.BankAccountTransactionEntity.new {
                         creditorIban = account.iban
                         creditorBic = account.bic
                         creditorName = account.name
-                        debtorIban = body.debtorIban
-                        debtorBic = reqDebtorBic
-                        debtorName = body.debtorName
-                        subject = body.subject
-                        amount = body.amount
+                        debtorIban = "DE64500105178797276788"
+                        debtorBic = "DEUTDEBB101"
+                        debtorName = "Max Mustermann"
+                        subject = "sample transaction 
$transactionReferenceCrdt"
+                        this.amount = amount.toString()
                         currency = account.currency
                         date = getUTCnow().toInstant().toEpochMilli()
-                        accountServicerReference = "sandbox-$randId"
+                        accountServicerReference = transactionReferenceCrdt
                         this.account = account
                         direction = "CRDT"
                     }
                 }
-                call.respond(object {})
-            }
 
-            /**
-             * Associates a new bank account with an existing Ebics subscriber.
-             */
-            post("/admin/ebics/bank-accounts") {
-                requireSuperuser(call.request)
-                val body = call.receiveJson<BankAccountRequest>()
-                if (!validateBic(body.bic)) {
-                    throw SandboxError(HttpStatusCode.BadRequest, "invalid BIC 
(${body.bic})")
-                }
-                transaction {
-                    val subscriber = getEbicsSubscriberFromDetails(
-                        body.subscriber.userID,
-                        body.subscriber.partnerID,
-                        body.subscriber.hostID
-                    )
-                    val check = BankAccountEntity.find {
-                        BankAccountsTable.iban eq body.iban or 
(BankAccountsTable.label eq body.label)
-                    }.count()
-                    if (check > 0) throw SandboxError(
-                        HttpStatusCode.BadRequest,
-                        "Either IBAN or account label were already taken; 
please choose fresh ones"
-                    )
-                    subscriber.bankAccount = BankAccountEntity.new {
-                        iban = body.iban
-                        bic = body.bic
-                        name = body.name
-                        label = body.label
-                        currency = body.currency.uppercase(Locale.ROOT)
-                    }
-                }
-                call.respondText("Bank account created")
-                return@post
-            }
-            get("/admin/bank-accounts") {
-                requireSuperuser(call.request)
-                val accounts = mutableListOf<BankAccountInfo>()
-                transaction {
-                    BankAccountEntity.all().forEach {
-                        accounts.add(
-                            BankAccountInfo(
-                                label = it.label,
-                                name = it.name,
-                                bic = it.bic,
-                                iban = it.iban,
-                                currency = it.currency
-                            )
-                        )
-                    }
-                }
-                call.respond(accounts)
-            }
-            get("/admin/bank-accounts/{label}/transactions") {
-                requireSuperuser(call.request)
-                val ret = AccountTransactions()
-                transaction {
-                    val accountLabel = ensureNonNull(call.parameters["label"])
-                    transaction {
-                        val account = getBankAccountFromLabel(accountLabel)
-                        BankAccountTransactionEntity.find {
-                            BankAccountTransactionsTable.account eq account.id
-                        }.forEach {
-                            ret.payments.add(
-                                PaymentInfo(
-                                    accountLabel = account.label,
-                                    creditorIban = it.creditorIban,
-                                    // FIXME: We need to modify the 
transactions table to have an actual
-                                    // account servicer reference here.
-                                    accountServicerReference = 
it.accountServicerReference,
-                                    paymentInformationId = it.pmtInfId,
-                                    debtorIban = it.debtorIban,
-                                    subject = it.subject,
-                                    date = GMTDate(it.date).toHttpDate(),
-                                    amount = it.amount,
-                                    creditorBic = it.creditorBic,
-                                    creditorName = it.creditorName,
-                                    debtorBic = it.debtorBic,
-                                    debtorName = it.debtorName,
-                                    currency = it.currency,
-                                    creditDebitIndicator = when (it.direction) 
{
-                                        "CRDT" -> "credit"
-                                        "DBIT" -> "debit"
-                                        else -> throw Error("invalid 
direction")
-                                    }
-                                )
-                            )
-                        }
-                    }
-                }
-                call.respond(ret)
-            }
-            post("/admin/bank-accounts/{label}/generate-transactions") {
-                requireSuperuser(call.request)
-                transaction {
-                    val accountLabel = ensureNonNull(call.parameters["label"])
-                    val account = getBankAccountFromLabel(accountLabel)
-                    val transactionReferenceCrdt = getRandomString(8)
-                    val transactionReferenceDbit = getRandomString(8)
-
-                    run {
-                        val amount = Random.nextLong(5, 25)
-                        BankAccountTransactionEntity.new {
-                            creditorIban = account.iban
-                            creditorBic = account.bic
-                            creditorName = account.name
-                            debtorIban = "DE64500105178797276788"
-                            debtorBic = "DEUTDEBB101"
-                            debtorName = "Max Mustermann"
-                            subject = "sample transaction 
$transactionReferenceCrdt"
-                            this.amount = amount.toString()
-                            currency = account.currency
-                            date = getUTCnow().toInstant().toEpochMilli()
-                            accountServicerReference = transactionReferenceCrdt
-                            this.account = account
-                            direction = "CRDT"
-                        }
-                    }
-
-                    run {
-                        val amount = Random.nextLong(5, 25)
-
-                        BankAccountTransactionEntity.new {
-                            debtorIban = account.iban
-                            debtorBic = account.bic
-                            debtorName = account.name
-                            creditorIban = "DE64500105178797276788"
-                            creditorBic = "DEUTDEBB101"
-                            creditorName = "Max Mustermann"
-                            subject = "sample transaction 
$transactionReferenceDbit"
-                            this.amount = amount.toString()
-                            currency = account.currency
-                            date = getUTCnow().toInstant().toEpochMilli()
-                            accountServicerReference = transactionReferenceDbit
-                            this.account = account
-                            direction = "DBIT"
-                        }
+                run {
+                    val amount = kotlin.random.Random.nextLong(5, 25)
+
+                    tech.libeufin.sandbox.BankAccountTransactionEntity.new {
+                        debtorIban = account.iban
+                        debtorBic = account.bic
+                        debtorName = account.name
+                        creditorIban = "DE64500105178797276788"
+                        creditorBic = "DEUTDEBB101"
+                        creditorName = "Max Mustermann"
+                        subject = "sample transaction 
$transactionReferenceDbit"
+                        this.amount = amount.toString()
+                        currency = account.currency
+                        date = getUTCnow().toInstant().toEpochMilli()
+                        accountServicerReference = transactionReferenceDbit
+                        this.account = account
+                        direction = "DBIT"
                     }
                 }
-                call.respond(object {})
             }
-            /**
-             * Creates a new Ebics subscriber.
-             */
-            post("/admin/ebics/subscribers") {
-                requireSuperuser(call.request)
-                val body = call.receiveJson<EbicsSubscriberElement>()
-                transaction {
-                    EbicsSubscriberEntity.new {
-                        partnerId = body.partnerID
-                        userId = body.userID
-                        systemId = null
-                        hostId = body.hostID
-                        state = SubscriberState.NEW
-                        nextOrderID = 1
-                    }
+            call.respond(object {})
+        }
+        /**
+         * Creates a new Ebics subscriber.
+         */
+        post("/admin/ebics/subscribers") {
+            requireSuperuser(call.request)
+            val body = call.receiveJson<EbicsSubscriberElement>()
+            transaction {
+                tech.libeufin.sandbox.EbicsSubscriberEntity.new {
+                    partnerId = body.partnerID
+                    userId = body.userID
+                    systemId = null
+                    hostId = body.hostID
+                    state = tech.libeufin.sandbox.SubscriberState.NEW
+                    nextOrderID = 1
                 }
-                call.respondText(
-                    "Subscriber created.",
-                    ContentType.Text.Plain, HttpStatusCode.OK
-                )
-                return@post
             }
-            /**
-             * Shows all the Ebics subscribers' details.
-             */
-            get("/admin/ebics/subscribers") {
-                requireSuperuser(call.request)
-                val ret = AdminGetSubscribers()
-                transaction {
-                    EbicsSubscriberEntity.all().forEach {
-                        ret.subscribers.add(
-                            EbicsSubscriberElement(
-                                userID = it.userId,
-                                partnerID = it.partnerId,
-                                hostID = it.hostId
-                            )
+            call.respondText(
+                "Subscriber created.",
+                io.ktor.http.ContentType.Text.Plain, 
io.ktor.http.HttpStatusCode.OK
+            )
+            return@post
+        }
+        /**
+         * Shows all the Ebics subscribers' details.
+         */
+        get("/admin/ebics/subscribers") {
+            requireSuperuser(call.request)
+            val ret = AdminGetSubscribers()
+            transaction {
+                tech.libeufin.sandbox.EbicsSubscriberEntity.all().forEach {
+                    ret.subscribers.add(
+                        EbicsSubscriberElement(
+                            userID = it.userId,
+                            partnerID = it.partnerId,
+                            hostID = it.hostId
                         )
-                    }
-                }
-                call.respond(ret)
-                return@get
-            }
-            post("/admin/ebics/hosts/{hostID}/rotate-keys") {
-                requireSuperuser(call.request)
-                val hostID: String = call.parameters["hostID"] ?: throw 
SandboxError(
-                    HttpStatusCode.BadRequest, "host ID missing in URL"
-                )
-                transaction {
-                    val host = EbicsHostEntity.find {
-                        EbicsHostsTable.hostID eq hostID
-                    }.firstOrNull() ?: throw SandboxError(
-                        HttpStatusCode.NotFound, "Host $hostID not found"
                     )
-                    val pairA = CryptoUtil.generateRsaKeyPair(2048)
-                    val pairB = CryptoUtil.generateRsaKeyPair(2048)
-                    val pairC = CryptoUtil.generateRsaKeyPair(2048)
-                    host.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
-                    host.encryptionPrivateKey = 
ExposedBlob(pairB.private.encoded)
-                    host.signaturePrivateKey = 
ExposedBlob(pairC.private.encoded)
                 }
-                call.respondText(
-                    "Keys of '${hostID}' rotated.",
-                    ContentType.Text.Plain,
-                    HttpStatusCode.OK
+            }
+            call.respond(ret)
+            return@get
+        }
+        post("/admin/ebics/hosts/{hostID}/rotate-keys") {
+            requireSuperuser(call.request)
+            val hostID: String = call.parameters["hostID"] ?: throw 
SandboxError(
+                io.ktor.http.HttpStatusCode.BadRequest, "host ID missing in 
URL"
+            )
+            transaction {
+                val host = tech.libeufin.sandbox.EbicsHostEntity.find {
+                    tech.libeufin.sandbox.EbicsHostsTable.hostID eq hostID
+                }.firstOrNull() ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.NotFound, "Host $hostID not 
found"
                 )
-                return@post
+                val pairA = 
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+                val pairB = 
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+                val pairC = 
tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+                host.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
+                host.encryptionPrivateKey = ExposedBlob(pairB.private.encoded)
+                host.signaturePrivateKey = ExposedBlob(pairC.private.encoded)
             }
+            call.respondText(
+                "Keys of '${hostID}' rotated.",
+                io.ktor.http.ContentType.Text.Plain,
+                io.ktor.http.HttpStatusCode.OK
+            )
+            return@post
+        }
 
-            /**
-             * Creates a new EBICS host.
-             */
-            post("/admin/ebics/hosts") {
-                requireSuperuser(call.request)
-                val req = call.receiveJson<EbicsHostCreateRequest>()
-                val pairA = CryptoUtil.generateRsaKeyPair(2048)
-                val pairB = CryptoUtil.generateRsaKeyPair(2048)
-                val pairC = CryptoUtil.generateRsaKeyPair(2048)
-                transaction {
-                    EbicsHostEntity.new {
-                        this.ebicsVersion = req.ebicsVersion
-                        this.hostId = req.hostID
-                        this.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
-                        this.encryptionPrivateKey = 
ExposedBlob(pairB.private.encoded)
-                        this.signaturePrivateKey = 
ExposedBlob(pairC.private.encoded)
-                    }
+        /**
+         * Creates a new EBICS host.
+         */
+        post("/admin/ebics/hosts") {
+            requireSuperuser(call.request)
+            val req = call.receiveJson<EbicsHostCreateRequest>()
+            val pairA = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+            val pairB = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+            val pairC = tech.libeufin.util.CryptoUtil.generateRsaKeyPair(2048)
+            transaction {
+                tech.libeufin.sandbox.EbicsHostEntity.new {
+                    this.ebicsVersion = req.ebicsVersion
+                    this.hostId = req.hostID
+                    this.authenticationPrivateKey = 
ExposedBlob(pairA.private.encoded)
+                    this.encryptionPrivateKey = 
ExposedBlob(pairB.private.encoded)
+                    this.signaturePrivateKey = 
ExposedBlob(pairC.private.encoded)
                 }
-                call.respondText(
-                    "Host '${req.hostID}' created.",
-                    ContentType.Text.Plain,
-                    HttpStatusCode.OK
-                )
-                return@post
             }
+            call.respondText(
+                "Host '${req.hostID}' created.",
+                io.ktor.http.ContentType.Text.Plain,
+                io.ktor.http.HttpStatusCode.OK
+            )
+            return@post
+        }
 
+        /**
+         * Show the names of all the Ebics hosts
+         */
+        get("/admin/ebics/hosts") {
+            requireSuperuser(call.request)
+            val ebicsHosts = transaction {
+                tech.libeufin.sandbox.EbicsHostEntity.all().map { it.hostId }
+            }
+            call.respond(EbicsHostsResponse(ebicsHosts))
+        }
+        /**
+         * Serves all the Ebics requests.
+         */
+        post("/ebicsweb") {
+            try {
+                call.ebicsweb()
+            }
             /**
-             * Show the names of all the Ebics hosts
+             * Those errors were all detected by the bank's logic.
              */
-            get("/admin/ebics/hosts") {
-                requireSuperuser(call.request)
-                val ebicsHosts = transaction {
-                    EbicsHostEntity.all().map { it.hostId }
+            catch (e: SandboxError) {
+                // Should translate to EBICS error code.
+                when (e.errorCode) {
+                    
tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw 
EbicsProcessingError("Invalid bank state.")
+                    
tech.libeufin.util.LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> throw 
EbicsProcessingError("Inconsistent bank state.")
+                    else -> throw EbicsProcessingError("Unknown LibEuFin error 
code: ${e.errorCode}.")
                 }
-                call.respond(EbicsHostsResponse(ebicsHosts))
+
             }
             /**
-             * Serves all the Ebics requests.
+             * An error occurred, but it wasn't explicitly thrown by the bank.
              */
-            post("/ebicsweb") {
-                try {
-                    call.ebicsweb()
-                }
-                /**
-                 * Those errors were all detected by the bank's logic.
-                 */
-                catch (e: SandboxError) {
-                    // Should translate to EBICS error code.
-                    when (e.errorCode) {
-                        LibeufinErrorCode.LIBEUFIN_EC_INVALID_STATE -> throw 
EbicsProcessingError("Invalid bank state.")
-                        LibeufinErrorCode.LIBEUFIN_EC_INCONSISTENT_STATE -> 
throw EbicsProcessingError("Inconsistent bank state.")
-                        else -> throw EbicsProcessingError("Unknown LibEuFin 
error code: ${e.errorCode}.")
-                    }
+            catch (e: Exception) {
+                throw EbicsProcessingError("Unmanaged error: $e")
+            }
 
-                }
-                /**
-                 * An error occurred, but it wasn't explicitly thrown by the 
bank.
-                 */
-                catch (e: Exception) {
-                    throw EbicsProcessingError("Unmanaged error: $e")
-                }
+        }
+        /**
+         * Activates a withdraw operation of 1 currency unit with
+         * the default exchange, from a designated/constant customer.
+         */
+        get("/taler") {
+            requireSuperuser(call.request)
+            SandboxAssert(
+                hostName != null,
+                "Own hostname not found.  Logs should have warned"
+            )
+            SandboxAssert(
+                currencyEnv != null,
+                "Currency not found.  Logs should have warned"
+            )
+            // check that the three canonical accounts exist
+            val wo = transaction {
+                val exchange = tech.libeufin.sandbox.BankAccountEntity.find {
+                    tech.libeufin.sandbox.BankAccountsTable.label eq 
"sandbox-account-exchange"
+                }.firstOrNull()
+                val customer = tech.libeufin.sandbox.BankAccountEntity.find {
+                    tech.libeufin.sandbox.BankAccountsTable.label eq 
"sandbox-account-customer"
+                }.firstOrNull()
+                val merchant = tech.libeufin.sandbox.BankAccountEntity.find {
+                    tech.libeufin.sandbox.BankAccountsTable.label eq 
"sandbox-account-merchant"
+                }.firstOrNull()
+
+                SandboxAssert(exchange != null, "exchange has no bank account")
+                SandboxAssert(customer != null, "customer has no bank account")
+                SandboxAssert(merchant != null, "merchant has no bank account")
+
+                // At this point, the three actors exist and a new withdraw 
operation can be created.
+                tech.libeufin.sandbox.TalerWithdrawalEntity.new {
+                    // wopid is autogenerated, and momentarily the only column
 
+                }
             }
             /**
-             * Activates a withdraw operation of 1 currency unit with
-             * the default exchange, from a designated/constant customer.
+             * Future versions will include the QR code in this response.
              */
-            get("/taler") {
-                requireSuperuser(call.request)
-                SandboxAssert(
-                    hostName != null,
-                    "Own hostname not found.  Logs should have warned"
-                )
-                SandboxAssert(
-                    currencyEnv != null,
-                    "Currency not found.  Logs should have warned"
+            call.respondText("taler://withdraw/${hostName}/api/${wo.wopid}")
+            return@get
+        }
+        get("/api/config") {
+            SandboxAssert(
+                currencyEnv != null,
+                "Currency not found.  Logs should have warned"
+            )
+            call.respond(object {
+                val name = "taler-bank-integration"
+
+                // FIXME: use actual version here!
+                val version = "0:0:0"
+                val currency = currencyEnv
+            })
+        }
+        /**
+         * not regulating the access here, as the wopid was only granted
+         * to logged-in users before (at the /taler endpoint) and has enough
+         * entropy to prevent guesses.
+         */
+        get("/api/withdrawal-operation/{wopid}") {
+            val wopid: String = ensureNonNull(call.parameters["wopid"])
+            val wo = transaction {
+
+                tech.libeufin.sandbox.TalerWithdrawalEntity.find {
+                    tech.libeufin.sandbox.TalerWithdrawalsTable.wopid eq 
java.util.UUID.fromString(wopid)
+                }.firstOrNull() ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.NotFound,
+                    "Withdrawal operation: $wopid not found"
                 )
-                // check that the three canonical accounts exist
-                val wo = transaction {
-                    val exchange = BankAccountEntity.find {
-                        BankAccountsTable.label eq "sandbox-account-exchange"
-                    }.firstOrNull()
-                    val customer = BankAccountEntity.find {
-                        BankAccountsTable.label eq "sandbox-account-customer"
-                    }.firstOrNull()
-                    val merchant = BankAccountEntity.find {
-                        BankAccountsTable.label eq "sandbox-account-merchant"
-                    }.firstOrNull()
-
-                    SandboxAssert(exchange != null, "exchange has no bank 
account")
-                    SandboxAssert(customer != null, "customer has no bank 
account")
-                    SandboxAssert(merchant != null, "merchant has no bank 
account")
-
-                    // At this point, the three actors exist and a new 
withdraw operation can be created.
-                    TalerWithdrawalEntity.new {
-                        // wopid is autogenerated, and momentarily the only 
column
-
-                    }
-                }
-                /**
-                 * Future versions will include the QR code in this response.
-                 */
-                
call.respondText("taler://withdraw/${hostName}/api/${wo.wopid}")
-                return@get
             }
-            get("/api/config") {
-                SandboxAssert(
-                    currencyEnv != null,
-                    "Currency not found.  Logs should have warned"
+            SandboxAssert(
+                envName != null,
+                "Env name not found, cannot suggest Exchange."
+            )
+            val ret = TalerWithdrawalStatus(
+                selection_done = wo.selectionDone,
+                transfer_done = wo.transferDone,
+                amount = "${currencyEnv}:5",
+                suggested_exchange = "https://exchange.${envName}.taler.net/";
+            )
+            call.respond(ret)
+            return@get
+        }
+        /**
+         * Here Sandbox collects the reserve public key to be used
+         * as the wire transfer subject, and pays the exchange - which
+         * is as well collected in this request.
+         */
+        post("/api/withdrawal-operation/{wopid}") {
+
+            val wopid: String = ensureNonNull(call.parameters["wopid"])
+            val body = call.receiveJson<TalerWithdrawalConfirmation>()
+
+            newSuspendedTransaction(context = singleThreadContext) {
+                var wo = tech.libeufin.sandbox.TalerWithdrawalEntity.find {
+                    tech.libeufin.sandbox.TalerWithdrawalsTable.wopid eq 
java.util.UUID.fromString(wopid)
+                }.firstOrNull() ?: throw SandboxError(
+                    io.ktor.http.HttpStatusCode.NotFound, "Withdrawal 
operation $wopid not found."
                 )
-                call.respond(object {
-                    val name = "taler-bank-integration"
-
-                    // FIXME: use actual version here!
-                    val version = "0:0:0"
-                    val currency = currencyEnv
-                })
-            }
-            /**
-             * not regulating the access here, as the wopid was only granted
-             * to logged-in users before (at the /taler endpoint) and has 
enough
-             * entropy to prevent guesses.
-             */
-            get("/api/withdrawal-operation/{wopid}") {
-                val wopid: String = ensureNonNull(call.parameters["wopid"])
-                val wo = transaction {
-
-                    TalerWithdrawalEntity.find {
-                        TalerWithdrawalsTable.wopid eq UUID.fromString(wopid)
-                    }.firstOrNull() ?: throw SandboxError(
-                        HttpStatusCode.NotFound,
-                        "Withdrawal operation: $wopid not found"
+                if (wo.selectionDone) {
+                    if (wo.transferDone) {
+                        logger.info("Wallet performs again this operation that 
was paid out earlier: idempotent")
+                        return@newSuspendedTransaction
+                    }
+                    // reservePub+exchange selected but not payed: check 
consistency
+                    if (body.reserve_pub != wo.reservePub) throw SandboxError(
+                        io.ktor.http.HttpStatusCode.Conflict,
+                        "Selecting a different reserve from the one already 
selected"
+                    )
+                    if (body.selected_exchange != wo.selectedExchangePayto) 
throw SandboxError(
+                        io.ktor.http.HttpStatusCode.Conflict,
+                        "Selecting a different exchange from the one already 
selected"
                     )
                 }
-                SandboxAssert(
-                    envName != null,
-                    "Env name not found, cannot suggest Exchange."
-                )
-                val ret = TalerWithdrawalStatus(
-                    selection_done = wo.selectionDone,
-                    transfer_done = wo.transferDone,
-                    amount = "${currencyEnv}:5",
-                    suggested_exchange = 
"https://exchange.${envName}.taler.net/";
+                // here only if (1) no selection done or (2) _only_ selection 
done:
+                // both ways no transfer must have happened.
+                SandboxAssert(!wo.transferDone, "Sandbox allowed paid but 
unselected reserve")
+
+                wireTransfer(
+                    "sandbox-account-customer",
+                    "sandbox-account-exchange",
+                    "$currencyEnv:5",
+                    body.reserve_pub
                 )
-                call.respond(ret)
-                return@get
+                wo.reservePub = body.reserve_pub
+                wo.selectedExchangePayto = body.selected_exchange
+                wo.selectionDone = true
+                wo.transferDone = true
             }
             /**
-             * Here Sandbox collects the reserve public key to be used
-             * as the wire transfer subject, and pays the exchange - which
-             * is as well collected in this request.
+             * NOTE: is this always guaranteed to run AFTER the suspended
+             * transaction block above?
              */
-            post("/api/withdrawal-operation/{wopid}") {
+            call.respond(object {
+                val transfer_done = true
+            })
+            return@post
+        }
 
-                val wopid: String = ensureNonNull(call.parameters["wopid"])
-                val body = call.receiveJson<TalerWithdrawalConfirmation>()
+        // Create a new demobank instance with a particular currency,
+        // debt limit and possibly other configuration
+        // (could also be a CLI command for now)
+        post("/demobanks") {
 
-                newSuspendedTransaction(context = singleThreadContext) {
-                    var wo = TalerWithdrawalEntity.find {
-                        TalerWithdrawalsTable.wopid eq UUID.fromString(wopid)
-                    }.firstOrNull() ?: throw SandboxError(
-                        HttpStatusCode.NotFound, "Withdrawal operation $wopid 
not found."
-                    )
-                    if (wo.selectionDone) {
-                        if (wo.transferDone) {
-                            logger.info("Wallet performs again this operation 
that was paid out earlier: idempotent")
-                            return@newSuspendedTransaction
-                        }
-                        // reservePub+exchange selected but not payed: check 
consistency
-                        if (body.reserve_pub != wo.reservePub) throw 
SandboxError(
-                            HttpStatusCode.Conflict,
-                            "Selecting a different reserve from the one 
already selected"
-                        )
-                        if (body.selected_exchange != 
wo.selectedExchangePayto) throw SandboxError(
-                            HttpStatusCode.Conflict,
-                            "Selecting a different exchange from the one 
already selected"
-                        )
-                    }
-                    // here only if (1) no selection done or (2) _only_ 
selection done:
-                    // both ways no transfer must have happened.
-                    SandboxAssert(!wo.transferDone, "Sandbox allowed paid but 
unselected reserve")
-
-                    wireTransfer(
-                        "sandbox-account-customer",
-                        "sandbox-account-exchange",
-                        "$currencyEnv:5",
-                        body.reserve_pub
-                    )
-                    wo.reservePub = body.reserve_pub
-                    wo.selectedExchangePayto = body.selected_exchange
-                    wo.selectionDone = true
-                    wo.transferDone = true
-                }
-                /**
-                 * NOTE: is this always guaranteed to run AFTER the suspended
-                 * transaction block above?
-                 */
-                call.respond(object {
-                    val transfer_done = true
-                })
-                return@post
-            }
+        }
 
-            // Create a new demobank instance with a particular currency,
-            // debt limit and possibly other configuration
-            // (could also be a CLI command for now)
-            post("/demobanks") {
+        // List configured demobanks
+        get("/demobanks") {
 
-            }
+        }
 
-            // List configured demobanks
-            get("/demobanks") {
+        delete("/demobank/{demobankid") {
 
-            }
+        }
 
-            delete("/demobank/{demobankid") {
+        get("/demobank/{demobankid") {
 
-            }
+        }
 
-            get("/demobank/{demobankid") {
+        route("/demobank/{demobankid}") {
+            // Note: Unlike the old pybank, the sandbox does *not* actually 
expose the
+            // taler wire gateway API, because the exchange uses the nexus.
 
-            }
+            // Endpoint(s) for making arbitrary payments in the sandbox for 
integration tests
+            // FIXME: Do we actually need this, or can we just use the sandbox 
admin APIs?
+            route("/testing-api") {
 
-            route("/demobank/{demobankid}") {
-                // Note: Unlike the old pybank, the sandbox does *not* 
actually expose the
-                // taler wire gateway API, because the exchange uses the nexus.
+            }
 
-                // Endpoint(s) for making arbitrary payments in the sandbox 
for integration tests
-                // FIXME: Do we actually need this, or can we just use the 
sandbox admin APIs?
-                route("/testing-api") {
+            route("/access-api") {
+                get("/accounts/{account_name}") {
+                    // Authenticated.  Accesses basic information (balance)
+                    // about an account. (see docs)
 
+                    // FIXME: Since we now use IBANs everywhere, maybe the 
account should also be assigned an IBAN
                 }
 
-                route("/access-api") {
-                    get("/accounts/{account_name}") {
-                        // Authenticated.  Accesses basic information (balance)
-                        // about an account. (see docs)
-
-                        // FIXME: Since we now use IBANs everywhere, maybe the 
account should also be assigned an IBAN
-                    }
-
-                    get("/accounts/{account_name}/history") {
-                        // New endpoint, access account history to display in 
the SPA
-                        // (could be merged with GET /accounts/{account_name}
-                    }
+                get("/accounts/{account_name}/history") {
+                    // New endpoint, access account history to display in the 
SPA
+                    // (could be merged with GET /accounts/{account_name}
+                }
 
-                    // [...]
+                // [...]
 
-                    get("/public-accounts") {
-                        // List public accounts.  Does not require any 
authentication.
-                        // XXX: New!
-                    }
-
-                    get("/public-accounts/{account_name}/history") {
-                        // Get transaction history of a public account
-                    }
+                get("/public-accounts") {
+                    // List public accounts.  Does not require any 
authentication.
+                    // XXX: New!
+                }
 
-                    post("/testing/register") {
-                        // Register a new account.
-                        // No authentication is required to register a new 
user.
-                        // FIXME:  Should probably not use "testing" as the 
prefix, since it's used "in production" in the demobank SPA
-                    }
+                get("/public-accounts/{account_name}/history") {
+                    // Get transaction history of a public account
                 }
 
+                post("/testing/register") {
+                    // Register a new account.
+                    // No authentication is required to register a new user.
+                    // FIXME:  Should probably not use "testing" as the 
prefix, since it's used "in production" in the demobank SPA
+                }
             }
+
         }
     }
+}
+fun serverMain(dbName: String, port: Int) {
+    execThrowableOrTerminate { dbCreateTables(dbName) }
+    val server = embeddedServer(Netty, port = port, module = sandboxApp)
     logger.info("LibEuFin Sandbox running on port $port")
     try {
         server.start(wait = true)
diff --git a/sandbox/src/test/kotlin/CamtTest.kt 
b/sandbox/src/test/kotlin/CamtTest.kt
deleted file mode 100644
index 56a53a4..0000000
--- a/sandbox/src/test/kotlin/CamtTest.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.sandbox.buildCamtString
-import tech.libeufin.util.RawPayment
-import tech.libeufin.util.XMLUtil
-import kotlin.test.assertTrue
-
-class CamtTest {
-
-    @Test
-    fun validationTest() {
-        val payment = RawPayment(
-            creditorIban = "GB33BUKB20201222222222",
-            creditorName = "Oliver Smith",
-            creditorBic = "BUKBGB33",
-            debtorIban = "GB33BUKB20201333333333",
-            debtorName = "John Doe",
-            debtorBic = "BUKBGB33",
-            amount = "2",
-            currency = "EUR",
-            subject = "reimbursement",
-            date = "1000-02-02",
-            uid = "0",
-            direction = "DBIT"
-        )
-        val xml = buildCamtString(
-            53,
-            "GB33BUKB20201222222222",
-            mutableListOf(payment)
-        )
-        assertTrue {
-            XMLUtil.validateFromString(xml)
-        }
-    }
-}
\ No newline at end of file
diff --git a/sandbox/src/test/kotlin/DBTest.kt 
b/sandbox/src/test/kotlin/DBTest.kt
index e52373f..5c99acf 100644
--- a/sandbox/src/test/kotlin/DBTest.kt
+++ b/sandbox/src/test/kotlin/DBTest.kt
@@ -94,7 +94,7 @@ class DBTest {
                 addLogger(StdOutSqlLogger)
                 BankAccountTransactionEntity.find {
                     BankAccountTransactionsTable.date.between(
-                        parseDashedDate("1970-01-01").millis(),
+                        
parseDashedDate("1970-01-01").toInstant().toEpochMilli(),
                         LocalDateTime.now().millis()
                     )
                 }.firstOrNull()
diff --git a/util/build.gradle b/util/build.gradle
index 87843cb..6f67b78 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -27,6 +27,8 @@ sourceSets {
 }
 
 def exposed_version = '0.32.1'
+def netty_version = '4.1.68.Final'
+def ktor_version = '1.6.1'
 
 dependencies {
     implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.21'
@@ -48,6 +50,10 @@ dependencies {
 
     implementation "org.jetbrains.exposed:exposed-core:$exposed_version"
     implementation "org.jetbrains.exposed:exposed-dao:$exposed_version"
+    implementation "io.netty:netty-all:$netty_version"
+    implementation "io.netty:netty-transport-native-epoll:$netty_version"
+    implementation "io.ktor:ktor-server-test-host:$ktor_version"
+    implementation "io.ktor:ktor-jackson:$ktor_version"
 
     testImplementation group: 'junit', name: 'junit', version: '4.13.2'
     testImplementation 'org.jetbrains.kotlin:kotlin-test-junit:1.5.21'
diff --git a/util/src/main/kotlin/UnixDomainSocket.kt 
b/util/src/main/kotlin/UnixDomainSocket.kt
new file mode 100644
index 0000000..b2713f2
--- /dev/null
+++ b/util/src/main/kotlin/UnixDomainSocket.kt
@@ -0,0 +1,97 @@
+import io.ktor.application.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.http.HttpMethod
+import io.ktor.server.engine.*
+import io.ktor.server.testing.*
+import io.netty.bootstrap.ServerBootstrap
+import io.netty.buffer.ByteBufInputStream
+import io.netty.buffer.Unpooled
+import io.netty.channel.*
+import io.netty.channel.epoll.EpollEventLoopGroup
+import io.netty.channel.epoll.EpollServerDomainSocketChannel
+import io.netty.channel.unix.DomainSocketAddress
+import io.netty.handler.codec.http.*
+import io.netty.handler.codec.http.DefaultHttpResponse
+import io.netty.util.AttributeKey
+
+fun startServer(unixSocketPath: String, app: Application.() -> Unit) {
+
+    val boss = EpollEventLoopGroup()
+    val worker = EpollEventLoopGroup()
+    val serverBootstrap = ServerBootstrap()
+    serverBootstrap.group(boss, worker).channel(
+        EpollServerDomainSocketChannel::class.java
+    ).childHandler(LibeufinHttpInit(app))
+
+    val socketPath = DomainSocketAddress(unixSocketPath)
+    serverBootstrap.bind(socketPath).sync().channel().closeFuture().sync()
+}
+
+private val ktorApplicationKey = AttributeKey.newInstance<Application.() -> 
Unit>("KtorApplicationCall")
+
+class LibeufinHttpInit(private val app: Application.() -> Unit) : 
ChannelInitializer<Channel>() {
+    override fun initChannel(ch: Channel) {
+        val libeufinHandler = LibeufinHttpHandler()
+        ch.pipeline(
+        ).addLast(
+            HttpServerCodec()
+        ).addLast(
+            HttpObjectAggregator(Int.MAX_VALUE)
+        ).addLast(
+            libeufinHandler
+        )
+        val libeufinCtx: ChannelHandlerContext = 
ch.pipeline().context(libeufinHandler)
+        libeufinCtx.attr(ktorApplicationKey).set(app)
+    }
+}
+
+class LibeufinHttpHandler : SimpleChannelInboundHandler<FullHttpRequest>() {
+
+    @OptIn(EngineAPI::class)
+    override fun channelRead0(ctx: ChannelHandlerContext?, msg: 
FullHttpRequest) {
+        val app = ctx?.attr(ktorApplicationKey)?.get()
+        if (app == null) throw UtilError(
+            HttpStatusCode.InternalServerError,
+            "custom libEufin Unix-domain-socket+HTTP handler lost its Web app",
+            null
+        )
+        /**
+         * Below is only a echo of what euFin gets from the network.  All
+         * the checks should then occur at the Web app + Ktor level.  Hence,
+         * a HTTP call of GET with a non-empty body is not to be blocked / 
warned
+         * at this level.
+         *
+         * The only exception is the HTTP version value in the response, as the
+         * response returned by the Web app does not set it.  Therefore, this
+         * proxy echoes back the HTTP version that was read in the request.
+         */
+        withTestApplication(app) {
+            val httpVersion = msg.protocolVersion()
+            // Proxying the request with Ktor API.
+            val call = handleRequest(closeRequest = false) {
+                msg.headers().forEach { addHeader(it.key, it.value) }
+                method = HttpMethod(msg.method().name())
+                uri = msg.uri()
+                version = httpVersion.text()
+                setBody(ByteBufInputStream(msg.content()).readAllBytes())
+            }
+            val statusCode: Int = call.response.status()?.value ?: throw 
UtilError(
+                HttpStatusCode.InternalServerError,
+                "app proxied via Unix domain socket did not include a response 
status code",
+                ec = null // FIXME: to be defined.
+            )
+            // Responding with Netty API.
+            val response = DefaultFullHttpResponse(
+                httpVersion,
+                HttpResponseStatus.valueOf(statusCode),
+                Unpooled.wrappedBuffer(call.response.byteContent ?: 
ByteArray(0))
+            )
+            call.response.headers.allValues().forEach { s, list ->
+                response.headers().set(s, list.joinToString()) // 
joinToString() separates with ", " by default.
+            }
+            ctx.write(response)
+            ctx.flush()
+        }
+    }
+}
\ No newline at end of file
diff --git a/util/src/test/kotlin/DomainSocketTest.kt 
b/util/src/test/kotlin/DomainSocketTest.kt
new file mode 100644
index 0000000..d6e5c18
--- /dev/null
+++ b/util/src/test/kotlin/DomainSocketTest.kt
@@ -0,0 +1,30 @@
+import io.ktor.application.*
+import io.ktor.features.*
+import io.ktor.http.*
+import io.ktor.response.*
+import io.ktor.routing.*
+import org.junit.Test
+import io.ktor.jackson.jackson
+import io.ktor.request.*
+
+class DomainSocketTest {
+    // @Test
+    fun bind() {
+        startServer("/tmp/java.sock") {
+            install(ContentNegotiation) { jackson() }
+            routing {
+                // responds with a empty JSON object.
+                get("/") {
+                    this.call.respond(object {})
+                    return@get
+                }
+                // echoes what it read in the request.
+                post("/") {
+                    val body = this.call.receiveText()
+                    this.call.respondText(body)
+                    return@post
+                }
+            }
+        }
+    }
+}
\ No newline at end of file

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