commit 04c5aa01c3b57e3c0c30795c0d9fdf0aeab10844 parent cf0c4ebfb782e3262cd4241d547d2d855c625e83 Author: MS <ms@taler.net> Date: Sat, 1 Apr 2023 22:06:11 +0200 x-libeufin-bank connection. Adding payment submission, the "connect()" method, and the CLI command to create a new x-libeufin-bank connection. Diffstat:
14 files changed, 241 insertions(+), 95 deletions(-)
diff --git a/cli/bin/libeufin-cli b/cli/bin/libeufin-cli @@ -498,6 +498,45 @@ def restore_backup(obj, backup_file, passphrase, connection_name): check_response_status(resp) +@connections.command(help="make a new x-libeufin-bank connection") +@click.option( + "--bank-url", + help="Bank base URL, typically ending with '../demobanks/default/access-api'", + required=True +) +@click.option( + "--username", + help="Username at the bank, that this connection impersonates.", + required=True +) +@click.password_option() +@click.argument("connection-name") +@click.pass_obj +def new_xlibeufinbank_connection(obj, bank_url, username, password, connection_name): + url = urljoin_nodrop(obj.nexus_base_url, "/bank-connections") + body = dict( + name=connection_name, + source="new", + type="x-libeufin-bank", + data=dict( + baseUrl=bank_url, + username=username, + password=password + ), + ) + try: + resp = post( + url, + json=body, + auth=auth.HTTPBasicAuth(obj.username, obj.password) + ) + except Exception as e: + print(e) + print(f"Could not reach nexus at {url}") + exit(1) + + check_response_status(resp) + @connections.command(help="make new EBICS bank connection") @click.option("--ebics-url", help="EBICS URL", required=True) @click.option("--host-id", help="Host ID", required=True) diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/BankConnectionProtocol.kt @@ -25,10 +25,13 @@ import io.ktor.http.HttpStatusCode import tech.libeufin.nexus.ebics.* import tech.libeufin.nexus.server.BankConnectionType import tech.libeufin.nexus.server.FetchSpecJson +import tech.libeufin.nexus.server.XLibeufinBankTransport +import tech.libeufin.nexus.xlibeufinbank.XlibeufinBankConnectionProtocol // 'const' allows only primitive types. val bankConnectionRegistry: Map<BankConnectionType, BankConnectionProtocol> = mapOf( - BankConnectionType.EBICS to EbicsBankConnectionProtocol() + BankConnectionType.EBICS to EbicsBankConnectionProtocol(), + BankConnectionType.X_LIBEUFIN_BANK to XlibeufinBankConnectionProtocol() ) interface BankConnectionProtocol { diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/DB.kt @@ -237,9 +237,7 @@ class NexusBankTransactionEntity(id: EntityID<Long>) : LongEntity(id) { } } -/** - * Represents a prepared payment. - */ +// Represents a prepared payment. object PaymentInitiationsTable : LongIdTable() { /** * Bank account that wants to initiate the payment. @@ -259,7 +257,6 @@ object PaymentInitiationsTable : LongIdTable() { val submitted = bool("submitted").default(false) var invalid = bool("invalid").nullable() val messageId = text("messageId") - /** * Points at the raw transaction witnessing that this * initiated payment was successfully performed. diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Taler.kt @@ -247,7 +247,6 @@ 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 diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt @@ -125,17 +125,15 @@ suspend fun submitAllPaymentInitiations( "(pointed by bank account '${it.bankAccount.bankAccountName}')" + " not found in the database." ) - // Filter out non EBICS. - if (bankConnection.type != "ebics") { + try { BankConnectionType.parseBankConnectionType(bankConnection.type) } + catch (e: Exception) { logger.info("Skipping non-implemented bank connection '${bankConnection.type}'") return@forEach } workQueue.add(Submission(it.id.value)) } } - workQueue.forEach { - submitPaymentInitiation(httpClient, it.id) - } + workQueue.forEach { submitPaymentInitiation(httpClient, it.id) } } /** diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt @@ -125,10 +125,8 @@ private suspend fun fetchEbicsC5x( } when (historyType) { - "C52" -> { - } - "C53" -> { - } + "C52" -> {} + "C53" -> {} else -> { throw NexusError(HttpStatusCode.BadRequest, "history type '$historyType' not supported") } @@ -526,7 +524,10 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol { override suspend fun submitPaymentInitiation(httpClient: HttpClient, paymentInitiationId: Long) { val dbData = transaction { val preparedPayment = getPaymentInitiation(paymentInitiationId) - val conn = preparedPayment.bankAccount.defaultBankConnection ?: throw NexusError(HttpStatusCode.NotFound, "no default bank connection available for submission") + 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( diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/xlibeufinbank/XLibeufinBankNexus.kt @@ -15,10 +15,7 @@ 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 tech.libeufin.util.* import java.net.MalformedURLException import java.net.URL @@ -46,7 +43,16 @@ fun getXLibeufinBankCredentials(connId: String): XLibeufinBankTransport { class XlibeufinBankConnectionProtocol : BankConnectionProtocol { override suspend fun connect(client: HttpClient, connId: String) { - TODO("Not yet implemented") + // Only checking that the credentials + bank URL are correct. + val conn = getBankConnection(connId) + val credentials = getXLibeufinBankCredentials(conn) + // Defining the URL to request the bank account balance. + val url = credentials.baseUrl + "/accounts/${credentials.username}" + // Error handling expected by the caller. + client.get(url) { + expectSuccess = true + basicAuth(credentials.username, credentials.password) + } } override suspend fun fetchAccounts(client: HttpClient, connId: String) { @@ -66,7 +72,6 @@ class XlibeufinBankConnectionProtocol : BankConnectionProtocol { connId: String, user: NexusUserEntity, data: JsonNode) { - val bankConn = transaction { NexusBankConnectionEntity.new { this.connectionId = connId @@ -105,8 +110,67 @@ class XlibeufinBankConnectionProtocol : BankConnectionProtocol { 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 submitPaymentInitiation( + httpClient: HttpClient, + paymentInitiationId: Long + ) { + /** + * Main steps. + * + * 1) Get prep from the DB. + * 2) Collect credentials. + * 3) Create the format to POST. + * 4) POST the transaction. + * 5) Mark the prep as submitted. + * */ + // 1 + val preparedPayment = getPaymentInitiation(paymentInitiationId) + // 2 + val conn = transaction { preparedPayment.bankAccount.defaultBankConnection } ?: throw + internalServerError("Default connection not found for bank account: ${preparedPayment.bankAccount.bankAccountName}") + val credentials: XLibeufinBankTransport = getXLibeufinBankCredentials(conn) + // 3 + val paytoUri = buildIbanPaytoUri( + iban = preparedPayment.creditorIban, + bic = preparedPayment.creditorBic ?: "SANDBOXX", + receiverName = preparedPayment.creditorName, + message = preparedPayment.subject + ) + val req = jacksonObjectMapper().writerWithDefaultPrettyPrinter().writeValueAsString( + XLibeufinBankPaytoReq( + paytoUri = paytoUri, + amount = "${preparedPayment.currency}:${preparedPayment.sum}", + pmtInfId = preparedPayment.paymentInformationId + ) + ) + // 4 + val url = credentials.baseUrl + "/accounts/${credentials.username}/transactions" + logger.debug("POSTing transactions to x-libeufin-bank at: $url") + val r = httpClient.post(url) { + expectSuccess = false + contentType(ContentType.Application.Json) + basicAuth(credentials.username, credentials.password) + setBody(req) + } + if (r.status.value.toString().startsWith("5")) { + throw NexusError( + HttpStatusCode.BadGateway, + "The bank failed: ${r.bodyAsText()}" + ) + } + if (!r.status.value.toString().startsWith("2")) { + throw NexusError( + /** + * Echoing whichever status code the bank gave. That + * however masks client errors where - for example - a + * request detail causes 404 where Nexus has no power. + */ + HttpStatusCode(r.status.value, r.status.description), + r.bodyAsText() + ) + } + // 5 + transaction { preparedPayment.submitted = true } } override suspend fun fetchTransactions( @@ -132,7 +196,7 @@ class XlibeufinBankConnectionProtocol : BankConnectionProtocol { /** * 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. + * 'previous-days' are NOT implemented. */ val baseUrl = URL(credentials.baseUrl) val fetchUrl = url { @@ -237,9 +301,7 @@ fun processXLibeufinBankMessage( } // Searching for duplicates. if (findDuplicate(bankAccountId, it.uid) != null) { - logger.debug( - "x-libeufin-bank ingestion: transaction ${it.uid} is a duplicate, skipping." - ) + logger.debug("x-libeufin-bank ingestion: transaction ${it.uid} is a duplicate, skipping.") return@forEach } val direction = if (it.debtorIban == bankAccount.iban) @@ -268,7 +330,7 @@ fun processXLibeufinBankMessage( * (outgoing) payment with the one being iterated over. */ if (direction == XLibeufinBankDirection.DEBIT) { - val maybePrepared = getPaymentInitiation(pmtInfId = it.uid) + val maybePrepared = it.pmtInfId?.let { it1 -> getPaymentInitiation(pmtInfId = it1) } if (maybePrepared != null) maybePrepared.confirmationTransaction = localTx } // x-libeufin-bank transactions are ALWAYS modeled as reports diff --git a/nexus/src/test/kotlin/EbicsTest.kt b/nexus/src/test/kotlin/EbicsTest.kt @@ -1,7 +1,4 @@ -import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import io.ktor.server.application.* -import io.ktor.client.* -import io.ktor.client.request.* import io.ktor.http.* import io.ktor.server.plugins.contentnegotiation.* import io.ktor.server.request.* @@ -23,14 +20,17 @@ import tech.libeufin.nexus.iso20022.NexusPaymentInitiationData import tech.libeufin.nexus.iso20022.createPain001document import tech.libeufin.nexus.server.FetchLevel import tech.libeufin.nexus.server.FetchSpecAllJson -import tech.libeufin.nexus.server.FetchSpecJson import tech.libeufin.nexus.server.Pain001Data import tech.libeufin.sandbox.* import tech.libeufin.util.* import tech.libeufin.util.ebics_h004.EbicsRequest import tech.libeufin.util.ebics_h004.EbicsResponse import tech.libeufin.util.ebics_h004.EbicsTypes -import kotlin.reflect.full.isSubclassOf + +/** + * These test cases run EBICS CCT and C52, mixing ordinary operations + * and some error cases. + */ /** * Data to make the test server return for EBICS @@ -87,9 +87,7 @@ fun getCustomEbicsServer(r: EbicsResponses, endpoint: String = "/ebicsweb"): App } class DownloadAndSubmit { - /** - * Download a C52 report from the bank. - */ + // Downloads a C52 report from the bank. @Test fun download() { withNexusAndSandboxUser { @@ -129,9 +127,8 @@ class DownloadAndSubmit { } } } - /** - * Upload one payment instruction to the bank. - */ + + // Uploads one payment instruction to the bank. @Test fun upload() { withNexusAndSandboxUser { @@ -223,8 +220,8 @@ class DownloadAndSubmit { } /** - * Submit one payment instruction with an invalid Pain.001 - * document, and check that it was marked as invalid. Hence, + * Submits one payment instruction with an invalid Pain.001 + * document, and checks 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. */ @@ -264,6 +261,10 @@ class DownloadAndSubmit { } } + /** + * Submits one pain.001 document with the wrong currency and checks + * that the bank responded with EBICS_PROCESSING_ERROR. + */ @Test fun unsupportedCurrency() { withNexusAndSandboxUser { @@ -278,7 +279,7 @@ class DownloadAndSubmit { creditorName = "Tester", subject = "test payment", sum = "1", - currency = "EUR" + currency = "EUR" // EUR not supported. ), transaction { NexusBankAccountEntity.findByName("foo") ?: throw Exception("Test failed") diff --git a/nexus/src/test/kotlin/TalerTest.kt b/nexus/src/test/kotlin/TalerTest.kt @@ -18,53 +18,61 @@ import tech.libeufin.nexus.talerFilter import tech.libeufin.sandbox.sandboxApp import tech.libeufin.sandbox.wireTransfer import tech.libeufin.util.NotificationsChannelDomains +import tech.libeufin.util.getIban // This class tests the features related to the Taler facade. class TalerTest { - val mapper = ObjectMapper() + private val mapper = ObjectMapper() + + @Test + fun historyOutgoingTestEbics() { + historyOutgoingTest("foo") + } + @Test + fun historyOutgoingTestXLibeufinBank() { + historyOutgoingTest("bar") + } // Checking that a call to POST /transfer results in // an outgoing payment in GET /history/outgoing. - @Test - fun historyOutgoingTest() { + fun historyOutgoingTest(testedAccount: String) { withNexusAndSandboxUser { testApplication { application(nexusApp) - client.post("/facades/foo-facade/taler-wire-gateway/transfer") { + client.post("/facades/$testedAccount-facade/taler-wire-gateway/transfer") { contentType(ContentType.Application.Json) - basicAuth("foo", "foo") // exchange's credentials + basicAuth(testedAccount, testedAccount) // exchange's credentials expectSuccess = true setBody(""" { "request_uid": "twg_transfer_0", "amount": "TESTKUDOS:3", "exchange_base_url": "http://exchange.example.com/", "wtid": "T0", - "credit_account": "payto://iban/${BAR_USER_IBAN}?receiver-name=Bar" - + "credit_account": "payto://iban/${BANK_IBAN}?receiver-name=Not-Used" } """.trimIndent()) } } - /* The EBICS layer sends the payment instruction to the bank here. - * and the reconciliation mechanism in Nexus should detect that one - * outgoing payment was indeed the one instructed via the TWG. The - * reconciliation will make the outgoing payment visible via /history/outgoing. - * The following block achieve this by starting Sandbox and sending all - * the prepared payments to it. + /* The bank connection sends the payment instruction to the bank here. + * and the reconciliation mechanism in Nexus should detect that one + * outgoing payment was indeed the one instructed via the TWG. The + * reconciliation will make the outgoing payment visible via /history/outgoing. + * The following block achieve this by starting Sandbox and sending all + * the prepared payments to it. */ testApplication { application(sandboxApp) - submitAllPaymentInitiations(client, "foo") + submitAllPaymentInitiations(client, testedAccount) /* Now downloads transactions from the bank, where the payment submitted in the previous block is expected to appear as outgoing. */ fetchBankAccountTransactions( client, fetchSpec = FetchSpecAllJson( - level = FetchLevel.REPORT, - "foo" + level = if (testedAccount == "bar") FetchLevel.STATEMENT else FetchLevel.REPORT, + bankConnection = testedAccount ), - "foo" + accountId = testedAccount ) } /** @@ -73,10 +81,10 @@ class TalerTest { */ testApplication { application(nexusApp) - val r = client.get("/facades/foo-facade/taler-wire-gateway/history/outgoing?delta=5") { + val r = client.get("/facades/$testedAccount-facade/taler-wire-gateway/history/outgoing?delta=5") { expectSuccess = true contentType(ContentType.Application.Json) - basicAuth("foo", "foo") + basicAuth(testedAccount, testedAccount) } val j = mapper.readTree(r.readBytes()) val wtidFromTwg = j.get("outgoing_transactions").get(0).get("wtid").asText() @@ -103,7 +111,7 @@ class TalerTest { ) } - // Tests that even if one call is long-polling, other calls + // Tests that even if one call is long-polling, other calls respond. @Test fun servingTest() { withTestDatabase { @@ -135,7 +143,7 @@ class TalerTest { // 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) { + private fun historyIncomingTest(testedAccount: String, connType: BankConnectionType) { val reservePub = "GX5H5RME193FDRCM1HZKERXXQ2K21KH7788CKQM8X6MYKYRBP8F0" withNexusAndSandboxUser { testApplication { @@ -163,10 +171,6 @@ class TalerTest { } 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", diff --git a/nexus/src/test/kotlin/XLibeufinBankTest.kt b/nexus/src/test/kotlin/XLibeufinBankTest.kt @@ -3,17 +3,18 @@ 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.* +import tech.libeufin.nexus.bankaccount.addPaymentInitiation 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.BankAccountTransactionEntity +import tech.libeufin.sandbox.BankAccountTransactionsTable import tech.libeufin.sandbox.sandboxApp import tech.libeufin.sandbox.wireTransfer import tech.libeufin.util.XLibeufinBankTransaction +import tech.libeufin.util.getIban import java.net.URL // Testing the x-libeufin-bank communication @@ -34,9 +35,37 @@ class XLibeufinBankTest { */ @Test fun submitTransaction() { - + withTestDatabase { + prepSandboxDb() + prepNexusDb() + testApplication { + application(sandboxApp) + val pId = addPaymentInitiation( + Pain001Data( + creditorIban = FOO_USER_IBAN, + creditorBic = "SANDBOXX", + creditorName = "Tester", + subject = "test payment", + sum = "1", + currency = "TESTKUDOS" + ), + transaction { + NexusBankAccountEntity.findByName("bar") ?: + throw Exception("Test failed, env didn't provide Nexus bank account 'bar'") + } + ) + val conn = XlibeufinBankConnectionProtocol() + conn.submitPaymentInitiation(this.client, pId.id.value) + val maybeArrivedPayment = transaction { + BankAccountTransactionEntity.find { + BankAccountTransactionsTable.pmtInfId eq pId.paymentInformationId + }.firstOrNull() + } + // Now look for the payment in the database. + assert(maybeArrivedPayment != null) + } + } } - /** * Testing that Nexus downloads one transaction from * Sandbox via the x-libeufin-bank protocol supplier diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -19,7 +19,6 @@ package tech.libeufin.sandbox -import com.fasterxml.jackson.annotation.JsonProperty import tech.libeufin.util.PaymentInfo data class WithdrawalRequest( @@ -148,16 +147,6 @@ data class TalerWithdrawalSelection( val selected_exchange: String? ) -data class NewTransactionReq( - /** - * This Payto address must contain the wire transfer - * subject among its query parameters -- 'message' parameter. - */ - val paytoUri: String, - // $currency:X.Y format - val amount: String? -) - data class SandboxConfig( val currency: String, val version: String, diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -1259,7 +1259,7 @@ val sandboxApp: Application.() -> Unit = { val authGranted: Boolean = !WITH_AUTH if (!authGranted && username != bankAccount.label) throw unauthorized("Username '$username' has no rights over bank account ${bankAccount.label}") - val req = call.receive<NewTransactionReq>() + val req = call.receive<XLibeufinBankPaytoReq>() val payto = parsePayto(req.paytoUri) val amount: String? = payto.amount ?: req.amount if (amount == null) throw badRequest("Amount is missing") @@ -1274,7 +1274,8 @@ val sandboxApp: Application.() -> Unit = { subject = payto.message ?: throw badRequest( "'message' query parameter missing in Payto address" ), - amount = amount + amount = amount, + pmtInfId = req.pmtInfId ) } call.respond(object {}) diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt @@ -52,6 +52,22 @@ enum class XLibeufinBankDirection(val direction: String) { } } } + +data class XLibeufinBankPaytoReq( + /** + * This Payto address MUST contain the wire transfer + * subject among its query parameters -- 'message' parameter. + */ + val paytoUri: String, + // $currency:X.Y format + val amount: String?, + /** + * This value MAY be specified by the payment submitter to + * help reconcile the payment when they later download new + * transactions. The name is only borrowed from CaMt terminology. + */ + val pmtInfId: String? +) data class XLibeufinBankTransaction( val creditorIban: String, val creditorBic: String?, @@ -66,9 +82,11 @@ data class XLibeufinBankTransaction( val date: String, 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. + /** + * 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 ) diff --git a/util/src/main/kotlin/Payto.kt b/util/src/main/kotlin/Payto.kt @@ -5,10 +5,9 @@ import io.ktor.http.* import java.net.URI import java.net.URLDecoder import java.net.URLEncoder +import javax.security.auth.Subject -/** - * Payto information. - */ +// Payto information. data class Payto( // represent query param "sender-name" or "receiver-name". val receiverName: String?, @@ -78,9 +77,15 @@ fun parsePayto(payto: String): Payto { fun buildIbanPaytoUri( iban: String, - bic: String?, + bic: String, receiverName: String, + message: String? = null ): String { val nameUrlEnc = URLEncoder.encode(receiverName, "utf-8") - return "payto://iban/${if (bic != null) "$bic/" else ""}$iban?receiver-name=$nameUrlEnc" + val ret = "payto://iban/$bic/$iban?receiver-name=$nameUrlEnc" + if (message != null) { + val messageUrlEnc = URLEncoder.encode(receiverName, "utf-8") + return "$ret&message=$messageUrlEnc" + } + return ret } \ No newline at end of file