commit 7ffa7a9f21f20f854b15c44c81b9db9eac93623e
parent cc35923acc45c1fe0acf4fa277ec490b43a27094
Author: Antoine A <>
Date: Tue, 14 Nov 2023 17:12:17 +0000
Track used iban and improve iban verification
Diffstat:
11 files changed, 141 insertions(+), 104 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -29,6 +29,7 @@ import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.concurrent.TimeUnit
import java.util.*
+import java.math.BigInteger
import java.net.*
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.*
@@ -43,9 +44,7 @@ import org.slf4j.event.Level
private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.TalerCommon")
const val MAX_SAFE_INTEGER = 9007199254740991L; // 2^53 - 1
-/**
- * 32-byte Crockford's Base32 encoded data.
- */
+/** 32-byte Crockford's Base32 encoded data */
@Serializable(with = Base32Crockford32B.Serializer::class)
class Base32Crockford32B {
private var encoded: String? = null
@@ -98,9 +97,7 @@ class Base32Crockford32B {
}
}
-/**
- * 64-byte Crockford's Base32 encoded data.
- */
+/** 64-byte Crockford's Base32 encoded data */
@Serializable(with = Base32Crockford64B.Serializer::class)
class Base32Crockford64B {
private var encoded: String? = null
@@ -153,9 +150,9 @@ class Base32Crockford64B {
}
}
-/** 32-byte hash code. */
+/** 32-byte hash code */
typealias ShortHashCode = Base32Crockford32B;
-/** 64-byte hash code. */
+/** 64-byte hash code */
typealias HashCode = Base32Crockford64B;
/**
* EdDSA and ECDHE public keys always point on Curve25519
@@ -164,9 +161,7 @@ typealias HashCode = Base32Crockford64B;
*/
typealias EddsaPublicKey = Base32Crockford32B;
-/**
- * Timestamp containing the number of seconds since epoch.
- */
+/** Timestamp containing the number of seconds since epoch */
@Serializable
data class TalerProtocolTimestamp(
@Serializable(with = TalerProtocolTimestamp.Serializer::class)
@@ -234,18 +229,31 @@ class TalerAmount {
this.currency = currency
}
constructor(encoded: String) {
- fun badAmount(hint: String): Exception =
- badRequest(hint, TalerErrorCode.BANK_BAD_FORMAT_AMOUNT)
-
- val match = PATTERN.matchEntire(encoded) ?: throw badAmount("Invalid amount format");
+ val match = PATTERN.matchEntire(encoded) ?: throw badRequest(
+ "Invalid amount format",
+ TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+ );
val (currency, value, frac) = match.destructured
this.currency = currency
- this.value = value.toLongOrNull() ?: throw badAmount("Invalid value")
- if (this.value > MAX_VALUE) throw badAmount("Value specified in amount is too large")
+ this.value = value.toLongOrNull() ?: throw badRequest(
+ "Invalid value",
+ TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+ )
+ if (this.value > MAX_VALUE) throw badRequest(
+ "Value specified in amount is too large",
+ TalerErrorCode.BANK_NUMBER_TOO_BIG
+ )
this.frac = if (frac.isEmpty()) {
0
} else {
- var tmp = frac.toIntOrNull() ?: throw badAmount("Invalid fractional value")
+ var tmp = frac.toIntOrNull() ?: throw badRequest(
+ "Invalid fractional value",
+ TalerErrorCode.BANK_BAD_FORMAT_AMOUNT
+ )
+ if (tmp > FRACTION_BASE) throw badRequest(
+ "Fractional calue specified in amount is too large",
+ TalerErrorCode.BANK_NUMBER_TOO_BIG
+ )
repeat(8 - frac.length) {
tmp *= 10
}
@@ -295,17 +303,14 @@ class DecimalNumber {
val frac: Int
constructor(encoded: String) {
- fun badAmount(hint: String): Exception =
- badRequest(hint, TalerErrorCode.BANK_BAD_FORMAT_AMOUNT)
-
- val match = PATTERN.matchEntire(encoded) ?: throw badAmount("Invalid decimal number format");
+ val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format");
val (value, frac) = match.destructured
- this.value = value.toLongOrNull() ?: throw badAmount("Invalid value")
- if (this.value > TalerAmount.MAX_VALUE) throw badAmount("Value specified in decimal number is too large")
+ this.value = value.toLongOrNull() ?: throw badRequest("Invalid value")
+ if (this.value > TalerAmount.MAX_VALUE) throw badRequest("Value specified in decimal number is too large")
this.frac = if (frac.isEmpty()) {
0
} else {
- var tmp = frac.toIntOrNull() ?: throw badAmount("Invalid fractional value")
+ var tmp = frac.toIntOrNull() ?: throw badRequest("Invalid fractional value")
repeat(8 - frac.length) {
tmp *= 10
}
@@ -422,6 +427,7 @@ sealed class PaytoUri {
class IbanPayTo: PaytoUri {
val parsed: URI
val canonical: String
+ val iban: String
override val amount: TalerAmount?
override val message: String?
override val receiverName: String?
@@ -433,8 +439,9 @@ class IbanPayTo: PaytoUri {
val splitPath = parsed.path.split("/").filter { it.isNotEmpty() }
require(splitPath.size < 3 && splitPath.isNotEmpty()) { "too many path segments" }
- val iban = (if (splitPath.size == 1) splitPath[0] else splitPath[1]).replace("-", "").uppercase()
- // TODO normalize && check IBAN ?
+ val rawIban = if (splitPath.size == 1) splitPath[0] else splitPath[1]
+ iban = rawIban.uppercase().replace(SEPARATOR, "")
+ checkIban(iban)
canonical = "payto://iban/$iban"
val params = (parsed.query ?: "").parseUrlEncodedParameters();
@@ -443,6 +450,8 @@ class IbanPayTo: PaytoUri {
receiverName = params["receiver-name"]
}
+ override fun toString(): String = canonical
+
internal object Serializer : KSerializer<IbanPayTo> {
override val descriptor: SerialDescriptor =
PrimitiveSerialDescriptor("IbanPayTo", PrimitiveKind.STRING)
@@ -455,4 +464,22 @@ class IbanPayTo: PaytoUri {
return IbanPayTo(decoder.decodeString())
}
}
+
+ companion object {
+ private val SEPARATOR = Regex("[\\ \\-]");
+
+ fun checkIban(iban: String) {
+ val builder = StringBuilder(iban.length + iban.asSequence().map { if (it.isDigit()) 1 else 2 }.sum())
+ (iban.subSequence(4, iban.length).asSequence() + iban.subSequence(0, 4).asSequence()).forEach {
+ if (it.isDigit()) {
+ builder.append(it)
+ } else {
+ builder.append((it.code - 'A'.code) + 10)
+ }
+ }
+ val str = builder.toString()
+ val mod = str.toBigInteger().mod(97.toBigInteger()).toInt();
+ if (mod != 1) throw badRequest("Iban malformed, modulo is $mod expected 1")
+ }
+ }
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -31,7 +31,6 @@ import net.taler.common.errorcodes.TalerErrorCode
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import tech.libeufin.util.extractReservePubFromSubject
-import tech.libeufin.util.stripIbanPayto
import java.time.Instant
import kotlin.math.abs
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt
@@ -217,6 +217,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
maxDebt: TalerAmount,
bonus: TalerAmount?
): CustomerCreationResult = conn { it ->
+ val now = Instant.now().toDbMicros() ?: throw faultyTimestampByBank();
it.transaction { conn ->
val idempotent = conn.prepareStatement("""
SELECT password_hash, name=?
@@ -270,6 +271,20 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
setString(6, cashoutPayto?.canonical)
oneOrNull { it.getLong("customer_id") }!!
}
+
+ conn.prepareStatement("""
+ INSERT INTO iban_history(
+ iban
+ ,creation_time
+ ) VALUES (?, ?)
+ """).run {
+ setString(1, internalPaytoUri.iban)
+ setLong(2, now)
+ if (!executeUpdateViolation()) {
+ conn.rollback()
+ return@transaction CustomerCreationResult.CONFLICT_PAY_TO
+ }
+ }
conn.prepareStatement("""
INSERT INTO bank_accounts(
@@ -300,7 +315,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val
setString(1, internalPaytoUri.canonical)
setLong(2, bonus.value)
setInt(3, bonus.frac)
- setLong(4, Instant.now().toDbMicros() ?: throw faultyTimestampByBank())
+ setLong(4, now)
executeQuery().use {
when {
!it.next() -> throw internalServerError("Bank transaction didn't properly return")
diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt
@@ -31,13 +31,13 @@ class AmountTest {
@Test
fun computationTest() = bankSetup { db ->
val conn = db.dbPool.getConnection().unwrap(PgConnection::class.java)
- conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 100000 WHERE internal_payto_uri = '${IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ").canonical}'")
+ conn.execSQLUpdate("UPDATE libeufin_bank.bank_accounts SET balance.val = 100000 WHERE internal_payto_uri = '$exchangePayto'")
val stmt = conn.prepareStatement("""
UPDATE libeufin_bank.bank_accounts
SET balance = (?, ?)::taler_amount
,has_debt = ?
,max_debt = (?, ?)::taler_amount
- WHERE internal_payto_uri = '${IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ").canonical}'
+ WHERE internal_payto_uri = '$merchantPayto'
""")
suspend fun routine(balance: TalerAmount, due: TalerAmount, hasBalanceDebt: Boolean, maxDebt: TalerAmount): Boolean {
stmt.setLong(1, balance.value)
@@ -49,7 +49,7 @@ class AmountTest {
// Check bank transaction
stmt.executeUpdate()
val (txRes, _) = db.bankTransaction(
- creditAccountPayto = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"),
+ creditAccountPayto = exchangePayto,
debitAccountUsername = "merchant",
subject = "test",
amount = due,
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -11,7 +11,6 @@ import net.taler.common.errorcodes.TalerErrorCode
import org.junit.Test
import tech.libeufin.bank.*
import tech.libeufin.util.CryptoUtil
-import tech.libeufin.util.stripIbanPayto
import java.util.*
import java.time.Instant
import kotlin.test.*
@@ -53,7 +52,7 @@ class BankIntegrationApiTest {
val reserve_pub = randEddsaPublicKey()
val req = json {
"reserve_pub" to reserve_pub
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
// Check bad UUID
@@ -104,14 +103,14 @@ class BankIntegrationApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/UNKNOWN-IBAN-XYZ")
+ "selected_exchange" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_ACCOUNT)
// Check account not exchange
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ")
+ "selected_exchange" to merchantPayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_NOT_EXCHANGE)
}
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -634,7 +634,7 @@ class CoreBankTransactionsApiTest {
authRoutine("/accounts/merchant/transactions/1", method = HttpMethod.Get)
// Create transaction
- tx("merchant", "KUDOS:0.3", "exchange")
+ tx("merchant", "KUDOS:0.3", "exchange", "tx")
// Check OK
client.get("/accounts/merchant/transactions/1") {
basicAuth("merchant", "merchant-password")
@@ -657,7 +657,7 @@ class CoreBankTransactionsApiTest {
@Test
fun create() = bankSetup { _ ->
val valid_req = json {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
+ "payto_uri" to "$exchangePayto?message=payout"
"amount" to "KUDOS:0.3"
}
@@ -682,7 +682,7 @@ class CoreBankTransactionsApiTest {
client.post("/accounts/merchant/transactions") {
basicAuth("merchant", "merchant-password")
jsonBody {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout2&amount=KUDOS:1.05"
+ "payto_uri" to "$exchangePayto?message=payout2&amount=KUDOS:1.05"
}
}.assertOk().run {
val id = json<TransactionCreateResponse>().row_id
@@ -699,7 +699,7 @@ class CoreBankTransactionsApiTest {
client.post("/accounts/merchant/transactions") {
basicAuth("merchant", "merchant-password")
jsonBody {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout3&amount=KUDOS:1.05"
+ "payto_uri" to "$exchangePayto?message=payout3&amount=KUDOS:1.05"
"amount" to "KUDOS:10.003"
}
}.assertOk().run {
@@ -732,7 +732,7 @@ class CoreBankTransactionsApiTest {
basicAuth("merchant", "merchant-password")
contentType(ContentType.Application.Json)
jsonBody(valid_req) {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ"
+ "payto_uri" to "$exchangePayto"
}
}.assertBadRequest()
// Unknown creditor
@@ -740,7 +740,7 @@ class CoreBankTransactionsApiTest {
basicAuth("merchant", "merchant-password")
contentType(ContentType.Application.Json)
jsonBody(valid_req) {
- "payto_uri" to "payto://iban/UNKNOWN-IBAN-XYZ?message=payout"
+ "payto_uri" to "$unknownPayto?message=payout"
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
// Transaction to self
@@ -748,7 +748,7 @@ class CoreBankTransactionsApiTest {
basicAuth("merchant", "merchant-password")
contentType(ContentType.Application.Json)
jsonBody(valid_req) {
- "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout"
+ "payto_uri" to "$merchantPayto?message=payout"
}
}.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT)
@@ -782,17 +782,12 @@ class CoreBankTransactionsApiTest {
checkBalance(true, "KUDOS:2.4", false, "KUDOS:0")
// Send 2 times 3
repeat(2) {
- client.post("/accounts/merchant/transactions") {
- basicAuth("merchant", "merchant-password")
- jsonBody {
- "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3"
- }
- }.assertOk()
+ tx("merchant", "KUDOS:3", "customer")
}
client.post("/accounts/merchant/transactions") {
basicAuth("merchant", "merchant-password")
jsonBody {
- "payto_uri" to "payto://iban/CUSTOMER-IBAN-XYZ?message=payout2&amount=KUDOS:3"
+ "payto_uri" to "$customerPayto?message=payout2&amount=KUDOS:3"
}
}.assertConflict(TalerErrorCode.BANK_UNALLOWED_DEBIT)
checkBalance(true, "KUDOS:8.4", false, "KUDOS:6")
@@ -800,7 +795,7 @@ class CoreBankTransactionsApiTest {
client.post("/accounts/customer/transactions") {
basicAuth("customer", "customer-password")
jsonBody {
- "payto_uri" to "payto://iban/MERCHANT-IBAN-XYZ?message=payout2&amount=KUDOS:10"
+ "payto_uri" to "$merchantPayto?message=payout2&amount=KUDOS:10"
}
}.assertOk()
checkBalance(false, "KUDOS:1.6", true, "KUDOS:4")
@@ -879,7 +874,7 @@ class CoreBankWithdrawalApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
@@ -899,7 +894,7 @@ class CoreBankWithdrawalApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
client.post("/withdrawals/$uuid/confirm").assertNoContent()
@@ -943,7 +938,7 @@ class CoreBankWithdrawalApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
@@ -963,7 +958,7 @@ class CoreBankWithdrawalApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
client.post("/withdrawals/$uuid/abort").assertNoContent()
@@ -983,7 +978,7 @@ class CoreBankWithdrawalApiTest {
client.post("/taler-integration/withdrawal-operation/$uuid") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
diff --git a/bank/src/test/kotlin/SecurityTest.kt b/bank/src/test/kotlin/SecurityTest.kt
@@ -35,7 +35,7 @@ class SecurityTest {
@Test
fun bodySizeLimit() = bankSetup { _ ->
val valid_req = json {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout"
+ "payto_uri" to "$exchangePayto?message=payout"
"amount" to "KUDOS:0.3"
}
client.post("/accounts/merchant/transactions") {
@@ -47,7 +47,7 @@ class SecurityTest {
client.post("/accounts/merchant/transactions") {
basicAuth("merchant", "merchant-password")
jsonBody(valid_req) {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+ "payto_uri" to "$exchangePayto?message=payout${"A".repeat(4100)}"
}
}.assertBadRequest()
@@ -55,7 +55,7 @@ class SecurityTest {
client.post("/accounts/merchant/transactions") {
basicAuth("merchant", "merchant-password")
jsonBody(valid_req, deflate = true) {
- "payto_uri" to "payto://iban/EXCHANGE-IBAN-XYZ?message=payout${"A".repeat(4100)}"
+ "payto_uri" to "$exchangePayto?message=payout${"A".repeat(4100)}"
}
}.assertBadRequest()
}
diff --git a/bank/src/test/kotlin/StatsTest.kt b/bank/src/test/kotlin/StatsTest.kt
@@ -53,7 +53,7 @@ class StatsTest {
db.conn { conn ->
val stmt = conn.prepareStatement("SELECT 0 FROM cashin(?, ?, (?, ?)::taler_amount, ?)")
stmt.setLong(1, Instant.now().toDbMicros()!!)
- stmt.setString(2, IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ").canonical)
+ stmt.setString(2, customerPayto.canonical)
val amount = TalerAmount(amount)
stmt.setLong(3, amount.value)
stmt.setInt(4, amount.frac)
diff --git a/bank/src/test/kotlin/WireGatewayApiTest.kt b/bank/src/test/kotlin/WireGatewayApiTest.kt
@@ -17,18 +17,6 @@ import kotlin.test.assertNotNull
import randHashCode
class WireGatewayApiTest {
- suspend fun Database.genTransaction(from: String, to: IbanPayTo, subject: String) {
- bankTransaction(
- creditAccountPayto = to,
- debitAccountUsername = from,
- subject = subject,
- amount = TalerAmount("KUDOS:10"),
- timestamp = Instant.now(),
- ).run {
- assertEquals(BankTransactionResult.SUCCESS, first)
- }
- }
-
// Test endpoint is correctly authenticated
suspend fun ApplicationTestBuilder.authRoutine(path: String, body: JsonObject? = null, method: HttpMethod = HttpMethod.Post, requireAdmin: Boolean = false) {
// No body when authentication must happen before parsing the body
@@ -78,7 +66,7 @@ class WireGatewayApiTest {
"amount" to "KUDOS:55"
"exchange_base_url" to "http://exchange.example.com/"
"wtid" to randShortHashCode()
- "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+ "credit_account" to merchantPayto
};
authRoutine("/accounts/merchant/taler-wire-gateway/transfer", valid_req)
@@ -125,7 +113,7 @@ class WireGatewayApiTest {
jsonBody(valid_req) {
"request_uid" to randHashCode()
"wtid" to randShortHashCode()
- "credit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+ "credit_account" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_CREDITOR)
@@ -135,7 +123,7 @@ class WireGatewayApiTest {
jsonBody(valid_req) {
"request_uid" to randHashCode()
"wtid" to randShortHashCode()
- "credit_account" to "payto://iban/EXCHANGE-IBAN-XYZ"
+ "credit_account" to exchangePayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
@@ -176,7 +164,7 @@ class WireGatewayApiTest {
* Testing the /history/incoming call from the TWG API.
*/
@Test
- fun historyIncoming() = bankSetup { db ->
+ fun historyIncoming() = bankSetup {
// Give Foo reasonable debt allowance:
setMaxDebt("merchant", TalerAmount("KUDOS:1000"))
@@ -198,11 +186,11 @@ class WireGatewayApiTest {
addIncoming("KUDOS:10")
}
// Should not show up in the taler wire gateway API history
- db.genTransaction("merchant", IbanPayTo("payto://iban/exchange-IBAN-XYZ"), "bogus")
+ tx("merchant", "KUDOS:10", "exchange", "bogus")
// Exchange pays merchant once, but that should not appear in the result
- db.genTransaction("exchange", IbanPayTo("payto://iban/merchant-IBAN-XYZ"), "ignored")
+ tx("exchange", "KUDOS:10", "merchant", "ignored")
// Gen one transaction using raw bank transaction logic
- db.genTransaction("merchant", IbanPayTo("payto://iban/exchange-IBAN-XYZ"), IncomingTxMetadata(randShortHashCode()).encode())
+ tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode())
// Gen one transaction using withdraw logic
client.post("/accounts/merchant/withdrawals") {
basicAuth("merchant", "merchant-password")
@@ -213,7 +201,7 @@ class WireGatewayApiTest {
client.post("/taler-integration/withdrawal-operation/${uuid}") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
client.post("/withdrawals/${uuid}/confirm") {
@@ -274,7 +262,7 @@ class WireGatewayApiTest {
}
}
delay(200)
- db.genTransaction("merchant", IbanPayTo("payto://iban/exchange-IBAN-XYZ"), IncomingTxMetadata(randShortHashCode()).encode())
+ tx("merchant", "KUDOS:10", "exchange", IncomingTxMetadata(randShortHashCode()).encode())
}
// Test trigger by withdraw operationr
@@ -296,7 +284,7 @@ class WireGatewayApiTest {
client.post("/taler-integration/withdrawal-operation/${uuid}") {
jsonBody {
"reserve_pub" to randEddsaPublicKey()
- "selected_exchange" to IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ")
+ "selected_exchange" to exchangePayto
}
}.assertOk()
client.post("/withdrawals/${uuid}/confirm") {
@@ -326,7 +314,7 @@ class WireGatewayApiTest {
* Testing the /history/outgoing call from the TWG API.
*/
@Test
- fun historyOutgoing() = bankSetup { db ->
+ fun historyOutgoing() = bankSetup {
setMaxDebt("exchange", TalerAmount("KUDOS:1000000"))
suspend fun HttpResponse.assertHistory(size: Int) {
@@ -347,12 +335,12 @@ class WireGatewayApiTest {
transfer("KUDOS:10")
}
// Should not show up in the taler wire gateway API history
- db.genTransaction("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), "bogus")
+ tx("exchange", "KUDOS:10", "merchant", "bogus")
// Merchant pays exchange once, but that should not appear in the result
- db.genTransaction("merchant", IbanPayTo("payto://iban/exchange-IBAN-XYZ"), "ignored")
+ tx("merchant", "KUDOS:10", "exchange", "ignored")
// Gen two transactions using raw bank transaction logic
repeat(2) {
- db.genTransaction("exchange", IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"), OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode())
+ tx("exchange", "KUDOS:10", "merchant", OutgoingTxMetadata(randShortHashCode(), ExchangeUrl("http://exchange.example.com/")).encode())
}
// Check ignore bogus subject
@@ -420,7 +408,7 @@ class WireGatewayApiTest {
val valid_req = json {
"amount" to "KUDOS:44"
"reserve_pub" to randEddsaPublicKey()
- "debit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+ "debit_account" to merchantPayto
};
authRoutine("/accounts/merchant/taler-wire-gateway/admin/add-incoming", valid_req, requireAdmin = true)
@@ -455,7 +443,7 @@ class WireGatewayApiTest {
basicAuth("admin", "admin-password")
jsonBody(valid_req) {
"reserve_pub" to randEddsaPublicKey()
- "debit_account" to "payto://iban/UNKNOWN-IBAN-XYZ"
+ "debit_account" to unknownPayto
}
}.assertConflict(TalerErrorCode.BANK_UNKNOWN_DEBTOR)
@@ -464,7 +452,7 @@ class WireGatewayApiTest {
basicAuth("admin", "admin-password")
jsonBody(valid_req) {
"reserve_pub" to randEddsaPublicKey()
- "debit_account" to "payto://iban/EXCHANGE-IBAN-XYZ"
+ "debit_account" to exchangePayto
}
}.assertConflict(TalerErrorCode.BANK_ACCOUNT_IS_EXCHANGE)
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
@@ -16,6 +16,16 @@ import tech.libeufin.util.*
/* ----- Setup ----- */
+val merchantPayto = IbanPayTo(genIbanPaytoUri())
+val exchangePayto = IbanPayTo(genIbanPaytoUri())
+val customerPayto = IbanPayTo(genIbanPaytoUri())
+val unknownPayto = IbanPayTo(genIbanPaytoUri())
+val paytos = mapOf(
+ "merchant" to merchantPayto,
+ "exchange" to exchangePayto,
+ "customer" to customerPayto
+)
+
fun setup(
conf: String = "test.conf",
lambda: suspend (Database, BankConfig) -> Unit
@@ -43,7 +53,7 @@ fun bankSetup(
login = "merchant",
password = "merchant-password",
name = "Merchant",
- internalPaytoUri = IbanPayTo("payto://iban/MERCHANT-IBAN-XYZ"),
+ internalPaytoUri = merchantPayto,
maxDebt = TalerAmount(10, 0, "KUDOS"),
isTalerExchange = false,
isPublic = false,
@@ -53,7 +63,7 @@ fun bankSetup(
login = "exchange",
password = "exchange-password",
name = "Exchange",
- internalPaytoUri = IbanPayTo("payto://iban/EXCHANGE-IBAN-XYZ"),
+ internalPaytoUri = exchangePayto,
maxDebt = TalerAmount(10, 0, "KUDOS"),
isTalerExchange = true,
isPublic = false,
@@ -63,7 +73,7 @@ fun bankSetup(
login = "customer",
password = "customer-password",
name = "Customer",
- internalPaytoUri = IbanPayTo("payto://iban/CUSTOMER-IBAN-XYZ"),
+ internalPaytoUri = customerPayto,
maxDebt = TalerAmount(10, 0, "KUDOS"),
isTalerExchange = false,
isPublic = false,
@@ -103,11 +113,11 @@ suspend fun ApplicationTestBuilder.assertBalance(account: String, info: CreditDe
}
}
-suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String): Long {
+suspend fun ApplicationTestBuilder.tx(from: String, amount: String, to: String, subject: String = "payout"): Long {
return client.post("/accounts/$from/transactions") {
basicAuth("$from", "$from-password")
jsonBody {
- "payto_uri" to "payto://iban/$to-IBAN-XYZ?message=tx&amount=$amount"
+ "payto_uri" to "${paytos[to]}?message=${subject.encodeURLQueryComponent()}&amount=$amount"
}
}.assertOk().run {
json<TransactionCreateResponse>().row_id
@@ -122,7 +132,7 @@ suspend fun ApplicationTestBuilder.transfer(amount: String) {
"amount" to TalerAmount(amount)
"exchange_base_url" to "http://exchange.example.com/"
"wtid" to randShortHashCode()
- "credit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+ "credit_account" to merchantPayto
}
}.assertOk()
}
@@ -133,7 +143,7 @@ suspend fun ApplicationTestBuilder.addIncoming(amount: String) {
jsonBody {
"amount" to TalerAmount(amount)
"reserve_pub" to randEddsaPublicKey()
- "debit_account" to "payto://iban/MERCHANT-IBAN-XYZ"
+ "debit_account" to merchantPayto
}
}.assertOk()
}
diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql
@@ -67,7 +67,6 @@ CREATE TABLE IF NOT EXISTS customers
,phone TEXT
,cashout_payto TEXT
);
-
COMMENT ON COLUMN customers.cashout_payto
IS 'RFC 8905 payto URI to collect fiat payments that come from the conversion of regional currency cash-out operations.';
COMMENT ON COLUMN customers.name
@@ -82,11 +81,9 @@ CREATE TABLE IF NOT EXISTS bearer_tokens
,is_refreshable BOOLEAN
,bank_customer BIGINT NOT NULL REFERENCES customers(customer_id) ON DELETE CASCADE
);
-
COMMENT ON TABLE bearer_tokens
IS 'Login tokens associated with one bank customer. There is currently'
' no garbage collector that deletes the expired tokens from the table';
-
COMMENT ON COLUMN bearer_tokens.bank_customer
IS 'The customer that directly created this token, or the customer that'
' created the very first token that originated all the refreshes until'
@@ -104,7 +101,6 @@ CREATE TABLE IF NOT EXISTS bank_accounts
,max_debt taler_amount DEFAULT (0, 0)
,has_debt BOOLEAN NOT NULL DEFAULT FALSE
);
-
COMMENT ON TABLE bank_accounts
IS 'In Sandbox, usernames (AKA logins) are different entities
respect to bank accounts (in contrast to what the Python bank
@@ -114,14 +110,18 @@ one bank account for one user, and additionally the bank
account label matches always the login.';
COMMENT ON COLUMN bank_accounts.has_debt
IS 'When true, the balance is negative';
-
COMMENT ON COLUMN bank_accounts.is_public
IS 'Indicates whether the bank account history
can be publicly shared';
-
COMMENT ON COLUMN bank_accounts.owning_customer_id
IS 'Login that owns the bank account';
+CREATE TABLE IF NOT EXISTS iban_history
+ (iban TEXT PRIMARY key
+ ,creation_time INT8 NOT NULL
+ );
+COMMENT ON TABLE iban_history IS 'Track all generated iban, some might be unused.';
+
-- end of: bank accounts
-- start of: money transactions
@@ -185,7 +185,7 @@ COMMENT ON COLUMN challenges.confirmation_date
CREATE TABLE IF NOT EXISTS cashout_operations
(cashout_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE
- ,request_uid BYTEA NOT NULL UNIQUE CHECK (LENGTH(request_uid)=32)
+ ,request_uid BYTEA NOT NULL PRIMARY KEY CHECK (LENGTH(request_uid)=32)
,amount_debit taler_amount NOT NULL
,amount_credit taler_amount NOT NULL
,subject TEXT NOT NULL
@@ -204,6 +204,9 @@ CREATE TABLE IF NOT EXISTS cashout_operations
ON DELETE RESTRICT
ON UPDATE RESTRICT
);
+COMMENT ON COLUMN cashout_operations.bank_account IS 'Bank amount to debit during confirmation';
+COMMENT ON COLUMN cashout_operations.challenge IS 'TAN challenge used to confirm the operation';
+COMMENT ON COLUMN cashout_operations.local_transaction IS 'Transaction generated during confirmation';
-- end of: cashout management
@@ -229,7 +232,8 @@ CREATE TABLE IF NOT EXISTS taler_exchange_incoming
);
CREATE TABLE IF NOT EXISTS taler_withdrawal_operations
- (withdrawal_uuid uuid NOT NULL PRIMARY KEY
+ (withdrawal_id BIGINT GENERATED BY DEFAULT AS IDENTITY
+ ,withdrawal_uuid uuid NOT NULL PRIMARY KEY
,amount taler_amount NOT NULL
,selection_done BOOLEAN DEFAULT FALSE NOT NULL
,aborted BOOLEAN DEFAULT FALSE NOT NULL