commit 640cc009d787ee69aaef8e32b35dcbd74d932c73
parent 15269b462cc172258702377babe03529de29234c
Author: Antoine A <>
Date: Fri, 13 Oct 2023 13:25:20 +0000
Improve wire gateway API
Diffstat:
6 files changed, 490 insertions(+), 238 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/CorebankApiHandlers.kt
@@ -246,7 +246,8 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
when (db.bankTransactionCreate(adminPaysBonus)) {
BankTransactionResult.NO_CREDITOR -> throw internalServerError("Bonus impossible: creditor not found, despite its recent creation.")
BankTransactionResult.NO_DEBTOR -> throw internalServerError("Bonus impossible: admin not found.")
- BankTransactionResult.CONFLICT -> throw internalServerError("Bonus impossible: admin has insufficient balance.")
+ BankTransactionResult.BALANCE_INSUFFICIENT -> throw internalServerError("Bonus impossible: admin has insufficient balance.")
+ BankTransactionResult.SAME_ACCOUNT -> throw internalServerError("Bonus impossible: admin should not be creditor.")
BankTransactionResult.SUCCESS -> {/* continue the execution */
}
}
@@ -546,10 +547,14 @@ fun Routing.accountsMgmtHandlers(db: Database, ctx: BankApplicationContext) {
)
val res = db.bankTransactionCreate(dbInstructions)
when (res) {
- BankTransactionResult.CONFLICT -> throw conflict(
+ BankTransactionResult.BALANCE_INSUFFICIENT -> throw conflict(
"Insufficient funds",
TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
)
+ BankTransactionResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
BankTransactionResult.NO_CREDITOR -> throw internalServerError("Creditor not found despite previous checks.")
BankTransactionResult.NO_DEBTOR -> throw internalServerError("Debtor not found despite the request was authenticated.")
BankTransactionResult.SUCCESS -> call.respond(HttpStatusCode.OK)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -733,12 +733,13 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
conn.transaction {
val stmt = conn.prepareStatement("""
SELECT
- out_nx_creditor
- ,out_nx_debtor
- ,out_balance_insufficient
+ out_same_account
+ ,out_debtor_not_found
+ ,out_creditor_not_found
+ ,out_balance_insufficient
,out_credit_row_id
,out_debit_row_id
- ,out_creditor_is_exchange
+ ,out_creditor_is_exchange
,out_debtor_is_exchange
FROM bank_wire_transfer(?,?,TEXT(?),(?,?)::taler_amount,?,TEXT(?),TEXT(?),TEXT(?))
"""
@@ -755,24 +756,24 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
stmt.executeQuery().use {
when {
!it.next() -> throw internalServerError("Bank transaction didn't properly return")
- it.getBoolean("out_nx_debtor") -> {
+ it.getBoolean("out_debtor_not_found") -> {
logger.error("No debtor account found")
BankTransactionResult.NO_DEBTOR
}
- it.getBoolean("out_nx_creditor") -> {
+ it.getBoolean("out_creditor_not_found") -> {
logger.error("No creditor account found")
BankTransactionResult.NO_CREDITOR
}
+ it.getBoolean("out_same_account") -> BankTransactionResult.SAME_ACCOUNT
it.getBoolean("out_balance_insufficient") -> {
logger.error("Balance insufficient")
- BankTransactionResult.CONFLICT
+ BankTransactionResult.BALANCE_INSUFFICIENT
}
else -> {
val metadata = TxMetadata.parse(tx.subject)
if (it.getBoolean("out_creditor_is_exchange")) {
val rowId = it.getLong("out_credit_row_id")
if (metadata is IncomingTxMetadata) {
-
val stmt = conn.prepareStatement("""
INSERT INTO taler_exchange_incoming
(reserve_pub, bank_transaction)
@@ -884,7 +885,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
private suspend fun <T> poolHistory(
params: HistoryParams,
bankAccountId: Long,
- listen: suspend NotificationWatcher.(Long, suspend (Flow<Notification>) -> Unit) -> Unit,
+ listen: suspend NotificationWatcher.(Long, suspend (Flow<Long>) -> Unit) -> Unit,
query: String,
map: (ResultSet) -> T
): List<T> {
@@ -940,7 +941,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
if (missing > 0) {
withTimeoutOrNull(params.poll_ms) {
buffered
- .filter { it.rowId > min } // Skip transactions already checked
+ .filter { it > min } // Skip transactions already checked
.take(missing).count() // Wait for missing transactions
}
@@ -1235,9 +1236,9 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
): WithdrawalConfirmationResult = conn { conn ->
val stmt = conn.prepareStatement("""
SELECT
- out_nx_op,
- out_nx_exchange,
- out_insufficient_funds,
+ out_no_op,
+ out_exchange_not_found,
+ out_balance_insufficient,
out_already_confirmed_conflict
FROM confirm_taler_withdrawal(?, ?, ?, ?, ?);
"""
@@ -1251,9 +1252,9 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
when {
!it.next() ->
throw internalServerError("No result from DB procedure confirm_taler_withdrawal")
- it.getBoolean("out_nx_op") -> WithdrawalConfirmationResult.OP_NOT_FOUND
- it.getBoolean("out_nx_exchange") -> WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND
- it.getBoolean("out_insufficient_funds") -> WithdrawalConfirmationResult.BALANCE_INSUFFICIENT
+ it.getBoolean("out_no_op") -> WithdrawalConfirmationResult.OP_NOT_FOUND
+ it.getBoolean("out_exchange_not_found") -> WithdrawalConfirmationResult.EXCHANGE_NOT_FOUND
+ it.getBoolean("out_balance_insufficient") -> WithdrawalConfirmationResult.BALANCE_INSUFFICIENT
it.getBoolean("out_already_confirmed_conflict") -> WithdrawalConfirmationResult.CONFLICT
else -> WithdrawalConfirmationResult.SUCCESS
}
@@ -1470,7 +1471,7 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
val txRowId: Long? = null,
val timestamp: TalerProtocolTimestamp? = null
)
- /**
+ /** TODO better doc
* This function calls the SQL function that (1) inserts the TWG
* requests details into the database and (2) performs the actual
* bank transaction to pay the merchant according to the 'req' parameter.
@@ -1492,26 +1493,20 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
val subject = OutgoingTxMetadata(req.wtid, req.exchange_base_url).toString()
val stmt = conn.prepareStatement("""
SELECT
- out_exchange_balance_insufficient
- ,out_nx_debitor
- ,out_nx_exchange
- ,out_nx_creditor
- ,out_tx_row_id
- ,out_request_uid_reuse
- ,out_timestamp
+ out_debtor_not_found
+ ,out_debtor_not_exchange
+ ,out_creditor_not_found
+ ,out_same_account
+ ,out_both_exchanges
+ ,out_request_uid_reuse
+ ,out_exchange_balance_insufficient
+ ,out_tx_row_id
+ ,out_timestamp
FROM
taler_transfer (
- ?,
- ?,
- ?,
+ ?, ?, ?,
(?,?)::taler_amount,
- ?,
- ?,
- ?,
- ?,
- ?,
- ?,
- ?
+ ?, ?, ?, ?, ?, ?, ?
);
""")
@@ -1532,16 +1527,20 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
when {
!it.next() ->
throw internalServerError("SQL function taler_transfer did not return anything.")
- it.getBoolean("out_nx_debitor") ->
+ it.getBoolean("out_debtor_not_found") ->
TalerTransferCreationResult(TalerTransferResult.NO_DEBITOR)
- it.getBoolean("out_nx_exchange") ->
+ it.getBoolean("out_debtor_not_exchange") ->
TalerTransferCreationResult(TalerTransferResult.NOT_EXCHANGE)
- it.getBoolean("out_request_uid_reuse") ->
- TalerTransferCreationResult(TalerTransferResult.REQUEST_UID_REUSE)
+ it.getBoolean("out_creditor_not_found") ->
+ TalerTransferCreationResult(TalerTransferResult.NO_CREDITOR)
+ it.getBoolean("out_same_account") ->
+ TalerTransferCreationResult(TalerTransferResult.SAME_ACCOUNT)
+ it.getBoolean("out_both_exchanges") ->
+ TalerTransferCreationResult(TalerTransferResult.BOTH_EXCHANGE)
it.getBoolean("out_exchange_balance_insufficient") ->
TalerTransferCreationResult(TalerTransferResult.BALANCE_INSUFFICIENT)
- it.getBoolean("out_nx_creditor") ->
- TalerTransferCreationResult(TalerTransferResult.NO_CREDITOR)
+ it.getBoolean("out_request_uid_reuse") ->
+ TalerTransferCreationResult(TalerTransferResult.REQUEST_UID_REUSE)
else -> {
TalerTransferCreationResult(
txResult = TalerTransferResult.SUCCESS,
@@ -1554,21 +1553,153 @@ class Database(dbConfig: String, private val bankCurrency: String): java.io.Clos
}
}
}
+
+ data class TalerAddIncomingCreationResult(
+ val txResult: TalerAddIncomingResult,
+ val txRowId: Long? = null
+ )
+
+ /** TODO better doc
+ * This function calls the SQL function that (1) inserts the TWG
+ * requests details into the database and (2) performs the actual
+ * bank transaction to pay the merchant according to the 'req' parameter.
+ *
+ * 'req' contains the same data that was POSTed by the exchange
+ * to the TWG /transfer endpoint. The exchangeBankAccountId parameter
+ * is the row ID of the exchange's bank account. The return type
+ * is the same returned by "bank_wire_transfer()" where however
+ * the NO_DEBTOR error will hardly take place.
+ */
+ suspend fun talerAddIncomingCreate(
+ req: AddIncomingRequest,
+ username: String,
+ timestamp: Instant,
+ acctSvcrRef: String = "not used",
+ pmtInfId: String = "not used",
+ endToEndId: String = "not used",
+ ): TalerAddIncomingCreationResult = conn { conn ->
+ val subject = IncomingTxMetadata(req.reserve_pub).toString()
+ val stmt = conn.prepareStatement("""
+ SELECT
+ out_creditor_not_found
+ ,out_creditor_not_exchange
+ ,out_debtor_not_found
+ ,out_same_account
+ ,out_both_exchanges
+ ,out_reserve_pub_reuse
+ ,out_debitor_balance_insufficient
+ ,out_tx_row_id
+ FROM
+ taler_add_incoming (
+ ?, ?,
+ (?,?)::taler_amount,
+ ?, ?, ?, ?, ?, ?
+ );
+ """)
+
+ stmt.setBytes(1, req.reserve_pub.raw)
+ stmt.setString(2, subject)
+ stmt.setLong(3, req.amount.value)
+ stmt.setInt(4, req.amount.frac)
+ stmt.setString(5, stripIbanPayto(req.debit_account) ?: throw badRequest("debit_account payto URI is invalid"))
+ stmt.setString(6, username)
+ stmt.setLong(7, timestamp.toDbMicros() ?: throw faultyTimestampByBank())
+ stmt.setString(8, acctSvcrRef)
+ stmt.setString(9, pmtInfId)
+ stmt.setString(10, endToEndId)
+
+ stmt.executeQuery().use {
+ when {
+ !it.next() ->
+ throw internalServerError("SQL function taler_add_incoming did not return anything.")
+ it.getBoolean("out_creditor_not_found") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.NO_CREDITOR)
+ it.getBoolean("out_creditor_not_exchange") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.NOT_EXCHANGE)
+ it.getBoolean("out_debtor_not_found") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.NO_DEBITOR)
+ it.getBoolean("out_same_account") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.SAME_ACCOUNT)
+ it.getBoolean("out_both_exchanges") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.BOTH_EXCHANGE)
+ it.getBoolean("out_debitor_balance_insufficient") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.BALANCE_INSUFFICIENT)
+ it.getBoolean("out_reserve_pub_reuse") ->
+ TalerAddIncomingCreationResult(TalerAddIncomingResult.RESERVE_PUB_REUSE)
+ else -> {
+ TalerAddIncomingCreationResult(
+ txResult = TalerAddIncomingResult.SUCCESS,
+ txRowId = it.getLong("out_tx_row_id")
+ )
+ }
+ }
+ }
+ }
+}
+
+/** Result status of customer account deletion */
+enum class CustomerDeletionResult {
+ SUCCESS,
+ CUSTOMER_NOT_FOUND,
+ BALANCE_NOT_ZERO
}
+/** Result status of bank transaction creation .*/
+enum class BankTransactionResult {
+ NO_CREDITOR,
+ NO_DEBTOR,
+ SAME_ACCOUNT,
+ BALANCE_INSUFFICIENT,
+ SUCCESS,
+}
+
+/** Result status of taler transfer transaction */
enum class TalerTransferResult {
NO_DEBITOR,
NOT_EXCHANGE,
NO_CREDITOR,
+ SAME_ACCOUNT,
+ BOTH_EXCHANGE,
REQUEST_UID_REUSE,
BALANCE_INSUFFICIENT,
SUCCESS
}
-private data class Notification(val rowId: Long)
+/** Result status of taler add incoming transaction */
+enum class TalerAddIncomingResult {
+ NO_DEBITOR,
+ NOT_EXCHANGE,
+ NO_CREDITOR,
+ SAME_ACCOUNT,
+ BOTH_EXCHANGE,
+ RESERVE_PUB_REUSE,
+ BALANCE_INSUFFICIENT,
+ SUCCESS
+}
+
+/**
+ * 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,
+
+ /**
+ * This state indicates that the withdrawal was already
+ * confirmed BUT Kotlin did not detect it and still invoked
+ * the SQL procedure to confirm the withdrawal. This is
+ * conflictual because only Kotlin is responsible to check
+ * for idempotency, and this state witnesses a failure in
+ * this regard.
+ */
+ CONFLICT
+}
private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
- private class CountedSharedFlow(val flow: MutableSharedFlow<Notification>, var count: Int)
+ private class CountedSharedFlow(val flow: MutableSharedFlow<Long>, var count: Int)
private val bankTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>()
private val outgoingTxFlows = ConcurrentHashMap<Long, CountedSharedFlow>()
@@ -1595,12 +1726,12 @@ private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
val creditRow = info[3];
bankTxFlows[debtorAccount]?.run {
- flow.emit(Notification(debitRow))
- flow.emit(Notification(creditRow))
+ flow.emit(debitRow)
+ flow.emit(creditRow)
}
bankTxFlows[creditorAccount]?.run {
- flow.emit(Notification(debitRow))
- flow.emit(Notification(creditRow))
+ flow.emit(debitRow)
+ flow.emit(creditRow)
}
} else {
val info = it.parameter.split(' ', limit = 2).map { it.toLong() }
@@ -1608,11 +1739,11 @@ private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
val row = info[1];
if (it.name == "outgoing_tx") {
outgoingTxFlows[account]?.run {
- flow.emit(Notification(row))
+ flow.emit(row)
}
} else {
incomingTxFlows[account]?.run {
- flow.emit(Notification(row))
+ flow.emit(row)
}
}
}
@@ -1626,7 +1757,7 @@ private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
}
}
- private suspend fun listen(map: ConcurrentHashMap<Long, CountedSharedFlow>, account: Long, lambda: suspend (Flow<Notification>) -> Unit) {
+ private suspend fun listen(map: ConcurrentHashMap<Long, CountedSharedFlow>, account: Long, lambda: suspend (Flow<Long>) -> Unit) {
// Register listener
val flow = map.compute(account) { _, v ->
val tmp = v ?: CountedSharedFlow(MutableSharedFlow(), 0);
@@ -1646,15 +1777,15 @@ private class NotificationWatcher(private val pgSource: PGSimpleDataSource) {
}
}
- suspend fun listenBank(account: Long, lambda: suspend (Flow<Notification>) -> Unit) {
+ suspend fun listenBank(account: Long, lambda: suspend (Flow<Long>) -> Unit) {
listen(bankTxFlows, account, lambda)
}
- suspend fun listenOutgoing(account: Long, lambda: suspend (Flow<Notification>) -> Unit) {
+ suspend fun listenOutgoing(account: Long, lambda: suspend (Flow<Long>) -> Unit) {
listen(outgoingTxFlows, account, lambda)
}
- suspend fun listenIncoming(account: Long, lambda: suspend (Flow<Notification>) -> Unit) {
+ suspend fun listenIncoming(account: Long, lambda: suspend (Flow<Long>) -> Unit) {
listen(incomingTxFlows, account, lambda)
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -426,46 +426,6 @@ fun ResourceName.canI(c: Customer, withAdmin: Boolean = true): Boolean {
fun ApplicationCall.getResourceName(param: String): ResourceName =
this.expectUriComponent(param)
-/**
- * This type communicates the result of deleting an account
- * from the database.
- */
-enum class CustomerDeletionResult {
- SUCCESS,
- CUSTOMER_NOT_FOUND,
- BALANCE_NOT_ZERO
-}
-
-/**
- * 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,
-
- /**
- * This state indicates that the withdrawal was already
- * confirmed BUT Kotlin did not detect it and still invoked
- * the SQL procedure to confirm the withdrawal. This is
- * conflictual because only Kotlin is responsible to check
- * for idempotency, and this state witnesses a failure in
- * this regard.
- */
- CONFLICT
-}
-
-/**
- * Communicates the result of creating a bank transaction in the database.
- */
-enum class BankTransactionResult {
- NO_CREDITOR,
- NO_DEBTOR,
- SUCCESS,
- CONFLICT // balance insufficient
-}
// GET /config response from the Taler Integration API.
@Serializable
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApiHandlers.kt
@@ -39,9 +39,9 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.nexus")
fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext) {
/** Authenticate and check access rights */
suspend fun ApplicationCall.authCheck(scope: TokenScope, withAdmin: Boolean): String {
- val authCustomer = authenticateBankRequest(db, scope) ?: throw unauthorized()
+ val authCustomer = authenticateBankRequest(db, scope) ?: throw unauthorized("Bad login")
val username = getResourceName("USERNAME")
- if (!username.canI(authCustomer, withAdmin)) throw forbidden()
+ if (!username.canI(authCustomer, withAdmin)) throw unauthorized("No right on $username account")
return username
}
@@ -73,20 +73,29 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
timestamp = Instant.now()
)
when (dbRes.txResult) {
- TalerTransferResult.NO_DEBITOR ->
- throw notFound(
- hint = "Customer $username not found",
- talerEc = TalerErrorCode.TALER_EC_END // FIXME: need EC.
- )
- TalerTransferResult.NOT_EXCHANGE ->
- throw forbidden("$username is not an exchange account.")
+ TalerTransferResult.NO_DEBITOR -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_END // FIXME: need EC.
+ )
+ TalerTransferResult.NOT_EXCHANGE -> throw conflict(
+ "$username is not an exchange account.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
TalerTransferResult.NO_CREDITOR -> throw notFound(
"Creditor account was not found",
TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
+ TalerTransferResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerTransferResult.BOTH_EXCHANGE -> throw conflict(
+ "Wire transfer attempted with credit and debit party being both exchange account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
TalerTransferResult.REQUEST_UID_REUSE -> throw conflict(
- hint = "request_uid used already",
- talerEc = TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
+ "request_uid used already",
+ TalerErrorCode.TALER_EC_BANK_TRANSFER_REQUEST_UID_REUSED
)
TalerTransferResult.BALANCE_INSUFFICIENT -> throw conflict(
"Insufficient balance for exchange",
@@ -106,10 +115,15 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
reduce: (List<T>, String) -> Any,
dbLambda: suspend Database.(HistoryParams, Long) -> List<T>
) {
- call.authCheck(TokenScope.readonly, true)
+ val username = call.authCheck(TokenScope.readonly, true)
val params = getHistoryParams(call.request.queryParameters)
val bankAccount = call.bankAccount()
- if (!bankAccount.isTalerExchange) throw forbidden("History is not related to a Taler exchange.")
+
+ if (!bankAccount.isTalerExchange)
+ throw conflict(
+ "$username is not an exchange account.",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
val items = db.dbLambda(params, bankAccount.id);
@@ -129,57 +143,54 @@ fun Routing.talerWireGatewayHandlers(db: Database, ctx: BankApplicationContext)
}
post("/accounts/{USERNAME}/taler-wire-gateway/admin/add-incoming") {
- call.authCheck(TokenScope.readwrite, false);
+ val username = call.authCheck(TokenScope.readwrite, false)
val req = call.receive<AddIncomingRequest>()
if (req.amount.currency != ctx.currency)
throw badRequest(
"Currency mismatch",
TalerErrorCode.TALER_EC_GENERIC_CURRENCY_MISMATCH
)
-
- val subject = IncomingTxMetadata(req.reserve_pub).toString()
-
- // TODO check conflict in transaction
- if (db.bankTransactionCheckExists(subject) != null)
- throw conflict(
- "Reserve pub. already used",
- TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+ val timestamp = Instant.now()
+ val dbRes = db.talerAddIncomingCreate(
+ req = req,
+ username = username,
+ timestamp = timestamp
+ )
+ when (dbRes.txResult) {
+ TalerAddIncomingResult.NO_CREDITOR -> throw notFound(
+ "Customer $username not found",
+ TalerErrorCode.TALER_EC_END // FIXME: need EC.
)
- val strippedIbanPayto: String = stripIbanPayto(req.debit_account) ?: throw badRequest("Invalid debit_account payto URI")
- val walletAccount = db.bankAccountGetFromInternalPayto(strippedIbanPayto)
- ?: throw notFound(
- "debit_account not found",
+ TalerAddIncomingResult.NOT_EXCHANGE -> throw conflict(
+ "$username is not an exchange account.",
TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
)
- val exchangeAccount = call.bankAccount()
- if (!exchangeAccount.isTalerExchange) throw forbidden("Expected taler exchange bank account.")
-
- val txTimestamp = Instant.now()
- val op = BankInternalTransaction(
- debtorAccountId = walletAccount.expectRowId(),
- amount = req.amount,
- creditorAccountId = exchangeAccount.id,
- transactionDate = txTimestamp,
- subject = subject
- )
- val res = db.bankTransactionCreate(op)
- /**
- * Other possible errors are highly unlikely, because of the
- * previous checks on the existence of the involved bank accounts.
- */
- if (res == BankTransactionResult.CONFLICT)
- throw conflict(
- "Insufficient balance",
+ TalerAddIncomingResult.NO_DEBITOR -> throw notFound(
+ "Debitor account was not found",
+ TalerErrorCode.TALER_EC_BANK_UNKNOWN_ACCOUNT
+ )
+ TalerAddIncomingResult.SAME_ACCOUNT -> throw conflict(
+ "Wire transfer attempted with credit and debit party being the same bank account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerAddIncomingResult.BOTH_EXCHANGE -> throw conflict(
+ "Wire transfer attempted with credit and debit party being both exchange account",
+ TalerErrorCode.TALER_EC_BANK_SAME_ACCOUNT
+ )
+ TalerAddIncomingResult.RESERVE_PUB_REUSE -> throw conflict(
+ "reserve_pub used already",
+ TalerErrorCode.TALER_EC_BANK_DUPLICATE_RESERVE_PUB_SUBJECT
+ )
+ TalerAddIncomingResult.BALANCE_INSUFFICIENT -> throw conflict(
+ "Insufficient balance for debitor",
TalerErrorCode.TALER_EC_BANK_UNALLOWED_DEBIT
)
- val rowId = db.bankTransactionCheckExists(req.reserve_pub.encoded())
- ?: throw internalServerError("Could not find the just inserted bank transaction")
- call.respond(
- AddIncomingResponse(
- row_id = rowId,
- timestamp = TalerProtocolTimestamp(txTimestamp)
+ TalerAddIncomingResult.SUCCESS -> call.respond(
+ AddIncomingResponse(
+ timestamp = TalerProtocolTimestamp(timestamp),
+ row_id = dbRes.txRowId!!
+ )
)
- )
- return@post
+ }
}
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/TalerApiTest.kt b/bank/src/test/kotlin/TalerApiTest.kt
@@ -4,8 +4,7 @@ import io.ktor.client.statement.*
import io.ktor.client.HttpClient
import io.ktor.http.*
import io.ktor.server.testing.*
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.*
import kotlinx.coroutines.*
import net.taler.wallet.crypto.Base32Crockford
import org.junit.Test
@@ -96,8 +95,8 @@ class TalerApiTest {
}
// Test endpoint is correctly authenticated
- suspend fun authRoutine(client: HttpClient, path: String, method: HttpMethod = HttpMethod.Post) {
- // No body because authentication must happen before parsing the body
+ suspend fun authRoutine(client: HttpClient, path: String, body: JsonObject? = null, method: HttpMethod = HttpMethod.Post) {
+ // No body when authentication must happen before parsing the body
// Unknown account
client.request(path) {
@@ -115,7 +114,14 @@ class TalerApiTest {
client.request(path) {
this.method = method
basicAuth("bar", "secret")
- }.assertStatus(HttpStatusCode.Forbidden)
+ }.assertStatus(HttpStatusCode.Unauthorized)
+
+ // Not exchange account
+ client.request(path) {
+ this.method = method
+ if (body != null) jsonBody(body)
+ basicAuth("foo", "pw")
+ }.assertStatus(HttpStatusCode.Conflict)
}
// Testing the POST /transfer call from the TWG API.
@@ -127,23 +133,15 @@ class TalerApiTest {
corebankWebApp(db, ctx)
}
- // TODO what to do when creditor and debtor are both exchanges
-
- authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer")
-
val valid_req = json {
"request_uid" to randHashCode()
"amount" to "KUDOS:55"
"exchange_base_url" to "http://exchange.example.com/"
"wtid" to randShortHashCode()
- "credit_account" to "${stripIbanPayto(bankAccountFoo.internalPaytoUri)}"
+ "credit_account" to stripIbanPayto(bankAccountFoo.internalPaytoUri)
};
- // Checking exchange account constraint.
- client.post("/accounts/foo/taler-wire-gateway/transfer") {
- basicAuth("foo", "pw")
- jsonBody(valid_req)
- }.assertStatus(HttpStatusCode.Forbidden)
+ authRoutine(client, "/accounts/foo/taler-wire-gateway/transfer", valid_req)
// Checking exchange debt constraint.
client.post("/accounts/bar/taler-wire-gateway/transfer") {
@@ -178,19 +176,17 @@ class TalerApiTest {
)
}.assertStatus(HttpStatusCode.Conflict)
- // Triggering currency mismatch
+ // Currency mismatch
client.post("/accounts/bar/taler-wire-gateway/transfer") {
basicAuth("bar", "secret")
jsonBody(
- json(valid_req) {
- "request_uid" to randHashCode()
- "wtid" to randShortHashCode()
+ json(valid_req) {
"amount" to "EUR:33"
}
)
}.assertBadRequest()
- // Unknown account currency mismatch
+ // Unknown account
client.post("/accounts/bar/taler-wire-gateway/transfer") {
basicAuth("bar", "secret")
jsonBody(
@@ -286,7 +282,7 @@ class TalerApiTest {
corebankWebApp(db, ctx)
}
- authRoutine(client, "/accounts/foo/taler-wire-gateway/history/incoming", HttpMethod.Get)
+ authRoutine(client, "/accounts/foo/taler-wire-gateway/history/incoming?delta=7", method = HttpMethod.Get)
// Check error when no transactions
client.get("/accounts/bar/taler-wire-gateway/history/incoming?delta=7") {
@@ -425,7 +421,7 @@ class TalerApiTest {
corebankWebApp(db, ctx)
}
- authRoutine(client, "/accounts/foo/taler-wire-gateway/history/outgoing", HttpMethod.Get)
+ authRoutine(client, "/accounts/foo/taler-wire-gateway/history/outgoing?delta=7", method = HttpMethod.Get)
// Check error when no transactions
client.get("/accounts/bar/taler-wire-gateway/history/outgoing?delta=7") {
@@ -524,34 +520,63 @@ class TalerApiTest {
// Testing the /admin/add-incoming call from the TWG API.
@Test
fun addIncoming() = commonSetup { db, ctx ->
- // Give Bar reasonable debt allowance:
- assert(db.bankAccountSetMaxDebt(
- 2L,
- TalerAmount(1000, 0, "KUDOS")
- ))
testApplication {
application {
corebankWebApp(db, ctx)
}
- authRoutine(client, "/accounts/foo/taler-wire-gateway/admin/add-incoming")
-
val valid_req = json {
"amount" to "KUDOS:44"
"reserve_pub" to randEddsaPublicKey()
- "debit_account" to "${"payto://iban/BAR-IBAN-ABC"}"
+ "debit_account" to bankAccountFoo.internalPaytoUri
};
-
- client.post("/accounts/foo/taler-wire-gateway/admin/add-incoming") {
- basicAuth("foo", "pw")
- jsonBody(valid_req, deflate = true)
- }.assertStatus(HttpStatusCode.Forbidden)
+
+ authRoutine(client, "/accounts/foo/taler-wire-gateway/admin/add-incoming", valid_req)
+
+ // Checking exchange debt constraint.
+ client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
+ basicAuth("bar", "secret")
+ jsonBody(valid_req)
+ }.assertStatus(HttpStatusCode.Conflict)
+
+ // Giving debt allowance and checking the OK case.
+ assert(db.bankAccountSetMaxDebt(
+ 1L,
+ TalerAmount(1000, 0, "KUDOS")
+ ))
client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
basicAuth("bar", "secret")
jsonBody(valid_req, deflate = true)
}.assertOk()
+ // Trigger conflict due to reused reserve_pub
+ client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
+ basicAuth("bar", "secret")
+ jsonBody(valid_req)
+ }.assertStatus(HttpStatusCode.Conflict)
+
+ // Currency mismatch
+ client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
+ basicAuth("bar", "secret")
+ jsonBody(
+ json(valid_req) {
+ "amount" to "EUR:33"
+ }
+ )
+ }.assertBadRequest()
+
+ // Unknown account
+ client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
+ basicAuth("bar", "secret")
+ jsonBody(
+ json(valid_req) {
+ "reserve_pub" to randEddsaPublicKey()
+ "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+ }
+ )
+ }.assertStatus(HttpStatusCode.NotFound)
+
// Bad BASE32 reserve_pub
client.post("/accounts/bar/taler-wire-gateway/admin/add-incoming") {
basicAuth("bar", "secret")
diff --git a/database-versioning/procedures.sql b/database-versioning/procedures.sql
@@ -206,11 +206,15 @@ CREATE OR REPLACE FUNCTION taler_transfer(
IN in_account_servicer_reference TEXT,
IN in_payment_information_id TEXT,
IN in_end_to_end_id TEXT,
+ -- Error status
+ OUT out_debtor_not_found BOOLEAN,
+ OUT out_debtor_not_exchange BOOLEAN,
+ OUT out_creditor_not_found BOOLEAN,
+ OUT out_same_account BOOLEAN,
+ OUT out_both_exchanges BOOLEAN,
OUT out_request_uid_reuse BOOLEAN,
OUT out_exchange_balance_insufficient BOOLEAN,
- OUT out_nx_debitor BOOLEAN,
- OUT out_nx_exchange BOOLEAN,
- OUT out_nx_creditor BOOLEAN,
+ -- Success return
OUT out_tx_row_id BIGINT,
OUT out_timestamp BIGINT
)
@@ -237,37 +241,38 @@ END IF;
-- Find exchange bank account id
SELECT
bank_account_id, NOT is_taler_exchange
- INTO exchange_bank_account_id, out_nx_exchange
+ INTO exchange_bank_account_id, out_debtor_not_exchange
FROM bank_accounts
JOIN customers
ON customer_id=owning_customer_id
WHERE login = in_username;
IF NOT FOUND THEN
- out_nx_debitor=TRUE;
+ out_debtor_not_found=TRUE;
RETURN;
-ELSIF out_nx_exchange THEN
+ELSIF out_debtor_not_exchange THEN
RETURN;
END IF;
--- Find receiver bank account id
--- TODO handle bounce when receiver is exchange ?
--- TODO handle transfer to self ?
+-- Find receiver bank account id
SELECT
- bank_account_id
- INTO receiver_bank_account_id
+ bank_account_id, is_taler_exchange
+ INTO receiver_bank_account_id, out_both_exchanges
FROM bank_accounts
WHERE internal_payto_uri = in_credit_account_payto;
-IF NOT FOUND
-THEN
- out_nx_creditor=TRUE;
+IF NOT FOUND THEN
+ out_creditor_not_found=TRUE;
+ RETURN;
+ELSIF out_both_exchanges THEN
RETURN;
END IF;
-- Perform bank transfer
SELECT
out_balance_insufficient,
- out_debit_row_id
+ out_debit_row_id,
+ transfer.out_same_account
INTO
out_exchange_balance_insufficient,
- out_tx_row_id
+ out_tx_row_id,
+ out_same_account
FROM bank_wire_transfer(
receiver_bank_account_id,
exchange_bank_account_id,
@@ -277,7 +282,7 @@ SELECT
in_account_servicer_reference,
in_payment_information_id,
in_end_to_end_id
- );
+ ) as transfer;
IF out_exchange_balance_insufficient THEN
RETURN;
END IF;
@@ -310,10 +315,124 @@ COMMENT ON FUNCTION taler_transfer(
text,
text,
text
- )
+ )-- TODO new comment
+ IS 'function that (1) inserts the TWG requests'
+ 'details into the database and (2) performs '
+ 'the actual bank transaction to pay the merchant';
+
+
+CREATE OR REPLACE FUNCTION taler_add_incoming(
+ IN in_reserve_pub BYTEA,
+ IN in_subject TEXT,
+ IN in_amount taler_amount,
+ IN in_debit_account_payto TEXT,
+ IN in_username TEXT,
+ IN in_timestamp BIGINT,
+ IN in_account_servicer_reference TEXT,
+ IN in_payment_information_id TEXT,
+ IN in_end_to_end_id TEXT,
+ -- Error status
+ OUT out_creditor_not_found BOOLEAN,
+ OUT out_creditor_not_exchange BOOLEAN,
+ OUT out_debtor_not_found BOOLEAN,
+ OUT out_same_account BOOLEAN,
+ OUT out_both_exchanges BOOLEAN,
+ OUT out_reserve_pub_reuse BOOLEAN,
+ OUT out_debitor_balance_insufficient BOOLEAN,
+ -- Success return
+ OUT out_tx_row_id BIGINT
+)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+exchange_bank_account_id BIGINT;
+sender_bank_account_id BIGINT;
+BEGIN
+-- Check for idempotence and conflict
+SELECT true
+ FROM taler_exchange_incoming
+ JOIN bank_account_transactions AS txs
+ ON bank_transaction=txs.bank_transaction_id
+ WHERE reserve_pub = in_reserve_pub
+ INTO out_reserve_pub_reuse;
+IF out_reserve_pub_reuse THEN
+ RETURN;
+END IF;
+-- Find exchange bank account id
+SELECT
+ bank_account_id, NOT is_taler_exchange
+ INTO exchange_bank_account_id, out_creditor_not_exchange
+ FROM bank_accounts
+ JOIN customers
+ ON customer_id=owning_customer_id
+ WHERE login = in_username;
+IF NOT FOUND THEN
+ out_creditor_not_found=TRUE;
+ RETURN;
+ELSIF out_creditor_not_exchange THEN
+ RETURN;
+END IF;
+-- Find sender bank account id
+SELECT
+ bank_account_id, is_taler_exchange
+ INTO sender_bank_account_id, out_both_exchanges
+ FROM bank_accounts
+ WHERE internal_payto_uri = in_debit_account_payto;
+IF NOT FOUND THEN
+ out_debtor_not_found=TRUE;
+ RETURN;
+ELSIF out_both_exchanges THEN
+ RETURN;
+END IF;
+-- Perform bank transfer
+SELECT
+ out_balance_insufficient,
+ out_debit_row_id,
+ transfer.out_same_account
+ INTO
+ out_debitor_balance_insufficient,
+ out_tx_row_id,
+ out_same_account
+ FROM bank_wire_transfer(
+ exchange_bank_account_id,
+ sender_bank_account_id,
+ in_subject,
+ in_amount,
+ in_timestamp,
+ in_account_servicer_reference,
+ in_payment_information_id,
+ in_end_to_end_id
+ ) as transfer;
+IF out_debitor_balance_insufficient THEN
+ RETURN;
+END IF;
+-- Register incoming transaction
+INSERT
+ INTO taler_exchange_incoming (
+ reserve_pub,
+ bank_transaction
+) VALUES (
+ in_reserve_pub,
+ out_tx_row_id
+);
+-- notify new transaction
+PERFORM pg_notify('incoming_tx', exchange_bank_account_id || ' ' || out_tx_row_id);
+END $$;
+COMMENT ON FUNCTION taler_add_incoming(
+ bytea,
+ text,
+ taler_amount,
+ text,
+ text,
+ bigint,
+ text,
+ text,
+ text
+ ) -- TODO new comment
IS 'function that (1) inserts the TWG requests'
'details into the database and (2) performs '
'the actual bank transaction to pay the merchant';
+
CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
IN in_withdrawal_uuid uuid,
@@ -321,12 +440,9 @@ CREATE OR REPLACE FUNCTION confirm_taler_withdrawal(
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,
+ OUT out_no_op BOOLEAN,
+ OUT out_balance_insufficient BOOLEAN,
+ OUT out_exchange_not_found BOOLEAN,
OUT out_already_confirmed_conflict BOOLEAN
)
LANGUAGE plpgsql
@@ -356,10 +472,10 @@ SELECT -- Really no-star policy and instead DECLARE almost one var per column?
WHERE withdrawal_uuid=in_withdrawal_uuid;
IF NOT FOUND
THEN
- out_nx_op=TRUE;
+ out_no_op=TRUE;
RETURN;
END IF;
-out_nx_op=FALSE;
+out_no_op=FALSE;
IF (confirmation_done_local)
THEN
out_already_confirmed_conflict=TRUE
@@ -378,14 +494,14 @@ SELECT
WHERE internal_payto_uri = selected_exchange_payto_local;
IF NOT FOUND
THEN
- out_nx_exchange=TRUE;
+ out_exchange_not_found=TRUE;
RETURN;
END IF;
-out_nx_exchange=FALSE;
+out_exchange_not_found=FALSE;
SELECT -- not checking for accounts existence, as it was done above.
- out_balance_insufficient
+ transfer.out_balance_insufficient
INTO
- maybe_balance_insufficient
+ out_balance_insufficient
FROM bank_wire_transfer(
exchange_bank_account_id,
wallet_bank_account_local,
@@ -395,12 +511,12 @@ FROM bank_wire_transfer(
in_acct_svcr_ref,
in_pmt_inf_id,
in_end_to_end_id
-);
+) as transfer;
IF (maybe_balance_insufficient)
THEN
- out_insufficient_funds=TRUE;
+ out_balance_insufficient=TRUE;
END IF;
-out_insufficient_funds=FALSE;
+out_balance_insufficient=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.';
@@ -414,9 +530,12 @@ CREATE OR REPLACE FUNCTION bank_wire_transfer(
IN in_account_servicer_reference TEXT,
IN in_payment_information_id TEXT,
IN in_end_to_end_id TEXT,
- OUT out_nx_creditor BOOLEAN,
- OUT out_nx_debtor BOOLEAN,
+ -- Error status
+ OUT out_same_account BOOLEAN,
+ OUT out_debtor_not_found BOOLEAN,
+ OUT out_creditor_not_found BOOLEAN,
OUT out_balance_insufficient BOOLEAN,
+ -- Success return
OUT out_credit_row_id BIGINT,
OUT out_debit_row_id BIGINT,
OUT out_creditor_is_exchange BOOLEAN,
@@ -446,6 +565,13 @@ potential_balance_ok BOOLEAN;
new_debit_row_id BIGINT;
new_credit_row_id BIGINT;
BEGIN
+
+IF in_creditor_account_id=in_debtor_account_id THEN
+ out_same_account=TRUE;
+ RETURN;
+END IF;
+out_same_account=FALSE;
+
-- check debtor exists.
SELECT
has_debt,
@@ -462,12 +588,11 @@ SELECT
FROM bank_accounts
JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
WHERE bank_account_id=in_debtor_account_id;
-IF NOT FOUND
-THEN
- out_nx_debtor=TRUE;
+IF NOT FOUND THEN
+ out_debtor_not_found=TRUE;
RETURN;
END IF;
-out_nx_debtor=FALSE;
+out_debtor_not_found=FALSE;
-- check creditor exists. Future versions may skip this
-- due to creditors being hosted at other banks.
SELECT
@@ -483,16 +608,16 @@ SELECT
FROM bank_accounts
JOIN customers ON (bank_accounts.owning_customer_id = customers.customer_id)
WHERE bank_account_id=in_creditor_account_id;
-IF NOT FOUND
-THEN
- out_nx_creditor=TRUE;
+IF NOT FOUND THEN
+ out_creditor_not_found=TRUE;
RETURN;
END IF;
-out_nx_creditor=FALSE;
+out_creditor_not_found=FALSE;
+
-- DEBTOR SIDE
-- check debtor has enough funds.
-IF (debtor_has_debt)
-THEN -- debt case: simply checking against the max debt allowed.
+IF debtor_has_debt THEN
+ -- debt case: simply checking against the max debt allowed.
CALL amount_add(debtor_balance,
in_amount,
potential_balance);
@@ -500,8 +625,7 @@ THEN -- debt case: simply checking against the max debt allowed.
INTO potential_balance_check
FROM amount_left_minus_right(debtor_max_debt,
potential_balance);
- IF (NOT potential_balance_check)
- THEN
+ IF NOT potential_balance_check THEN
out_balance_insufficient=TRUE;
RETURN;
END IF;
@@ -517,8 +641,7 @@ ELSE -- not a debt account
potential_balance.frac
FROM amount_left_minus_right(debtor_balance,
in_amount);
- IF (potential_balance_ok) -- debtor has enough funds in the (positive) balance.
- THEN
+ IF potential_balance_ok THEN -- debtor has enough funds in the (positive) balance.
new_debtor_balance=potential_balance;
will_debtor_have_debt=FALSE;
ELSE -- debtor will switch to debt: determine their new negative balance.
@@ -533,8 +656,7 @@ ELSE -- not a debt account
INTO potential_balance_check
FROM amount_left_minus_right(debtor_max_debt,
new_debtor_balance);
- IF (NOT potential_balance_check)
- THEN
+ IF NOT potential_balance_check THEN
out_balance_insufficient=TRUE;
RETURN;
END IF;
@@ -545,8 +667,7 @@ END IF;
-- Here we figure out whether the creditor would switch
-- from debit to a credit situation, and adjust the balance
-- accordingly.
-IF (NOT creditor_has_debt) -- easy case.
-THEN
+IF NOT creditor_has_debt THEN -- easy case.
CALL amount_add(creditor_balance, in_amount, new_creditor_balance);
will_creditor_have_debt=FALSE;
ELSE -- creditor had debit but MIGHT switch to credit.
@@ -558,9 +679,8 @@ ELSE -- creditor had debit but MIGHT switch to credit.
amount_at_least_debit
FROM amount_left_minus_right(in_amount,
creditor_balance);
- IF (amount_at_least_debit)
- -- the amount is at least as big as the debit, can switch to credit then.
- THEN
+ IF amount_at_least_debit THEN
+ -- the amount is at least as big as the debit, can switch to credit then.
will_creditor_have_debt=FALSE;
-- compute new balance.
ELSE