diff options
author | Antoine A <> | 2024-02-02 21:10:20 +0100 |
---|---|---|
committer | Antoine A <> | 2024-02-02 21:10:20 +0100 |
commit | 836956edb700e953ceb03fbb862822c4f31fe722 (patch) | |
tree | 6848036fbdb9f5bbe48cc514930c3f9b6cd057f8 | |
parent | e8c44ca6a3b7e5d91b74f060689bc2e4389faa35 (diff) | |
download | libeufin-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.conf | 12 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/BankIntegrationApi.kt | 2 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/Config.kt | 6 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 74 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 2 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt | 12 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/db/WithdrawalDAO.kt | 8 | ||||
-rw-r--r-- | bank/src/main/kotlin/tech/libeufin/bank/helpers.kt | 2 | ||||
-rw-r--r-- | bank/src/test/kotlin/BankIntegrationApiTest.kt | 3 | ||||
-rw-r--r-- | bank/src/test/kotlin/DatabaseTest.kt | 5 | ||||
-rw-r--r-- | bank/src/test/kotlin/PaytoTest.kt | 75 | ||||
-rw-r--r-- | bank/src/test/kotlin/helpers.kt | 6 | ||||
-rw-r--r-- | common/src/main/kotlin/TalerCommon.kt | 47 | ||||
-rw-r--r-- | nexus/src/test/kotlin/DatabaseTest.kt | 6 |
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) } } } |