libeufin

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

commit ebfc8ed1420a5fcf8d56efae33de429f6310e522
parent a03159906df6342432c238a6f7956a4872498443
Author: MS <ms@taler.net>
Date:   Wed, 20 Sep 2023 13:52:50 +0200

Taler withdrawal confirmation.

Introducing the SQL procedure that atomically sets
the withdrawal operation as confirmed and wires the
amount to the exchange.

Reserve pubs are stored as TEXT in the withdrawals table,
in order to let the SQL copy it to the payment subject
(without any additional base32 encoding burden).

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Database.kt | 43++++++++++++++++++++++++++++++++-----------
Mbank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt | 4+---
Mbank/src/main/kotlin/tech/libeufin/bank/types.kt | 17++++++++++++++---
Mbank/src/test/kotlin/DatabaseTest.kt | 13+++++++++----
Mdatabase-versioning/libeufin-bank-0001.sql | 2+-
Mdatabase-versioning/procedures.sql | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 144 insertions(+), 22 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt @@ -609,7 +609,7 @@ class Database(private val dbConfig: String) { walletBankAccount = it.getLong("wallet_bank_account"), confirmationDone = it.getBoolean("confirmation_done"), aborted = it.getBoolean("aborted"), - reservePub = it.getBytes("reserve_pub"), + reservePub = it.getString("reserve_pub"), withdrawalUuid = it.getObject("withdrawal_uuid") as UUID ) } @@ -638,9 +638,9 @@ class Database(private val dbConfig: String) { // Values coming from the wallet. fun talerWithdrawalSetDetails( - opUUID: UUID, + opUuid: UUID, exchangePayto: String, - reservePub: ByteArray + reservePub: String ): Boolean { reconnect() val stmt = prepare(""" @@ -650,20 +650,41 @@ class Database(private val dbConfig: String) { """ ) stmt.setString(1, exchangePayto) - stmt.setBytes(2, reservePub) - stmt.setObject(3, opUUID) + stmt.setString(2, reservePub) + stmt.setObject(3, opUuid) return myExecute(stmt) } - fun talerWithdrawalConfirm(opUUID: UUID): Boolean { + + fun talerWithdrawalConfirm( + opUuid: UUID, + timestamp: Long, + accountServicerReference: String = "NOT-USED", + endToEndId: String = "NOT-USED", + paymentInfId: String = "NOT-USED" + ): WithdrawalConfirmationResult { reconnect() val stmt = prepare(""" - UPDATE taler_withdrawal_operations - SET confirmation_done = true - WHERE withdrawal_uuid=? + SELECT + out_nx_op, + out_nx_exchange, + out_insufficient_funds + FROM confirm_taler_withdrawal(?, ?, ?, ?, ?); """ ) - stmt.setObject(1, opUUID) - return myExecute(stmt) + stmt.setObject(1, opUuid) + stmt.setLong(2, timestamp) + stmt.setString(3, accountServicerReference) + stmt.setString(4, endToEndId) + stmt.setString(5, paymentInfId) + val res = stmt.executeQuery() + res.use { + if (!res.next()) + throw internalServerError("No result from DB procedure confirm_taler_withdrawal") + if (it.getBoolean("out_nx_op")) return WithdrawalConfirmationResult.OP_NOT_FOUND + if (it.getBoolean("out_nx_exchange")) return WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND + if (it.getBoolean("out_insufficient_funds")) return WithdrawalConfirmationResult.BALANCE_INSUFFICIENT + } + return WithdrawalConfirmationResult.SUCCESS } fun cashoutCreate(op: Cashout): Boolean { diff --git a/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/talerWebHandlers.kt @@ -111,9 +111,7 @@ fun Routing.talerWebHandlers() { confirmation_done = op.confirmationDone, selection_done = op.selectionDone, selected_exchange_account = op.selectedExchangePayto, - selected_reserve_pub = if (op.reservePub != null) { - Base32Crockford.encode(op.reservePub) - } else null + selected_reserve_pub = op.reservePub )) return@get } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/types.kt b/bank/src/main/kotlin/tech/libeufin/bank/types.kt @@ -308,7 +308,7 @@ data class TalerWithdrawalOperation( val selectionDone: Boolean = false, val aborted: Boolean = false, val confirmationDone: Boolean = false, - val reservePub: ByteArray?, + val reservePub: String?, val selectedExchangePayto: String?, val walletBankAccount: Long ) @@ -428,4 +428,15 @@ fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean { * has the rights on the resource. */ fun ApplicationCall.getResourceName(param: String): ResourceName = - this.expectUriComponent(param) -\ No newline at end of file + this.expectUriComponent(param) + +/** + * This type communicates the result of a database operation + * to confirm one withdrawal operation. + */ +enum class WithdrawalConfirmationResult { + SUCCESS, + OP_NOT_FOUND, + EXCHANGE_NOT_FOUND, + BALANCE_INSUFFICIENT +} +\ No newline at end of file diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt @@ -201,6 +201,8 @@ class DatabaseTest { val uuid = UUID.randomUUID() assert(db.customerCreate(customerFoo) != null) assert(db.bankAccountCreate(bankAccountFoo)) + assert(db.customerCreate(customerBar) != null) // plays the exchange. + assert(db.bankAccountCreate(bankAccountBar)) // insert new. assert(db.talerWithdrawalCreate( uuid, @@ -212,13 +214,16 @@ class DatabaseTest { assert(op?.walletBankAccount == 1L && op.withdrawalUuid == uuid) // Setting the details. assert(db.talerWithdrawalSetDetails( - uuid, - "exchange-payto", - ByteArray(32) + opUuid = uuid, + exchangePayto = "BAR-IBAN-ABC", + reservePub = "UNCHECKED-RESERVE-PUB" )) val opSelected = db.talerWithdrawalGet(uuid) assert(opSelected?.selectionDone == true && !opSelected.confirmationDone) - assert(db.talerWithdrawalConfirm(uuid)) + println( + db.talerWithdrawalConfirm(uuid, 1L) // == + // WithdrawalConfirmationResult.SUCCESS + ) // Finally confirming the operation (means customer wired funds to the exchange.) assert(db.talerWithdrawalGet(uuid)?.confirmationDone == true) } diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -371,7 +371,7 @@ CREATE TABLE IF NOT EXISTS taler_withdrawal_operations ,selection_done BOOLEAN DEFAULT FALSE NOT NULL ,aborted BOOLEAN DEFAULT FALSE NOT NULL ,confirmation_done BOOLEAN DEFAULT FALSE NOT NULL - ,reserve_pub BYTEA CHECK(length(reserve_pub)=32) NULL + ,reserve_pub TEXT NULL -- Kotlin must check it's valid. ,selected_exchange_payto TEXT ,wallet_bank_account BIGINT NOT NULL REFERENCES bank_accounts(bank_account_id) diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql @@ -86,6 +86,93 @@ END $$; COMMENT ON PROCEDURE bank_set_config(TEXT, TEXT) IS 'Update or insert configuration values'; +CREATE OR REPLACE FUNCTION confirm_taler_withdrawal( + IN in_withdrawal_uuid uuid, + IN in_confirmation_date BIGINT, + IN in_acct_svcr_ref TEXT, + IN in_pmt_inf_id TEXT, + IN in_end_to_end_id TEXT, + OUT out_nx_op BOOLEAN, + -- can't use out_balance_insufficient, because + -- it conflicts with the return column of the called + -- function that moves the funds. FIXME? + OUT out_insufficient_funds BOOLEAN, + OUT out_nx_exchange BOOLEAN +) +LANGUAGE plpgsql +AS $$ +DECLARE + confirmation_done_local BOOLEAN; + reserve_pub_local TEXT; + selected_exchange_payto_local TEXT; + wallet_bank_account_local BIGINT; + amount_local taler_amount; + exchange_bank_account_id BIGINT; + maybe_balance_insufficient BOOLEAN; +BEGIN +SELECT -- Really no-star policy and instead DECLARE almost one var per column? + confirmation_done, + reserve_pub, + selected_exchange_payto, + wallet_bank_account, + (amount).val, (amount).frac + INTO + confirmation_done_local, + reserve_pub_local, + selected_exchange_payto_local, + wallet_bank_account_local, + amount_local.val, amount_local.frac + FROM taler_withdrawal_operations + WHERE withdrawal_uuid=in_withdrawal_uuid; +IF NOT FOUND +THEN + out_nx_op=TRUE; + RETURN; +END IF; +out_nx_op=FALSE; +IF (confirmation_done_local) +THEN + RETURN; -- nothing to do, idempotentially returning. +END IF; +-- exists and wasn't confirmed, do it. +UPDATE taler_withdrawal_operations + SET confirmation_done = true + WHERE withdrawal_uuid=in_withdrawal_uuid; +-- sending the funds to the exchange, but needs first its bank account row ID +SELECT + bank_account_id + INTO exchange_bank_account_id + FROM bank_accounts + WHERE internal_payto_uri = selected_exchange_payto_local; +IF NOT FOUND +THEN + out_nx_exchange=TRUE; + RETURN; +END IF; +out_nx_exchange=FALSE; +SELECT -- not checking for accounts existence, as it was done above. + out_balance_insufficient + INTO + maybe_balance_insufficient +FROM bank_wire_transfer( + exchange_bank_account_id, + wallet_bank_account_local, + reserve_pub_local, + amount_local, + in_confirmation_date, + in_acct_svcr_ref, + in_pmt_inf_id, + in_end_to_end_id +); +IF (maybe_balance_insufficient) +THEN + out_insufficient_funds=TRUE; +END IF; +out_insufficient_funds=FALSE; +END $$; +COMMENT ON FUNCTION confirm_taler_withdrawal(uuid, bigint, text, text, text) + IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.'; + CREATE OR REPLACE FUNCTION bank_wire_transfer( IN in_creditor_account_id BIGINT, IN in_debtor_account_id BIGINT,