aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2024-02-02 21:10:20 +0100
committerAntoine A <>2024-02-02 21:10:20 +0100
commit836956edb700e953ceb03fbb862822c4f31fe722 (patch)
tree6848036fbdb9f5bbe48cc514930c3f9b6cd057f8
parente8c44ca6a3b7e5d91b74f060689bc2e4389faa35 (diff)
downloadlibeufin-836956edb700e953ceb03fbb862822c4f31fe722.tar.gz
libeufin-836956edb700e953ceb03fbb862822c4f31fe722.tar.bz2
libeufin-836956edb700e953ceb03fbb862822c4f31fe722.zip
Add support for x-taler-bank payto URI
-rw-r--r--bank/conf/test_x_taler_bank.conf12
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Config.kt6
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt74
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt2
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt12
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt8
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/helpers.kt2
-rw-r--r--bank/src/test/kotlin/BankIntegrationApiTest.kt3
-rw-r--r--bank/src/test/kotlin/DatabaseTest.kt5
-rw-r--r--bank/src/test/kotlin/PaytoTest.kt75
-rw-r--r--bank/src/test/kotlin/helpers.kt6
-rw-r--r--common/src/main/kotlin/TalerCommon.kt47
-rw-r--r--nexus/src/test/kotlin/DatabaseTest.kt6
14 files changed, 208 insertions, 52 deletions
diff --git a/bank/conf/test_x_taler_bank.conf b/bank/conf/test_x_taler_bank.conf
new file mode 100644
index 00000000..294aa371
--- /dev/null
+++ b/bank/conf/test_x_taler_bank.conf
@@ -0,0 +1,12 @@
+[libeufin-bank]
+DEFAULT_DEBT_LIMIT = KUDOS:100
+SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com
+ALLOW_REGISTRATION = yes
+ALLOW_ACCOUNT_DELETION = yes
+ALLOW_EDIT_NAME = yes
+ALLOW_EDIT_CASHOUT_PAYTO_URI = yes
+PAYMENT_METHOD = x-taler-bank
+X_TALER_BANK_PAYTO_HOSTNAME = bank.hostname.test
+
+[libeufin-bankdb-postgres]
+CONFIG = postgresql:///libeufincheck
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
index 5fece403..3440bd54 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt
@@ -43,7 +43,7 @@ fun Routing.bankIntegrationApi(db: Database, ctx: BankConfig) {
get("/taler-integration/withdrawal-operation/{wopid}") {
val uuid = call.uuidParameter("wopid")
val params = StatusParams.extract(call.request.queryParameters)
- val op = db.withdrawal.pollStatus(uuid, params) ?: throw notFound(
+ val op = db.withdrawal.pollStatus(uuid, params, ctx.wireMethod) ?: throw notFound(
"Withdrawal operation '$uuid' not found",
TalerErrorCode.BANK_TRANSACTION_NOT_FOUND
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
index b6db1799..7539da69 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -50,7 +50,8 @@ data class BankConfig(
val fiatCurrencySpec: CurrencySpecification?,
val spaPath: Path?,
val tanChannels: Map<TanChannel, Path>,
- val payto: BankPaytoCtx
+ val payto: BankPaytoCtx,
+ val wireMethod: WireMethod
)
@Serializable
@@ -130,7 +131,8 @@ fun TalerConfig.loadBankConfig(): BankConfig {
fiatCurrency = fiatCurrency,
fiatCurrencySpec = fiatCurrencySpec,
tanChannels = tanChannels,
- payto = payto
+ payto = payto,
+ wireMethod = method
)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
index e530b579..e6b8ae20 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -185,33 +185,59 @@ suspend fun createAccount(
TalerErrorCode.END
)
- var retry = if (req.payto_uri == null) IBAN_ALLOCATION_RETRY_COUNTER else 0
+ when (cfg.wireMethod) {
+ WireMethod.IBAN -> {
+ var retry = if (req.payto_uri == null) IBAN_ALLOCATION_RETRY_COUNTER else 0
- while (true) {
- val internalPayto = req.payto_uri ?: IbanPayto.rand() as Payto
- val res = db.account.create(
- login = req.username,
- name = req.name,
- email = req.contact_data?.email?.get(),
- phone = req.contact_data?.phone?.get(),
- cashoutPayto = req.cashout_payto_uri,
- password = req.password,
- internalPaytoUri = internalPayto,
- isPublic = req.is_public,
- isTalerExchange = req.is_taler_exchange,
- maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
- bonus = if (!req.is_taler_exchange) cfg.registrationBonus
- else TalerAmount(0, 0, cfg.regionalCurrency),
- tanChannel = req.tan_channel,
- checkPaytoIdempotent = req.payto_uri != null
- )
- // Retry with new IBAN
- if (res == AccountCreationResult.PayToReuse && retry > 0) {
- retry--
- continue
+ while (true) {
+ val internalPayto = req.payto_uri ?: IbanPayto.rand() as Payto
+ val res = db.account.create(
+ login = req.username,
+ name = req.name,
+ email = req.contact_data?.email?.get(),
+ phone = req.contact_data?.phone?.get(),
+ cashoutPayto = req.cashout_payto_uri,
+ password = req.password,
+ internalPayto = internalPayto,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
+ bonus = if (!req.is_taler_exchange) cfg.registrationBonus
+ else TalerAmount(0, 0, cfg.regionalCurrency),
+ tanChannel = req.tan_channel,
+ checkPaytoIdempotent = req.payto_uri != null
+ )
+ // Retry with new IBAN
+ if (res == AccountCreationResult.PayToReuse && retry > 0) {
+ retry--
+ continue
+ }
+ return Pair(res, internalPayto.bank(req.name, cfg.payto))
+ }
+ }
+ WireMethod.X_TALER_BANK -> {
+ val internalPayto = XTalerBankPayto.forUsername(req.username)
+ val res = db.account.create(
+ login = req.username,
+ name = req.name,
+ email = req.contact_data?.email?.get(),
+ phone = req.contact_data?.phone?.get(),
+ cashoutPayto = req.cashout_payto_uri,
+ password = req.password,
+ internalPayto = internalPayto,
+ isPublic = req.is_public,
+ isTalerExchange = req.is_taler_exchange,
+ maxDebt = req.debit_threshold ?: cfg.defaultDebtLimit,
+ bonus = if (!req.is_taler_exchange) cfg.registrationBonus
+ else TalerAmount(0, 0, cfg.regionalCurrency),
+ tanChannel = req.tan_channel,
+ checkPaytoIdempotent = req.payto_uri != null
+ )
+ return Pair(res, internalPayto.bank(req.name, cfg.payto))
}
- return Pair(res, internalPayto.bank(req.name, cfg.payto))
}
+
+
}
suspend fun patchAccount(
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
index 4622d2d5..822b1642 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -448,7 +448,7 @@ data class BankWithdrawalOperationStatus(
val confirm_transfer_url: String? = null,
val selected_reserve_pub: EddsaPublicKey? = null,
val selected_exchange_account: String? = null,
- val wire_types: MutableList<String> = mutableListOf("iban"),
+ val wire_types: List<String>,
// TODO remove
val aborted: Boolean,
val selection_done: Boolean,
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 16d5d4d1..e172ea78 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -42,7 +42,7 @@ class AccountDAO(private val db: Database) {
email: String?,
phone: String?,
cashoutPayto: IbanPayto?,
- internalPaytoUri: Payto,
+ internalPayto: Payto,
isPublic: Boolean,
isTalerExchange: Boolean,
maxDebt: TalerAmount,
@@ -73,7 +73,7 @@ class AccountDAO(private val db: Database) {
setString(3, phone)
setString(4, cashoutPayto?.full(name))
setBoolean(5, checkPaytoIdempotent)
- setString(6, internalPaytoUri.canonical)
+ setString(6, internalPayto.canonical)
setBoolean(7, isPublic)
setBoolean(8, isTalerExchange)
setString(9, tanChannel?.name)
@@ -90,14 +90,14 @@ class AccountDAO(private val db: Database) {
AccountCreationResult.LoginReuse
}
} else {
- if (internalPaytoUri is IbanPayto)
+ if (internalPayto is IbanPayto)
conn.prepareStatement("""
INSERT INTO iban_history(
iban
,creation_time
) VALUES (?, ?)
""").run {
- setString(1, internalPaytoUri.iban.value)
+ setString(1, internalPayto.iban.value)
setLong(2, now)
if (!executeUpdateViolation()) {
conn.rollback()
@@ -137,7 +137,7 @@ class AccountDAO(private val db: Database) {
,max_debt
) VALUES (?, ?, ?, ?, (?, ?)::taler_amount)
""").run {
- setString(1, internalPaytoUri.canonical)
+ setString(1, internalPayto.canonical)
setLong(2, customerId)
setBoolean(3, isPublic)
setBoolean(4, isTalerExchange)
@@ -154,7 +154,7 @@ class AccountDAO(private val db: Database) {
SELECT out_balance_insufficient
FROM bank_transaction(?,'admin','bonus',(?,?)::taler_amount,?,true)
""").run {
- setString(1, internalPaytoUri.canonical)
+ setString(1, internalPayto.canonical)
setLong(2, bonus.value)
setInt(3, bonus.frac)
setLong(4, now)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
index 36abdb64..cc952691 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt
@@ -267,7 +267,7 @@ class WithdrawalDAO(private val db: Database) {
}
/** Pool public status of operation [uuid] */
- suspend fun pollStatus(uuid: UUID, params: StatusParams): BankWithdrawalOperationStatus? =
+ suspend fun pollStatus(uuid: UUID, params: StatusParams, wire: WireMethod): BankWithdrawalOperationStatus? =
poll(uuid, params, status = { it.status }) {
db.conn { conn ->
val stmt = conn.prepareStatement("""
@@ -303,6 +303,12 @@ class WithdrawalDAO(private val db: Database) {
suggested_exchange = null,
selected_exchange_account = it.getString("selected_exchange_payto"),
selected_reserve_pub = it.getBytes("reserve_pub")?.run(::EddsaPublicKey),
+ wire_types = listOf(
+ when (wire) {
+ WireMethod.IBAN -> "iban"
+ WireMethod.X_TALER_BANK -> "x-taler-bank"
+ }
+ )
)
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 1b5ec830..5f52fda0 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -122,7 +122,7 @@ suspend fun maybeCreateAdminAccount(db: Database, ctx: BankConfig, pw: String? =
login = "admin",
password = pwStr,
name = "Bank administrator",
- internalPaytoUri = IbanPayto.rand(),
+ internalPayto = IbanPayto.rand(),
isPublic = false,
isTalerExchange = false,
maxDebt = ctx.defaultDebtLimit,
diff --git a/bank/src/test/kotlin/BankIntegrationApiTest.kt b/bank/src/test/kotlin/BankIntegrationApiTest.kt
index e36668a3..73d6a700 100644
--- a/bank/src/test/kotlin/BankIntegrationApiTest.kt
+++ b/bank/src/test/kotlin/BankIntegrationApiTest.kt
@@ -53,7 +53,8 @@ class BankIntegrationApiTest {
assert(!it.aborted)
assert(!it.transfer_done)
assertEquals(amount, it.amount)
- // TODO check all status
+ assertEquals(listOf("iban"), it.wire_types)
+ assertEquals(amount, it.amount)
}
}
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
index 01cb0dd2..877e6042 100644
--- a/bank/src/test/kotlin/DatabaseTest.kt
+++ b/bank/src/test/kotlin/DatabaseTest.kt
@@ -168,7 +168,4 @@ class DatabaseTest {
assertEquals(Triple(true, false, false), cTry(this, "new-code", expired))
}
}}
-}
-
-
-
+} \ No newline at end of file
diff --git a/bank/src/test/kotlin/PaytoTest.kt b/bank/src/test/kotlin/PaytoTest.kt
new file mode 100644
index 00000000..635d864f
--- /dev/null
+++ b/bank/src/test/kotlin/PaytoTest.kt
@@ -0,0 +1,75 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2024 Taler Systems S.A.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING. If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+
+import io.ktor.client.plugins.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+import io.ktor.http.*
+import io.ktor.server.testing.*
+import java.util.*
+import kotlinx.coroutines.*
+import kotlinx.serialization.json.*
+import org.junit.Test
+import tech.libeufin.bank.*
+import tech.libeufin.common.*
+import kotlin.test.*
+
+class PaytoTest {
+ // x-taler-bank
+ @Test
+ fun xTalerBank() = bankSetup("test_x_taler_bank.conf") { _ ->
+ // Check Ok
+ client.post("/accounts") {
+ json {
+ "username" to "john"
+ "password" to "john-password"
+ "name" to "John"
+ }
+ }.assertOkJson<RegisterAccountResponse> {
+ assertEquals("payto://x-taler-bank/bank.hostname.test/john?receiver-name=John", it.internal_payto_uri)
+ }
+
+ // Check payto_uri is ignored
+ client.post("/accounts") {
+ json {
+ "username" to "foo"
+ "password" to "foo-password"
+ "name" to "Jane"
+ "payto_uri" to IbanPayto.rand()
+ }
+ }.assertOkJson<RegisterAccountResponse> {
+ assertEquals("payto://x-taler-bank/bank.hostname.test/foo?receiver-name=Jane", it.internal_payto_uri)
+ }
+
+ // Check payto canonicalisation
+ client.postA("/accounts/john/transactions") {
+ json {
+ "payto_uri" to "payto://x-taler-bank/ignored/foo?message=payout&amount=KUDOS:0.3"
+ }
+ }.assertOkJson<TransactionCreateResponse> {
+ client.getA("/accounts/john/transactions/${it.row_id}")
+ .assertOkJson<BankAccountTransactionInfo> { tx ->
+ assertEquals("payout", tx.subject)
+ assertEquals("payto://x-taler-bank/bank.hostname.test/foo?receiver-name=Jane", tx.creditor_payto_uri)
+ assertEquals("payto://x-taler-bank/bank.hostname.test/john?receiver-name=John", tx.debtor_payto_uri)
+ assertEquals(TalerAmount("KUDOS:0.3"), tx.amount)
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 14ebe805..3175c74c 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -86,7 +86,7 @@ fun bankSetup(
login = "merchant",
password = "merchant-password",
name = "Merchant",
- internalPaytoUri = merchantPayto,
+ internalPayto = merchantPayto,
maxDebt = TalerAmount("KUDOS:10"),
isTalerExchange = false,
isPublic = false,
@@ -101,7 +101,7 @@ fun bankSetup(
login = "exchange",
password = "exchange-password",
name = "Exchange",
- internalPaytoUri = exchangePayto,
+ internalPayto = exchangePayto,
maxDebt = TalerAmount("KUDOS:10"),
isTalerExchange = true,
isPublic = false,
@@ -116,7 +116,7 @@ fun bankSetup(
login = "customer",
password = "customer-password",
name = "Customer",
- internalPaytoUri = customerPayto,
+ internalPayto = customerPayto,
maxDebt = TalerAmount("KUDOS:10"),
isTalerExchange = false,
isPublic = false,
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
index ebc9fe93..b96e87a3 100644
--- a/common/src/main/kotlin/TalerCommon.kt
+++ b/common/src/main/kotlin/TalerCommon.kt
@@ -150,12 +150,20 @@ sealed class Payto {
/** Transform a payto URI to its bank form, using [name] as the receiver-name and the bank [ctx] */
fun bank(name: String, ctx: BankPaytoCtx): String = when (this) {
is IbanPayto -> "payto://iban/${ctx.bic!!}/$iban?receiver-name=${name.encodeURLParameter()}"
+ is XTalerBankPayto -> "payto://x-taler-bank/${ctx.hostname!!}/$username?receiver-name=${name.encodeURLParameter()}"
}
fun expectIban(): IbanPayto {
return when (this) {
is IbanPayto -> this
- else -> throw CommonError.Payto("expected a IBAN payto URI got '${parsed.host}'")
+ else -> throw CommonError.Payto("expected an IBAN payto URI got '${parsed.host}'")
+ }
+ }
+
+ fun expectXTalerBank(): XTalerBankPayto {
+ return when (this) {
+ is XTalerBankPayto -> this
+ else -> throw CommonError.Payto("expected a x-taler-bank payto URI got '${parsed.host}'")
}
}
@@ -188,11 +196,11 @@ sealed class Payto {
return when (parsed.host) {
"iban" -> {
- val splitPath = parsed.path.split("/").filter { it.isNotEmpty() }
+ val splitPath = parsed.path.split("/", limit=3).filter { it.isNotEmpty() }
val (bic, rawIban) = when (splitPath.size) {
1 -> Pair(null, splitPath[0])
2 -> Pair(splitPath[0], splitPath[1])
- else -> throw CommonError.Payto("too many path segments for a IBAN payto URI")
+ else -> throw CommonError.Payto("too many path segments for an IBAN payto URI")
}
val iban = IBAN.parse(rawIban)
IbanPayto(
@@ -205,14 +213,26 @@ sealed class Payto {
iban
)
}
+ "x-taler-bank" -> {
+ val splitPath = parsed.path.split("/", limit=3).filter { it.isNotEmpty() }
+ if (splitPath.size != 2)
+ throw CommonError.Payto("bad number of path segments for a x-taler-bank payto URI")
+ val username = splitPath[1]
+ XTalerBankPayto(
+ parsed,
+ "payto://x-taler-bank/localhost/$username",
+ amount,
+ message,
+ receiverName,
+ username
+ )
+ }
else -> throw CommonError.Payto("unsupported payto URI kind '${parsed.host}'")
}
}
}
}
-// TODO x-taler-bank Payto
-
@Serializable(with = IbanPayto.Serializer::class)
class IbanPayto internal constructor(
override val parsed: URI,
@@ -252,6 +272,23 @@ class IbanPayto internal constructor(
}
}
+class XTalerBankPayto internal constructor(
+ override val parsed: URI,
+ override val canonical: String,
+ override val amount: TalerAmount?,
+ override val message: String?,
+ override val receiverName: String?,
+ val username: String
+): Payto() {
+ override fun toString(): String = parsed.toString()
+
+ companion object {
+ fun forUsername(username: String): XTalerBankPayto {
+ return Payto.parse("payto://x-taler-bank/hostname/$username").expectXTalerBank()
+ }
+ }
+}
+
/** Context specific data nescessary to create a bank payto URI from a canonical payto URI */
data class BankPaytoCtx(
val bic: String? = null,
diff --git a/nexus/src/test/kotlin/DatabaseTest.kt b/nexus/src/test/kotlin/DatabaseTest.kt
index dd7d0e85..187ac0f1 100644
--- a/nexus/src/test/kotlin/DatabaseTest.kt
+++ b/nexus/src/test/kotlin/DatabaseTest.kt
@@ -289,9 +289,9 @@ class PaymentInitiationsTest {
db.initiatedPaymentsSubmittableGet("KUDOS").apply {
assertEquals(3, this.size)
- assertEquals("#1", this[0]?.wireTransferSubject)
- assertEquals("#2", this[1]?.wireTransferSubject)
- assertEquals("#4", this[2]?.wireTransferSubject)
+ assertEquals("#1", this[0].wireTransferSubject)
+ assertEquals("#2", this[1].wireTransferSubject)
+ assertEquals("#4", this[2].wireTransferSubject)
}
}
}