summaryrefslogtreecommitdiff
path: root/bank
diff options
context:
space:
mode:
Diffstat (limited to 'bank')
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Constants.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt3
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/api/CoreBankApi.kt5
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt11
-rw-r--r--bank/src/test/kotlin/AmountTest.kt3
-rw-r--r--bank/src/test/kotlin/CoreBankApiTest.kt66
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") {