commit ab6b90924a3f9c14c11ace80ea91772144ad4c84
parent 1f1c99b56623000c61b5b735604683ffe911e31b
Author: Antoine A <>
Date: Thu, 1 Feb 2024 14:22:41 +0100
receiver-name everywhere
Diffstat:
13 files changed, 111 insertions(+), 71 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt
@@ -138,7 +138,7 @@ private fun Routing.coreBankTokenApi(db: Database) {
}
}
-suspend fun createAccount(db: Database, ctx: BankConfig, req: RegisterAccountRequest, isAdmin: Boolean): Pair<AccountCreationResult, IbanPayto> {
+suspend fun createAccount(db: Database, ctx: BankConfig, req: RegisterAccountRequest, isAdmin: Boolean): Pair<AccountCreationResult, FullIbanPayto> {
// Prohibit reserved usernames:
if (RESERVED_ACCOUNTS.contains(req.username))
throw conflict(
@@ -205,7 +205,7 @@ suspend fun createAccount(db: Database, ctx: BankConfig, req: RegisterAccountReq
retry--
continue
}
- return Pair(res, internalPayto)
+ return Pair(res, internalPayto.withName(req.name))
}
}
@@ -264,10 +264,10 @@ private fun Routing.coreBankAccountsApi(db: Database, ctx: BankConfig) {
TalerErrorCode.BANK_REGISTER_USERNAME_REUSE
)
AccountCreationResult.PayToReuse -> throw conflict(
- "Bank internalPayToUri reuse '${internalPayto.canonical}'",
+ "Bank internalPayToUri reuse '${internalPayto.payto}'",
TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE
)
- AccountCreationResult.Success -> call.respond(RegisterAccountResponse(internalPayto.canonical))
+ AccountCreationResult.Success -> call.respond(RegisterAccountResponse(internalPayto))
}
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -141,10 +141,10 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) {
}
install(StatusPages) {
exception<Exception> { call, cause ->
+ logger.debug("request failed", cause)
when (cause) {
is LibeufinException -> call.err(cause)
is SQLException -> {
- logger.debug("request failed", cause)
when (cause.sqlState) {
PSQLState.SERIALIZATION_FAILURE.state -> call.err(
HttpStatusCode.InternalServerError,
@@ -194,7 +194,6 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) {
)
}
else -> {
- logger.debug("request failed", cause)
call.err(
HttpStatusCode.InternalServerError,
cause.message,
@@ -478,7 +477,7 @@ class CreateAccount : CliktCommand(
AccountCreationResult.LoginReuse ->
throw Exception("Account username reuse '${req.username}'")
AccountCreationResult.PayToReuse ->
- throw Exception("Bank internalPayToUri reuse '${internalPayto.canonical}'")
+ throw Exception("Bank internalPayToUri reuse '${internalPayto.payto}'")
AccountCreationResult.Success ->
logger.info("Account '${req.username}' created")
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/RevenueApi.kt
@@ -43,7 +43,7 @@ fun Routing.revenueApi(db: Database, ctx: BankConfig) {
if (items.isEmpty()) {
call.respond(HttpStatusCode.NoContent)
} else {
- call.respond(RevenueIncomingHistory(items, bankAccount.internalPaytoUri))
+ call.respond(RevenueIncomingHistory(items, bankAccount.payto))
}
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt
@@ -180,7 +180,7 @@ data class RegisterAccountRequest(
@Serializable
data class RegisterAccountResponse(
- val internal_payto_uri: String
+ val internal_payto_uri: FullIbanPayto
)
/**
@@ -245,7 +245,7 @@ data class MonitorWithConversion(
* from/to the database.
*/
data class BankInfo(
- val internalPaytoUri: String,
+ val payto: FullIbanPayto,
val bankAccountId: Long,
val isTalerExchange: Boolean,
)
@@ -341,7 +341,7 @@ data class Balance(
data class AccountMinimalData(
val username: String,
val name: String,
- val payto_uri: String,
+ val payto_uri: FullIbanPayto,
val balance: Balance,
val debit_threshold: TalerAmount,
val is_public: Boolean,
@@ -363,7 +363,7 @@ data class ListBankAccountsResponse(
data class AccountData(
val name: String,
val balance: Balance,
- val payto_uri: String,
+ val payto_uri: FullIbanPayto,
val debit_threshold: TalerAmount,
val contact_data: ChallengeContactData? = null,
val cashout_payto_uri: String? = null,
@@ -387,8 +387,8 @@ data class TransactionCreateResponse(
or from GET /transactions */
@Serializable
data class BankAccountTransactionInfo(
- val creditor_payto_uri: String,
- val debtor_payto_uri: String,
+ val creditor_payto_uri: FullIbanPayto,
+ val debtor_payto_uri: FullIbanPayto,
val amount: TalerAmount,
val direction: TransactionDirection,
val subject: String,
@@ -557,7 +557,7 @@ data class AddIncomingResponse(
@Serializable
data class IncomingHistory(
val incoming_transactions: List<IncomingReserveTransaction>,
- val credit_account: String // Receiver's Payto URI.
+ val credit_account: FullIbanPayto
)
/**
@@ -569,7 +569,7 @@ data class IncomingReserveTransaction(
val row_id: Long, // DB row ID of the payment.
val date: TalerProtocolTimestamp,
val amount: TalerAmount,
- val debit_account: String, // Payto of the sender.
+ val debit_account: FullIbanPayto,
val reserve_pub: EddsaPublicKey
)
@@ -579,7 +579,7 @@ data class IncomingReserveTransaction(
@Serializable
data class OutgoingHistory(
val outgoing_transactions: List<OutgoingTransaction>,
- val debit_account: String // Debitor's Payto URI.
+ val debit_account: FullIbanPayto
)
/**
@@ -590,7 +590,7 @@ data class OutgoingTransaction(
val row_id: Long, // DB row ID of the payment.
val date: TalerProtocolTimestamp,
val amount: TalerAmount,
- val credit_account: String, // Payto of the receiver.
+ val credit_account: FullIbanPayto,
val wtid: ShortHashCode,
val exchange_base_url: String,
)
@@ -598,7 +598,7 @@ data class OutgoingTransaction(
@Serializable
data class RevenueIncomingHistory(
val incoming_transactions : List<RevenueIncomingBankTransaction>,
- val credit_account: String
+ val credit_account: FullIbanPayto
)
@Serializable
@@ -606,7 +606,7 @@ data class RevenueIncomingBankTransaction(
val row_id: Long,
val date: TalerProtocolTimestamp,
val amount: TalerAmount,
- val debit_account: String,
+ val debit_account: FullIbanPayto,
val subject: String
)
@@ -645,7 +645,7 @@ data class PublicAccountsResponse(
@Serializable
data class PublicAccount(
val username: String,
- val payto_uri: String,
+ val payto_uri: FullIbanPayto,
val balance: Balance,
val is_taler_exchange: Boolean,
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/WireGatewayApi.kt
@@ -82,7 +82,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
}
auth(db, TokenScope.readonly) {
suspend fun <T> PipelineContext<Unit, ApplicationCall>.historyEndpoint(
- reduce: (List<T>, String) -> Any,
+ reduce: (List<T>, FullIbanPayto) -> Any,
dbLambda: suspend ExchangeDAO.(HistoryParams, Long) -> List<T>
) {
val params = HistoryParams.extract(context.request.queryParameters)
@@ -99,7 +99,7 @@ fun Routing.wireGatewayApi(db: Database, ctx: BankConfig) {
if (items.isEmpty()) {
call.respond(HttpStatusCode.NoContent)
} else {
- call.respond(reduce(items, bankAccount.internalPaytoUri))
+ call.respond(reduce(items, bankAccount.payto))
}
}
get("/accounts/{USERNAME}/taler-wire-gateway/history/incoming") {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/AccountDAO.kt
@@ -96,7 +96,7 @@ class AccountDAO(private val db: Database) {
,creation_time
) VALUES (?, ?)
""").run {
- setString(1, internalPaytoUri.iban)
+ setString(1, internalPaytoUri.iban.value)
setLong(2, now)
if (!executeUpdateViolation()) {
conn.rollback()
@@ -423,6 +423,7 @@ class AccountDAO(private val db: Database) {
SELECT
bank_account_id
,internal_payto_uri
+ ,name
,is_taler_exchange
FROM bank_accounts
JOIN customers
@@ -432,7 +433,7 @@ class AccountDAO(private val db: Database) {
stmt.setString(1, login)
stmt.oneOrNull {
BankInfo(
- internalPaytoUri = it.getString("internal_payto_uri"),
+ payto = it.getFullPayto("internal_payto_uri", "name"),
isTalerExchange = it.getBoolean("is_taler_exchange"),
bankAccountId = it.getLong("bank_account_id")
)
@@ -471,7 +472,7 @@ class AccountDAO(private val db: Database) {
),
tan_channel = it.getString("tan_channel")?.run { TanChannel.valueOf(this) },
cashout_payto_uri = it.getString("cashout_payto"),
- payto_uri = it.getString("internal_payto_uri"),
+ payto_uri = it.getFullPayto("internal_payto_uri", "name"),
balance = Balance(
amount = it.getAmount("balance", db.bankCurrency),
credit_debit_indicator =
@@ -499,8 +500,9 @@ class AccountDAO(private val db: Database) {
(balance).frac AS balance_frac,
has_debt,
internal_payto_uri,
- c.login
- ,is_taler_exchange
+ c.login,
+ is_taler_exchange,
+ name
FROM bank_accounts JOIN customers AS c
ON owning_customer_id = c.customer_id
WHERE is_public=true AND c.login LIKE ? AND
@@ -512,7 +514,7 @@ class AccountDAO(private val db: Database) {
) {
PublicAccount(
username = it.getString("login"),
- payto_uri = it.getString("internal_payto_uri"),
+ payto_uri = it.getFullPayto("internal_payto_uri", "name"),
balance = Balance(
amount = it.getAmount("balance", db.bankCurrency),
credit_debit_indicator = if (it.getBoolean("has_debt")) {
@@ -565,7 +567,7 @@ class AccountDAO(private val db: Database) {
debit_threshold = it.getAmount("max_debt", db.bankCurrency),
is_public = it.getBoolean("is_public"),
is_taler_exchange = it.getBoolean("is_taler_exchange"),
- payto_uri = it.getString("internal_payto_uri"),
+ payto_uri = it.getFullPayto("internal_payto_uri", "name"),
)
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ExchangeDAO.kt
@@ -40,6 +40,7 @@ class ExchangeDAO(private val db: Database) {
,(amount).val AS amount_val
,(amount).frac AS amount_frac
,debtor_payto_uri
+ ,debtor_name
,reserve_pub
FROM taler_exchange_incoming AS tfr
JOIN bank_account_transactions AS txs
@@ -50,7 +51,7 @@ class ExchangeDAO(private val db: Database) {
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
amount = it.getAmount("amount", db.bankCurrency),
- debit_account = it.getString("debtor_payto_uri"),
+ debit_account = it.getFullPayto("debtor_payto_uri", "debtor_name"),
reserve_pub = EddsaPublicKey(it.getBytes("reserve_pub")),
)
}
@@ -67,6 +68,7 @@ class ExchangeDAO(private val db: Database) {
,(amount).val AS amount_val
,(amount).frac AS amount_frac
,creditor_payto_uri
+ ,creditor_name
,wtid
,exchange_base_url
FROM taler_exchange_outgoing AS tfr
@@ -78,7 +80,7 @@ class ExchangeDAO(private val db: Database) {
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
amount = it.getAmount("amount", db.bankCurrency),
- credit_account = it.getString("creditor_payto_uri"),
+ credit_account = it.getFullPayto("creditor_payto_uri", "creditor_name"),
wtid = ShortHashCode(it.getBytes("wtid")),
exchange_base_url = it.getString("exchange_base_url")
)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/TransactionDAO.kt
@@ -145,7 +145,9 @@ class TransactionDAO(private val db: Database) {
val stmt = conn.prepareStatement("""
SELECT
creditor_payto_uri
+ ,creditor_name
,debtor_payto_uri
+ ,debtor_name
,subject
,(amount).val AS amount_val
,(amount).frac AS amount_frac
@@ -161,8 +163,8 @@ class TransactionDAO(private val db: Database) {
stmt.setString(2, login)
stmt.oneOrNull {
BankAccountTransactionInfo(
- creditor_payto_uri = it.getString("creditor_payto_uri"),
- debtor_payto_uri = it.getString("debtor_payto_uri"),
+ creditor_payto_uri = it.getFullPayto("creditor_payto_uri", "creditor_name"),
+ debtor_payto_uri = it.getFullPayto("debtor_payto_uri", "debtor_name"),
amount = it.getAmount("amount", db.bankCurrency),
direction = TransactionDirection.valueOf(it.getString("direction")),
subject = it.getString("subject"),
@@ -184,7 +186,9 @@ class TransactionDAO(private val db: Database) {
,(amount).val AS amount_val
,(amount).frac AS amount_frac
,debtor_payto_uri
+ ,debtor_name
,creditor_payto_uri
+ ,creditor_name
,subject
,direction
FROM bank_account_transactions WHERE
@@ -192,8 +196,8 @@ class TransactionDAO(private val db: Database) {
BankAccountTransactionInfo(
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
- debtor_payto_uri = it.getString("debtor_payto_uri"),
- creditor_payto_uri = it.getString("creditor_payto_uri"),
+ creditor_payto_uri = it.getFullPayto("creditor_payto_uri", "creditor_name"),
+ debtor_payto_uri = it.getFullPayto("debtor_payto_uri", "debtor_name"),
amount = it.getAmount("amount", db.bankCurrency),
subject = it.getString("subject"),
direction = TransactionDirection.valueOf(it.getString("direction"))
@@ -213,6 +217,7 @@ class TransactionDAO(private val db: Database) {
,(amount).val AS amount_val
,(amount).frac AS amount_frac
,debtor_payto_uri
+ ,debtor_name
,subject
FROM bank_account_transactions WHERE direction='credit' AND
""") {
@@ -220,7 +225,7 @@ class TransactionDAO(private val db: Database) {
row_id = it.getLong("bank_transaction_id"),
date = it.getTalerTimestamp("transaction_date"),
amount = it.getAmount("amount", db.bankCurrency),
- debit_account = it.getString("debtor_payto_uri"),
+ debit_account = it.getFullPayto("debtor_payto_uri", "debtor_name"),
subject = it.getString("subject")
)
}
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -206,7 +206,7 @@ class CoreBankAccountsApiTest {
client.post("/accounts") {
json(req)
}.assertOkJson<RegisterAccountResponse> {
- assertEquals(IbanPayto.canonical, it.internal_payto_uri)
+ assertEquals(IbanPayto.withName("Jane").full, it.internal_payto_uri.full)
}
// Testing idempotency
client.post("/accounts") {
@@ -902,10 +902,10 @@ class CoreBankTransactionsApiTest {
}
}.assertConflict(TalerErrorCode.BANK_SAME_ACCOUNT)
// Transaction to admin
- val adminPayto = db.account.bankInfo("admin")!!.internalPaytoUri
+ val adminPayto = db.account.bankInfo("admin")!!.payto
client.postA("/accounts/merchant/transactions") {
json(valid_req) {
- "payto_uri" to "$adminPayto?message=payout"
+ "payto_uri" to "${adminPayto.payto}?message=payout"
}
}.assertConflict(TalerErrorCode.BANK_ADMIN_CREDITOR)
diff --git a/common/src/main/kotlin/DB.kt b/common/src/main/kotlin/DB.kt
@@ -337,4 +337,11 @@ fun ResultSet.getAmount(name: String, currency: String): TalerAmount{
getInt("${name}_frac"),
currency
)
+}
+
+fun ResultSet.getFullPayto(payto: String, name: String): FullIbanPayto{
+ return FullIbanPayto(
+ IbanPayto(getString(payto)),
+ getString(name),
+ )
}
\ No newline at end of file
diff --git a/common/src/main/kotlin/TalerCommon.kt b/common/src/main/kotlin/TalerCommon.kt
@@ -102,6 +102,31 @@ class TalerAmount {
}
}
+@JvmInline
+value class IBAN private constructor(val value: String) {
+ override fun toString(): String = value
+
+ companion object {
+ private val SEPARATOR = Regex("[\\ \\-]");
+
+ fun parse(raw: String): IBAN {
+ val iban: String = raw.uppercase().replace(SEPARATOR, "")
+ 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 CommonError.IbanPayto("Iban malformed, modulo is $mod expected 1")
+ return IBAN(iban)
+ }
+ }
+}
+
sealed class PaytoUri {
abstract val amount: TalerAmount?
@@ -115,11 +140,13 @@ sealed class PaytoUri {
class IbanPayto: PaytoUri {
val parsed: URI
val canonical: String
- val iban: String
+ val iban: IBAN
override val amount: TalerAmount?
override val message: String?
override val receiverName: String?
+ // TODO maybe add a fster builder that performs less expensive checks when the payto is from the database ?
+
constructor(raw: String) {
try {
parsed = URI(raw)
@@ -136,8 +163,7 @@ class IbanPayto: PaytoUri {
2 -> splitPath[1]
else -> throw CommonError.IbanPayto("too many path segments")
}
- iban = rawIban.uppercase().replace(SEPARATOR, "")
- checkIban(iban)
+ iban = IBAN.parse(rawIban)
canonical = "payto://iban/$iban"
val params = (parsed.query ?: "").parseUrlEncodedParameters();
@@ -146,20 +172,21 @@ class IbanPayto: PaytoUri {
receiverName = params["receiver-name"]
}
+ /** Full IBAN payto with receiver-name parameter set to [name] */
+ fun withName(name: String): FullIbanPayto = FullIbanPayto(this, name)
+
/** Full IBAN payto with receiver-name parameter if present */
fun maybeFull(): FullIbanPayto? {
- return FullIbanPayto(this, receiverName ?: return null)
+ return withName(receiverName ?: return null)
}
+ /** Full IBAN payto with receiver-name parameter set to [defaultName] if absent */
+ fun full(defaultName: String): FullIbanPayto = withName(receiverName ?: defaultName)
+
/** Full IBAN payto with receiver-name parameter if present, fail if absent */
fun requireFull(): FullIbanPayto {
return maybeFull() ?: throw Exception("Missing receiver-name")
- }
-
- /** Full IBAN payto with receiver-name parameter set to [defaultName] if absent */
- fun full(defaultName: String): FullIbanPayto {
- return FullIbanPayto(this, receiverName ?: defaultName)
- }
+ }
override fun toString(): String = canonical
@@ -175,26 +202,24 @@ 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 CommonError.IbanPayto("Iban malformed, modulo is $mod expected 1")
- }
- }
}
+@Serializable(with = FullIbanPayto.Serializer::class)
class FullIbanPayto(val payto: IbanPayto, val receiverName: String) {
val full = payto.canonical + "?receiver-name=" + receiverName.encodeURLParameter()
+
+ override fun toString(): String = full
+
+ internal object Serializer : KSerializer<FullIbanPayto> {
+ override val descriptor: SerialDescriptor =
+ PrimitiveSerialDescriptor("IbanPayto", PrimitiveKind.STRING)
+
+ override fun serialize(encoder: Encoder, value: FullIbanPayto) {
+ encoder.encodeString(value.full)
+ }
+
+ override fun deserialize(decoder: Decoder): FullIbanPayto {
+ return IbanPayto(decoder.decodeString()).requireFull()
+ }
+ }
}
\ No newline at end of file
diff --git a/common/src/test/kotlin/PaytoTest.kt b/common/src/test/kotlin/PaytoTest.kt
@@ -32,10 +32,10 @@ class PaytoTest {
@Test
fun parsePaytoTest() {
val withBic = IbanPayto("payto://iban/BIC123/CH9300762011623852957?receiver-name=The%20Name")
- assertEquals(withBic.iban, "CH9300762011623852957")
+ assertEquals(withBic.iban.value, "CH9300762011623852957")
assertEquals(withBic.receiverName, "The Name")
val complete = IbanPayto("payto://iban/BIC123/CH9300762011623852957?sender-name=The%20Name&amount=EUR:1&message=donation")
- assertEquals(withBic.iban, "CH9300762011623852957")
+ assertEquals(withBic.iban.value, "CH9300762011623852957")
assertEquals(withBic.receiverName, "The Name")
assertEquals(complete.message, "donation")
assertEquals(complete.amount.toString(), "EUR:1")
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt b/nexus/src/main/kotlin/tech/libeufin/nexus/Iso20022.kt
@@ -113,7 +113,7 @@ fun createPain001(
text(amountWithoutCurrency)
}
el("Cdtr/Nm", creditAccount.receiverName)
- el("CdtrAcct/Id/IBAN", creditAccount.payto.iban)
+ el("CdtrAcct/Id/IBAN", creditAccount.payto.iban.value)
el("RmtInf/Ustrd", wireTransferSubject)
}
}