libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit f629b3a17e94f1de1ea70bf59b25c71cdcd72b4f
parent 94740f4989096a12298e89c23c7253a157238da8
Author: ms <ms@taler.net>
Date:   Sat, 18 Sep 2021 09:12:28 +0200

Implement /withdrawal-operation.

Diffstat:
Msandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt | 16++++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt | 14++++++++++++++
Msandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt | 130+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mutil/src/main/kotlin/Config.kt | 4++--
5 files changed, 165 insertions(+), 64 deletions(-)

diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/DB.kt @@ -409,10 +409,26 @@ class BankAccountStatementEntity(id: EntityID<Int>) : IntEntity(id) { object TalerWithdrawalsTable : LongIdTable() { val wopid = uuid("wopid").autoGenerate() + + /** + * Turns to true after the wallet gave the reserve public key + * and the exchange details to the bank. + */ + val selectionDone = bool("selectionDone").default(false) + + /** + * Turns to true after the wire transfer to the exchange bank account + * gets completed _on the bank's side_. This does never guarantees that + * the payment arrived at the exchange's bank yet. + */ + val transferDone = bool("transferDone").default(false) + } class TalerWithdrawalEntity(id: EntityID<Long>) : LongEntity(id) { companion object : LongEntityClass<TalerWithdrawalEntity>(TalerWithdrawalsTable) var wopid by TalerWithdrawalsTable.wopid + var selectionDone by TalerWithdrawalsTable.selectionDone + var transferDone by TalerWithdrawalsTable.transferDone } object BankAccountReportsTable : IntIdTable() { diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/JSON.kt @@ -71,3 +71,17 @@ data class CamtParams( val type: Int, // need range parameter ) + +data class TalerWithdrawalStatus( + val selection_done: Boolean, + val transfer_done: Boolean, + val amount: String, + val wire_types: List<String> = listOf("x-taler-bank"), + val suggested_exchange: String? = null, + val sender_wire: String? = null +) + +data class TalerWithdrawalConfirmation( + val reserve_pub: String, + val exchange_wire_details: String +) diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/Main.kt @@ -84,7 +84,8 @@ import kotlin.random.Random import kotlin.system.exitProcess private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") -private val hostName: String? = getHostnameFromEnv("LIBEUFIN_SANDBOX_HOSTNAME") +private val hostName: String? = getValueFromEnv("LIBEUFIN_SANDBOX_HOSTNAME") +private val currency: String? = getValueFromEnv("LIBEUFIN_SANDBOX_CURRENCY") const val SANDBOX_DB_ENV_VAR_NAME = "LIBEUFIN_SANDBOX_DB_CONNECTION" data class SandboxError( @@ -227,67 +228,15 @@ class MakeTransaction : CliktCommand("Wire-transfer money between Sandbox bank a override fun run() { val dbConnString = getDbConnFromEnv(SANDBOX_DB_ENV_VAR_NAME) Database.connect(dbConnString) - // check accounts exist - transaction { - val credit = BankAccountEntity.find { - BankAccountsTable.label eq creditAccount - }.firstOrNull() ?: run { - System.err.println("Credit account: $creditAccount, not found") - exitProcess(1) - } - val debit = BankAccountEntity.find { - BankAccountsTable.label eq debitAccount - }.firstOrNull() ?: run { - System.err.println("Debit account: $debitAccount, not found") - exitProcess(1) - } - if (credit.currency != debit.currency) { - System.err.println( - "Sandbox has inconsistent state: " + - "currency of credit (${credit.currency}) and debit (${debit.currency}) account differs.") - exitProcess(1) - } - val amountObj = try { - parseAmount(amount) - } catch (e: Exception) { - System.err.println("Amount given not valid: $amount") - exitProcess(1) - } - if (amountObj.currency != credit.currency || amountObj.currency != debit.currency) { - System.err.println("Amount's currency (${amountObj.currency}) can't be accepted") - exitProcess(1) - } - val randId = getRandomString(16) - BankAccountTransactionEntity.new { - creditorIban = credit.iban - creditorBic = credit.bic - creditorName = credit.name - debtorIban = debit.iban - debtorBic = debit.bic - debtorName = debit.name - subject = subjectArg - amount = amountObj.amount.toString() - currency = amountObj.currency - date = getUTCnow().toInstant().toEpochMilli() - accountServicerReference = "sandbox-$randId" - account = debit - direction = "DBIT" - } - BankAccountTransactionEntity.new { - creditorIban = credit.iban - creditorBic = credit.bic - creditorName = credit.name - debtorIban = debit.iban - debtorBic = debit.bic - debtorName = debit.name - subject = subjectArg - amount = amountObj.amount.toString() - currency = amountObj.currency - date = getUTCnow().toInstant().toEpochMilli() - accountServicerReference = "sandbox-$randId" - account = credit - direction = "CRDT" - } + try { + wireTransfer(debitAccount, creditAccount, amount, subjectArg) + } catch (e: SandboxError) { + print(e.message) + exitProcess(1) + } catch (e: Exception) { + // Here, Sandbox is in a highly unstable state. + println(e) + exitProcess(1) } } } @@ -1005,6 +954,10 @@ fun serverMain(dbName: String, port: Int) { hostName != null, "Own hostname not found. Logs should have warned" ) + SandboxAssert( + currency != null, + "Currency not found. Logs should have warned" + ) // check that the three canonical accounts exist val wo = transaction { val exchange = BankAccountEntity.find { @@ -1032,6 +985,59 @@ fun serverMain(dbName: String, port: Int) { call.respondText("taler://withdraw/${hostName}/api/${wo.wopid}") return@get } + /** + * 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("/withdrawal-operation/{wopid}") { + val wopid: String = ensureNonNull("wopid") + val wo = transaction { + + TalerWithdrawalEntity.find { + TalerWithdrawalsTable.wopid eq UUID.fromString(wopid) + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, + "Withdrawal operation: $wopid not found" + ) + } + val ret = TalerWithdrawalStatus( + selection_done = wo.selectionDone, + transfer_done = wo.transferDone, + amount = "${currency}:1" + ) + 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("/withdrawal-operation/{wopid}") { + val wopid = ensureNonNull("wopid") + val body = call.receiveJson<TalerWithdrawalConfirmation>() + + transaction { + var wo = TalerWithdrawalEntity.find { + TalerWithdrawalsTable.wopid eq UUID.fromString(wopid) + }.firstOrNull() ?: throw SandboxError( + HttpStatusCode.NotFound, "Withdrawal operation $wopid not found." + ) + wireTransfer( + "sandbox-account-customer", + "sandbox-account-exchange", + "$currency:1", + body.reserve_pub + ) + wo.selectionDone = true + wo.transferDone = true + } + call.respond(object { + val transfer_done = true + }) + return@post + } } } logger.info("LibEuFin Sandbox running on port $port") diff --git a/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt b/sandbox/src/main/kotlin/tech/libeufin/sandbox/bankAccount.kt @@ -7,6 +7,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import tech.libeufin.util.* import java.math.BigDecimal +import kotlin.system.exitProcess private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.sandbox") @@ -114,3 +115,67 @@ fun historyForAccount(bankAccount: BankAccountEntity): MutableList<RawPayment> { } return history } + +fun wireTransfer( + debitAccount: String, creditAccount: String, + amount: String, subjectArg: String +) { + // check accounts exist + transaction { + val credit = BankAccountEntity.find { + BankAccountsTable.label eq creditAccount + }.firstOrNull() ?: run { + throw SandboxError(HttpStatusCode.NotFound, "Credit account: $creditAccount, not found") + } + val debit = BankAccountEntity.find { + BankAccountsTable.label eq debitAccount + }.firstOrNull() ?: run { + throw SandboxError(HttpStatusCode.NotFound, "Debit account: $debitAccount, not found") + } + if (credit.currency != debit.currency) { + throw SandboxError(HttpStatusCode.InternalServerError, + "Sandbox has inconsistent state: " + + "currency of credit (${credit.currency}) and debit (${debit.currency}) account differs." + ) + } + val amountObj = try { + parseAmount(amount) + } catch (e: Exception) { + throw SandboxError(HttpStatusCode.BadRequest, "Amount given not valid: $amount") + } + if (amountObj.currency != credit.currency || amountObj.currency != debit.currency) { + throw SandboxError(HttpStatusCode.BadRequest, "currency (${amountObj.currency}) can't be accepted") + } + val randId = getRandomString(16) + BankAccountTransactionEntity.new { + creditorIban = credit.iban + creditorBic = credit.bic + creditorName = credit.name + debtorIban = debit.iban + debtorBic = debit.bic + debtorName = debit.name + subject = subjectArg + this.amount = amountObj.amount.toString() + currency = amountObj.currency + date = getUTCnow().toInstant().toEpochMilli() + accountServicerReference = "sandbox-$randId" + account = debit + direction = "DBIT" + } + BankAccountTransactionEntity.new { + creditorIban = credit.iban + creditorBic = credit.bic + creditorName = credit.name + debtorIban = debit.iban + debtorBic = debit.bic + debtorName = debit.name + subject = subjectArg + this.amount = amountObj.amount.toString() + currency = amountObj.currency + date = getUTCnow().toInstant().toEpochMilli() + accountServicerReference = "sandbox-$randId" + account = credit + direction = "CRDT" + } + } +} diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt @@ -50,10 +50,10 @@ fun setLogLevel(logLevel: String?) { } } -fun getHostnameFromEnv(varName: String): String? { +fun getValueFromEnv(varName: String): String? { val hostName = System.getenv(varName) if (hostName.isNullOrBlank() or hostName.isNullOrEmpty()) { - println("WARNING, the hostname wasn't found in env's $varName. Will stay unknown") + println("WARNING, $varName was not found in the environment. Will stay unknown") return null } return hostName