diff options
Diffstat (limited to 'bank')
7 files changed, 81 insertions, 11 deletions
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt index 48fc3992..7a192308 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/Constants.kt @@ -40,7 +40,7 @@ const val IBAN_ALLOCATION_RETRY_COUNTER: Int = 5 const val MAX_BODY_LENGTH: Long = 4 * 1024 // 4kB // API version -const val COREBANK_API_VERSION: String = "4:3:0" +const val COREBANK_API_VERSION: String = "4:4:0" const val CONVERSION_API_VERSION: String = "0:0:0" const val INTEGRATION_API_VERSION: String = "2:0:2" const val WIRE_GATEWAY_API_VERSION: String = "0:2:0" diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt index 0284266b..ce459baa 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -380,7 +380,8 @@ data class AccountData( @Serializable data class TransactionCreateRequest( val payto_uri: Payto, - val amount: TalerAmount? + val amount: TalerAmount?, + val request_uid: ShortHashCode? ) @Serializable diff --git a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt index dd6aab4b..a3bb8265 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt @@ -459,6 +459,7 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { subject = subject, amount = amount, timestamp = Instant.now(), + requestUid = req.request_uid, is2fa = challenge != null ) when (res) { @@ -479,6 +480,10 @@ private fun Routing.coreBankTransactionsApi(db: Database, ctx: BankConfig) { "Insufficient funds", TalerErrorCode.BANK_UNALLOWED_DEBIT ) + BankTransactionResult.RequestUidReuse -> throw conflict( + "request_uid used already", + TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED + ) is BankTransactionResult.Success -> call.respond(TransactionCreateResponse(res.id)) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt index 13bc1e09..f78947e7 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt @@ -157,7 +157,7 @@ class AccountDAO(private val db: Database) { if (bonus.value != 0L || bonus.frac != 0) { conn.prepareStatement(""" SELECT out_balance_insufficient - FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,true) + FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,true,NULL) """).run { setString(1, internalPayto.canonical) setLong(2, bonus.value) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt index 9b03cd2d..81fd558f 100644 --- a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt +++ b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt @@ -38,6 +38,7 @@ class TransactionDAO(private val db: Database) { data object BothPartySame: BankTransactionResult data object BalanceInsufficient: BankTransactionResult data object TanRequired: BankTransactionResult + data object RequestUidReuse: BankTransactionResult } /** Create a new transaction */ @@ -47,7 +48,8 @@ class TransactionDAO(private val db: Database) { subject: String, amount: TalerAmount, timestamp: Instant, - is2fa: Boolean + is2fa: Boolean, + requestUid: ShortHashCode?, ): BankTransactionResult = db.serializable { conn -> val now = timestamp.toDbMicros() ?: throw faultyTimestampByBank() conn.transaction { @@ -57,6 +59,7 @@ class TransactionDAO(private val db: Database) { ,out_debtor_not_found ,out_same_account ,out_balance_insufficient + ,out_request_uid_reuse ,out_tan_required ,out_credit_bank_account_id ,out_debit_bank_account_id @@ -65,7 +68,8 @@ class TransactionDAO(private val db: Database) { ,out_creditor_is_exchange ,out_debtor_is_exchange ,out_creditor_admin - FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?) + ,out_idempotent + FROM bank_transaction(?,?,?,(?,?)::taler_amount,?,?,?) """ ) stmt.setString(1, creditAccountPayto.canonical) @@ -75,6 +79,7 @@ class TransactionDAO(private val db: Database) { stmt.setInt(5, amount.frac) stmt.setLong(6, now) stmt.setBoolean(7, is2fa) + stmt.setBytes(8, requestUid?.raw) stmt.executeQuery().use { when { !it.next() -> throw internalServerError("Bank transaction didn't properly return") @@ -83,6 +88,8 @@ class TransactionDAO(private val db: Database) { it.getBoolean("out_same_account") -> BankTransactionResult.BothPartySame it.getBoolean("out_balance_insufficient") -> BankTransactionResult.BalanceInsufficient it.getBoolean("out_creditor_admin") -> BankTransactionResult.AdminCreditor + it.getBoolean("out_request_uid_reuse") -> BankTransactionResult.RequestUidReuse + it.getBoolean("out_idempotent") -> BankTransactionResult.Success(it.getLong("out_debit_row_id")) it.getBoolean("out_tan_required") -> BankTransactionResult.TanRequired else -> { val creditAccountId = it.getLong("out_credit_bank_account_id") diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt index 94716208..ccd64400 100644 --- a/bank/src/test/kotlin/AmountTest.kt +++ b/bank/src/test/kotlin/AmountTest.kt @@ -53,7 +53,8 @@ class AmountTest { subject = "test", amount = due, timestamp = Instant.now(), - is2fa = false + is2fa = false, + requestUid = null ) val txBool = when (txRes) { BankTransactionResult.BalanceInsufficient -> false diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt index f5f668f5..b1b89cc9 100644 --- a/bank/src/test/kotlin/CoreBankApiTest.kt +++ b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -174,7 +174,9 @@ class CoreBankAccountsApiTest { // Check idempotency client.post("/accounts") { json(req) - }.assertOk() + }.assertOkJson<RegisterAccountResponse> { + assertEquals(payto, it.internal_payto_uri) + } // Check idempotency with payto client.post("/accounts") { json(req) { @@ -208,8 +210,9 @@ class CoreBankAccountsApiTest { // Testing idempotency client.post("/accounts") { json(req) - }.assertOk() - + }.assertOkJson<RegisterAccountResponse> { + assertEquals(payto.full("Jane"), it.internal_payto_uri) + } // Check admin only debit_threshold obj { "username" to "bat" @@ -851,6 +854,28 @@ class CoreBankTransactionsApiTest { assertEquals(TalerAmount("KUDOS:0.3"), tx.amount) } } + + // Check idempotency + ShortHashCode.rand().let { requestUid -> + val id = client.postA("/accounts/merchant/transactions") { + json(valid_req) { + "request_uid" to requestUid + } + }.assertOkJson<TransactionCreateResponse>().row_id + client.postA("/accounts/merchant/transactions") { + json(valid_req) { + "request_uid" to requestUid + } + }.assertOkJson<TransactionCreateResponse> { + assertEquals(id, it.row_id) + } + client.postA("/accounts/merchant/transactions") { + json(valid_req) { + "request_uid" to requestUid + "amount" to "KUDOS:42" + } + }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) + } // Check amount in payto_uri client.postA("/accounts/merchant/transactions") { @@ -975,6 +1000,31 @@ class CoreBankTransactionsApiTest { assertBalance("merchant", "+KUDOS:2") assertBalance("customer", "+KUDOS:1") } + + // Check 2fa idempotency + val req = obj { + "payto_uri" to "$customerPayto?message=tan+check&amount=KUDOS:1" + "request_uid" to ShortHashCode.rand() + } + val id = client.postA("/accounts/merchant/transactions") { + json(req) + }.assertChallenge { _,_-> + assertBalance("merchant", "+KUDOS:2") + assertBalance("customer", "+KUDOS:1") + }.assertOkJson <TransactionCreateResponse> { + assertBalance("merchant", "+KUDOS:1") + assertBalance("customer", "+KUDOS:2") + }.row_id + client.postA("/accounts/merchant/transactions") { + json(req) + }.assertOkJson<TransactionCreateResponse> { + assertEquals(id, it.row_id) + } + client.postA("/accounts/merchant/transactions") { + json(req) { + "payto_uri" to "$customerPayto?message=tan+chec2k&amount=KUDOS:1" + } + }.assertConflict(TalerErrorCode.BANK_TRANSFER_REQUEST_UID_REUSED) } } @@ -1122,7 +1172,6 @@ class CoreBankWithdrawalApiTest { } } - class CoreBankCashoutApiTest { // POST /accounts/{USERNAME}/cashouts @Test @@ -1143,9 +1192,16 @@ class CoreBankCashoutApiTest { fillCashoutInfo("customer") // Check OK + val id = client.postA("/accounts/customer/cashouts") { + json(req) + }.assertOkJson<CashoutResponse>().cashout_id + + // Check idempotent client.postA("/accounts/customer/cashouts") { json(req) - }.assertOkJson<CashoutResponse>() + }.assertOkJson<CashoutResponse> { + assertEquals(id, it.cashout_id) + } // Trigger conflict due to reused request_uid client.postA("/accounts/customer/cashouts") { |