gnunet-svn
[Top][All Lists]
Advanced

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

[libeufin] 08/08: Fixes after wallet tests harness.


From: gnunet
Subject: [libeufin] 08/08: Fixes after wallet tests harness.
Date: Wed, 10 Nov 2021 16:22:51 +0100

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

ms pushed a commit to branch master
in repository libeufin.

commit fe22997387eefa9fb205384fe59f42d2480ea425
Author: ms <ms@taler.net>
AuthorDate: Wed Nov 10 16:21:04 2021 +0100

    Fixes after wallet tests harness.
    
    - implement TWG /admin/add-incoming.
    - generate checksum-valid IBANs.
---
 access-api-stash/AccessApiNexus.kt                 | 210 +++++++++++++++++++++
 .../main/kotlin/tech/libeufin/sandbox/Helpers.kt   |  10 +-
 .../src/main/kotlin/tech/libeufin/sandbox/Main.kt  |  50 ++++-
 util/src/main/kotlin/JSON.kt                       |   6 +
 util/src/main/kotlin/Payto.kt                      |   4 +-
 util/src/main/kotlin/iban.kt                       |  15 +-
 util/src/test/kotlin/ibanTest.kt                   |  10 +
 7 files changed, 288 insertions(+), 17 deletions(-)

diff --git a/access-api-stash/AccessApiNexus.kt 
b/access-api-stash/AccessApiNexus.kt
new file mode 100644
index 00000000..18347083
--- /dev/null
+++ b/access-api-stash/AccessApiNexus.kt
@@ -0,0 +1,210 @@
+package tech.libeufin.nexus.`access-api`
+
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.ktor.client.*
+import io.ktor.client.features.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.util.*
+import org.jetbrains.exposed.sql.not
+import org.jetbrains.exposed.sql.transactions.transaction
+import tech.libeufin.nexus.*
+import tech.libeufin.nexus.server.AccessApiNewTransport
+import tech.libeufin.nexus.server.EbicsNewTransport
+import tech.libeufin.nexus.server.FetchSpecJson
+import tech.libeufin.nexus.server.client
+import tech.libeufin.util.*
+import java.net.URL
+import java.nio.charset.Charset
+
+private fun getAccessApiClient(connId: String): AccessApiClientEntity {
+    val conn = NexusBankConnectionEntity.find {
+        NexusBankConnectionsTable.connectionId eq connId
+    }.firstOrNull() ?: throw notFound("Connection '$connId' not found.")
+    val client = AccessApiClientEntity.find {
+        AccessApiClientsTable.nexusBankConnection eq conn.id.value
+    }.firstOrNull() ?: throw notFound("Connection '$connId' has no client 
data.")
+    return client
+}
+
+suspend fun HttpClient.accessApiReq(
+    method: HttpMethod,
+    url: String,
+    body: Any? = null,
+    // username, password
+    credentials: Pair<String, String>): String? {
+
+    val reqBuilder: HttpRequestBuilder.() -> Unit = {
+        contentType(ContentType.Application.Json)
+        if (body != null)
+            this.body = body
+
+        headers.apply {
+            this.set(
+                "Authorization",
+                "Basic " + 
bytesToBase64("${credentials.first}:${credentials.second}".toByteArray(Charsets.UTF_8))
+            )
+        }
+    }
+    return try {
+        when(method) {
+            HttpMethod.Get -> {
+                this.get(url, reqBuilder)
+            }
+            HttpMethod.Post -> {
+                this.post(url, reqBuilder)
+            }
+            else -> throw internalServerError("Method $method not supported.")
+        }
+    } catch (e: ClientRequestException) {
+        logger.error(e.message)
+        throw NexusError(
+            HttpStatusCode.BadGateway,
+            e.message
+        )
+    }
+}
+
+/**
+ * Talk to the Sandbox via native Access API.  The main reason
+ * for this class was to still allow x-taler-bank as a wire method
+ * (to accommodate wallet harness tests), and therefore skip the
+ * Camt+Pain schemas.
+ */
+class JsonBankConnectionProtocol: BankConnectionProtocol {
+
+    override fun createConnection(connId: String, user: NexusUserEntity, data: 
JsonNode) {
+        val bankConn = NexusBankConnectionEntity.new {
+            this.connectionId = connId
+            owner = user
+            type = "access-api"
+        }
+        val newTransportData = jacksonObjectMapper(
+        ).treeToValue(data, AccessApiNewTransport::class.java) ?: throw 
NexusError(
+            HttpStatusCode.BadRequest, "Access Api details not found in 
request"
+        )
+        AccessApiClientEntity.new {
+            username = newTransportData.username
+            bankURL = newTransportData.bankURL
+            remoteBankAccountLabel = newTransportData.remoteBankAccountLabel
+            nexusBankConnection = bankConn
+            password = newTransportData.password
+        }
+    }
+
+    override fun getConnectionDetails(conn: NexusBankConnectionEntity): 
JsonNode {
+        val details = transaction { getAccessApiClient(conn.connectionId) }
+        val ret = ObjectMapper().createObjectNode()
+        ret.put("username", details.username)
+        ret.put("bankURL", details.bankURL)
+        ret.put("passwordHash", CryptoUtil.hashpw(details.password))
+        ret.put("remoteBankAccountLabel", details.remoteBankAccountLabel)
+        return ret
+    }
+
+    override suspend fun submitPaymentInitiation(
+        httpClient: HttpClient,
+        paymentInitiationId: Long // must refer to an x-taler-bank 
payto://-instruction.
+    ) {
+        val payInit = 
XTalerBankPaymentInitiationEntity.findById(paymentInitiationId) ?: throw 
notFound(
+            "Payment initiation '$paymentInitiationId' not found."
+        )
+        val conn = payInit.defaultBankConnection ?: throw notFound(
+            "No default bank connection for payment initiation 
'${paymentInitiationId}' was found."
+        )
+        val details = getAccessApiClient(conn.connectionId)
+
+        client.accessApiReq(
+            method = HttpMethod.Post,
+            url = urlJoinNoDrop(
+                details.bankURL,
+                "accounts/${details.remoteBankAccountLabel}/transactions"
+            ),
+            body = object {
+                val paytoUri = payInit.paytoUri
+                val amount = payInit.amount
+                val subject = payInit.subject
+            },
+            credentials = Pair(details.username, details.password)
+        )
+    }
+    /**
+     * This function gets always the fresh transactions from
+     * the bank.  Any other Wire Gateway API policies will be
+     * implemented by the respective facade (XTalerBank.kt) */
+    override suspend fun fetchTransactions(
+        fetchSpec: FetchSpecJson,
+        client: HttpClient,
+        bankConnectionId: String,
+        /**
+         * Label of the local bank account that mirrors
+         * the remote bank account pointed to by 'bankConnectionId' */
+        accountId: String
+    ) {
+        val details = getAccessApiClient(bankConnectionId)
+        val txsRaw = client.accessApiReq(
+            method = HttpMethod.Get,
+            url = urlJoinNoDrop(
+                details.bankURL,
+                "accounts/${details.remoteBankAccountLabel}/transactions"
+            ),
+            credentials = Pair(details.username, details.password)
+        )
+        // What format does Access API communicates the records in?
+        /**
+         * NexusXTalerBankTransactions.new {
+         *
+         *     .. details ..
+         * }
+         */
+    }
+
+    override fun exportBackup(bankConnectionId: String, passphrase: String): 
JsonNode {
+        throw NexusError(
+            HttpStatusCode.NotImplemented,
+            "Operation not needed."
+        )
+    }
+
+    override fun exportAnalogDetails(conn: NexusBankConnectionEntity): 
ByteArray {
+        throw NexusError(
+            HttpStatusCode.NotImplemented,
+            "Operation not needed."
+        )
+    }
+
+    override suspend fun fetchAccounts(client: HttpClient, connId: String) {
+        throw NexusError(
+            HttpStatusCode.NotImplemented,
+            "access-api connections assume that remote and local bank" +
+                    " accounts are called the same.  No need to 'fetch'"
+        )
+    }
+
+    override fun createConnectionFromBackup(
+        connId: String,
+        user: NexusUserEntity,
+        passphrase: String?,
+        backup: JsonNode
+    ) {
+        throw NexusError(
+            HttpStatusCode.NotImplemented,
+            "Operation not needed."
+        )
+    }
+
+    override suspend fun connect(client: HttpClient, connId: String) {
+        /**
+         * Future versions might create a bank account at this step.
+         * Right now, all the tests do create those accounts beforehand.
+         */
+        throw NexusError(
+            HttpStatusCode.NotImplemented,
+            "Operation not needed."
+        )
+    }
+}
+
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
index 81974703..2e927000 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Helpers.kt
@@ -176,13 +176,13 @@ fun wireTransfer(
     amount: String // $currency:x.y
 ): String {
     val args: Triple<BankAccountEntity, BankAccountEntity, 
DemobankConfigEntity> = transaction {
-        val debitAccount = BankAccountEntity.find {
+        val debitAccountDb = BankAccountEntity.find {
             BankAccountsTable.label eq debitAccount
         }.firstOrNull() ?: throw SandboxError(
             HttpStatusCode.NotFound,
             "Debit account '$debitAccount' not found"
         )
-        val creditAccount = BankAccountEntity.find {
+        val creditAccountDb = BankAccountEntity.find {
             BankAccountsTable.label eq creditAccount
         }.firstOrNull() ?: throw SandboxError(
             HttpStatusCode.NotFound,
@@ -195,7 +195,7 @@ fun wireTransfer(
             "Demobank '$demobank' not found"
         )
 
-        Triple(debitAccount, creditAccount, demoBank)
+        Triple(debitAccountDb, creditAccountDb, demoBank)
     }
 
     /**
@@ -228,7 +228,8 @@ fun wireTransfer(
     subject: String,
     amount: String,
 ): String {
-
+    // sanity check on the amount, no currency allowed here.
+    parseDecimal(amount)
     val timeStamp = getUTCnow().toInstant().toEpochMilli()
     val transactionRef = getRandomString(8)
     transaction {
@@ -281,7 +282,6 @@ fun getWithdrawalOperation(opId: String): 
TalerWithdrawalEntity {
 fun getBankAccountFromPayto(paytoUri: String): BankAccountEntity {
     val paytoParse = parsePayto(paytoUri)
     return getBankAccountFromIban(paytoParse.iban)
-
 }
 
 fun getBankAccountFromIban(iban: String): BankAccountEntity {
diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt 
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
index 454db034..2e405b40 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt
@@ -986,6 +986,39 @@ val sandboxApp: Application.() -> Unit = {
 
         route("/demobanks/{demobankid}") {
 
+            // NOTE: TWG assumes that username == bank account label.
+            route("/taler-wire-gateway") {
+                post("/{exchangeUsername}/admin/add-incoming") {
+                    val username = call.getUriComponent("exchangeUsername")
+                    val usernameAuth = call.request.basicAuth()
+                    if (username != usernameAuth) {
+                        throw forbidden(
+                            "Bank account name and username differ: $username 
vs $usernameAuth"
+                        )
+                    }
+                    logger.debug("TWG add-incoming passed authentication")
+                    val body = call.receive<TWGAdminAddIncoming>()
+                    transaction {
+                        val demobank = ensureDemobank(call)
+                        val bankAccountCredit = 
getBankAccountFromLabel(username, demobank)
+                        if (bankAccountCredit.owner != username) throw 
forbidden(
+                            "User '$username' cannot access bank account with 
label: $username."
+                        )
+                        val bankAccountDebit = 
getBankAccountFromPayto(body.debit_account)
+                        logger.debug("TWG add-incoming about to wire transfer")
+                        wireTransfer(
+                            bankAccountDebit.label,
+                            bankAccountCredit.label,
+                            demobank.name,
+                            body.reserve_pub,
+                            body.amount
+                        )
+                        logger.debug("TWG add-incoming has wire transferred")
+                    }
+                    call.respond(object {})
+                    return@post
+                }
+            }
             // Talk to wallets.
             route("/integration-api") {
 
@@ -1283,8 +1316,8 @@ val sandboxApp: Application.() -> Unit = {
                     }
                     // Create new customer.
                     requireValidResourceName(req.username)
-                    transaction {
-                        BankAccountEntity.new {
+                    val bankAccount = transaction {
+                        val bankAccount = BankAccountEntity.new {
                             iban = getIban()
                             /**
                              * For now, keep same semantics of Pybank: a 
username
@@ -1299,8 +1332,19 @@ val sandboxApp: Application.() -> Unit = {
                             username = req.username
                             passwordHash = CryptoUtil.hashpw(req.password)
                         }
+                        bankAccount
                     }
-                    call.respond(object {})
+                    call.respond(object {
+                        val balance = {
+                            val amount = "${demobank.currency}:0"
+                            val credit_debit_indicator = "CRDT"
+                        }
+                        val paytoUri = buildIbanPaytoUri(
+                            iban = bankAccount.iban,
+                            bic = bankAccount.bic,
+                            receiverName = 
getPersonNameFromCustomer(req.username)
+                        )
+                    })
                     return@post
                 }
             }
diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt
index ee36b37e..c4a97fcb 100644
--- a/util/src/main/kotlin/JSON.kt
+++ b/util/src/main/kotlin/JSON.kt
@@ -59,6 +59,12 @@ data class IncomingPaymentInfo(
     val subject: String
 )
 
+data class TWGAdminAddIncoming(
+    val amount: String,
+    val reserve_pub: String,
+    val debit_account: String
+)
+
 data class PaymentInfo(
     val accountLabel: String,
     val creditorIban: String,
diff --git a/util/src/main/kotlin/Payto.kt b/util/src/main/kotlin/Payto.kt
index 2308a25e..dcf9c874 100644
--- a/util/src/main/kotlin/Payto.kt
+++ b/util/src/main/kotlin/Payto.kt
@@ -74,9 +74,9 @@ fun parsePayto(paytoLine: String): Payto {
 
 fun buildIbanPaytoUri(
     iban: String,
-    bic: String,
+    bic: String?,
     receiverName: String,
 ): String {
     val nameUrlEnc = URLEncoder.encode(receiverName, "utf-8")
-    return "payto://iban/$bic/$iban?receiver-name=$nameUrlEnc"
+    return "payto://iban/${if (bic != null) "$bic/" else 
""}$iban?receiver-name=$nameUrlEnc"
 }
\ No newline at end of file
diff --git a/util/src/main/kotlin/iban.kt b/util/src/main/kotlin/iban.kt
index 4b8055da..cf1fc005 100644
--- a/util/src/main/kotlin/iban.kt
+++ b/util/src/main/kotlin/iban.kt
@@ -1,11 +1,12 @@
 package tech.libeufin.util
 
+import java.math.BigInteger
+
 fun getIban(): String {
-    val bankCode = "00000000" // 8 digits
-    val accountCodeChars = ('0'..'9')
-    // 10 digits
-    val accountCode = (0..9).map {
-        accountCodeChars.random()
-    }.joinToString("")
-    return "EU00" + bankCode + accountCode
+    val ccNoCheck = "131400" // DE00
+    val bban = (0..3).map {
+        (0..9).random()
+    }.joinToString("") // 4 digits BBAN.
+    val checkDigits = 
"98".toBigInteger().minus("$bban$ccNoCheck".toBigInteger().mod("97".toBigInteger()))
+    return "DE$checkDigits$bban"
 }
\ No newline at end of file
diff --git a/util/src/test/kotlin/ibanTest.kt b/util/src/test/kotlin/ibanTest.kt
new file mode 100644
index 00000000..964f822d
--- /dev/null
+++ b/util/src/test/kotlin/ibanTest.kt
@@ -0,0 +1,10 @@
+import org.junit.Test
+import tech.libeufin.util.getIban
+
+class IbanTest {
+
+    @Test
+    fun genIban() {
+        println(getIban())
+    }
+}
\ 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]