libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit 2bc6907cb1f3b1bb6b207e85294bfa57c28bb2e9
parent 41628115dcac19c5f1532fdafb06e361a409fa8a
Author: Antoine A <>
Date:   Fri, 10 Nov 2023 11:35:18 +0000

Improve conversion endpoints errors  and use internal and external names everywhere

Diffstat:
Mbank/conf/test.conf | 2+-
Mbank/conf/test_no_tan.conf | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/Config.kt | 16++++++++--------
Mbank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt | 12++++++------
Mbank/src/main/kotlin/tech/libeufin/bank/Error.kt | 6+++---
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 4++--
Mbank/src/main/kotlin/tech/libeufin/bank/Params.kt | 18++++++++++--------
Mbank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt | 2+-
Mbank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt | 4++--
Mbank/src/main/kotlin/tech/libeufin/bank/db/Database.kt | 4++--
Mbank/src/test/kotlin/CoreBankApiTest.kt | 24+++++++++++++++++++++---
Mbank/src/test/kotlin/helpers.kt | 2+-
Mdatabase-versioning/libeufin-bank-0001.sql | 2+-
14 files changed, 60 insertions(+), 40 deletions(-)

diff --git a/bank/conf/test.conf b/bank/conf/test.conf @@ -5,7 +5,7 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com have_cashout = YES -fiat_currency = FIAT +external_currency = FIAT tan_sms = libeufin-tan-file.sh tan_email = libeufin-tan-fail.sh diff --git a/bank/conf/test_no_tan.conf b/bank/conf/test_no_tan.conf @@ -5,7 +5,7 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000 REGISTRATION_BONUS_ENABLED = NO SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com have_cashout = YES -fiat_currency = FIAT +external_currency = FIAT [libeufin-bankdb-postgres] SQL_DIR = $DATADIR/sql/ diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt @@ -78,7 +78,7 @@ data class BankConfig( */ val spaCaptchaURL: String?, val haveCashout: Boolean, - val fiatCurrency: String?, + val externalCurrency: String?, val conversionInfo: ConversionInfo?, val tanSms: String?, val tanEmail: String?, @@ -128,12 +128,12 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { val currencySpecification = sections.find { it.startsWith("CURRENCY-") && requireBoolean(it, "enabled") && requireString(it, "code") == currency }?.let { loadCurrencySpecification(it) } ?: throw TalerConfigError("missing currency specification for $currency") - var fiatCurrency: String? = null; + var externalCurrency: String? = null; var conversionInfo: ConversionInfo? = null; val haveCashout = lookupBoolean("libeufin-bank", "have_cashout") ?: false; if (haveCashout) { - fiatCurrency = requireString("libeufin-bank", "fiat_currency"); - conversionInfo = loadConversionInfo(currency, fiatCurrency) + externalCurrency = requireString("libeufin-bank", "external_currency"); + conversionInfo = loadConversionInfo(currency, externalCurrency) } BankConfig( currency = currency, @@ -147,23 +147,23 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError { restrictAccountDeletion = lookupBoolean("libeufin-bank", "restrict_account_deletion") ?: true, currencySpecification = currencySpecification, haveCashout = haveCashout, - fiatCurrency = fiatCurrency, + externalCurrency = externalCurrency, conversionInfo = conversionInfo, tanSms = lookupPath("libeufin-bank", "tan_sms"), tanEmail = lookupPath("libeufin-bank", "tan_email"), ) } -private fun TalerConfig.loadConversionInfo(currency: String, fiatCurrency: String): ConversionInfo = catchError { +private fun TalerConfig.loadConversionInfo(currency: String, externalCurrency: String): ConversionInfo = catchError { ConversionInfo( buy_ratio = requireDecimalNumber("libeufin-bank-conversion", "buy_ratio"), buy_fee = requireDecimalNumber("libeufin-bank-conversion", "buy_fee"), buy_tiny_amount = amount("libeufin-bank-conversion", "buy_tiny_amount", currency) ?: TalerAmount(0, 1, currency), buy_rounding_mode = RoundingMode("libeufin-bank-conversion", "buy_rounding_mode") ?: RoundingMode.zero, - buy_min_amount = amount("libeufin-bank-conversion", "buy_min_amount", fiatCurrency) ?: TalerAmount(0, 0, fiatCurrency), + buy_min_amount = amount("libeufin-bank-conversion", "buy_min_amount", externalCurrency) ?: TalerAmount(0, 0, externalCurrency), sell_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_ratio"), sell_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_fee"), - sell_tiny_amount = amount("libeufin-bank-conversion", "sell_tiny_amount", fiatCurrency) ?: TalerAmount(0, 1, fiatCurrency), + sell_tiny_amount = amount("libeufin-bank-conversion", "sell_tiny_amount", externalCurrency) ?: TalerAmount(0, 1, externalCurrency), sell_rounding_mode = RoundingMode("libeufin-bank-conversion", "sell_rounding_mode") ?: RoundingMode.zero, sell_min_amount = amount("libeufin-bank-conversion", "sell_min_amount", currency) ?: TalerAmount(0, 0, currency), ) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/CoreBankApi.kt @@ -47,7 +47,7 @@ fun Routing.coreBankApi(db: Database, ctx: BankConfig) { Config( currency = ctx.currencySpecification, have_cashout = ctx.haveCashout, - fiat_currency = ctx.fiatCurrency, + external_currency = ctx.externalCurrency, conversion_info = ctx.conversionInfo, allow_registrations = !ctx.restrictRegistration, allow_deletions = !ctx.restrictAccountDeletion @@ -476,7 +476,7 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { val req = call.receive<CashoutRequest>() ctx.checkInternalCurrency(req.amount_debit) - ctx.checkFiatCurrency(req.amount_credit) + ctx.checkexternalCurrency(req.amount_credit) val tanChannel = req.tan_channel ?: TanChannel.sms val tanScript = when (tanChannel) { @@ -639,10 +639,10 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { val params = RateParams.extract(call.request.queryParameters) params.debit?.let { ctx.checkInternalCurrency(it) } - params.credit?.let { ctx.checkFiatCurrency(it) } + params.credit?.let { ctx.checkexternalCurrency(it) } if (params.debit != null) { - val credit = db.conversion.internalToFiat(params.debit) ?: + val credit = db.conversion.internalToExternal(params.debit) ?: throw conflict( "${params.debit} is too small to be converted", TalerErrorCode.BANK_BAD_CONVERSION @@ -655,11 +655,11 @@ private fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) { get("/cashin-rate") { val params = RateParams.extract(call.request.queryParameters) - params.debit?.let { ctx.checkFiatCurrency(it) } + params.debit?.let { ctx.checkexternalCurrency(it) } params.credit?.let { ctx.checkInternalCurrency(it) } if (params.debit != null) { - val credit = db.conversion.fiatToInternal(params.debit) ?: + val credit = db.conversion.externalToInternal(params.debit) ?: throw conflict( "${params.debit} is too small to be converted", TalerErrorCode.BANK_BAD_CONVERSION diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Error.kt b/bank/src/main/kotlin/tech/libeufin/bank/Error.kt @@ -91,9 +91,9 @@ fun BankConfig.checkInternalCurrency(amount: TalerAmount) { ) } -fun BankConfig.checkFiatCurrency(amount: TalerAmount) { - if (amount.currency != fiatCurrency) throw badRequest( - "Wrong currency: expected fiat currency $fiatCurrency got ${amount.currency}", +fun BankConfig.checkexternalCurrency(amount: TalerAmount) { + if (amount.currency != externalCurrency) throw badRequest( + "Wrong currency: expected external currency $externalCurrency got ${amount.currency}", TalerErrorCode.GENERIC_CURRENCY_MISMATCH ) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -303,7 +303,7 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") logger.info("Can only serve libeufin-bank via TCP") exitProcess(1) } - val db = Database(dbCfg.dbConnStr, ctx.currency, ctx.fiatCurrency) + val db = Database(dbCfg.dbConnStr, ctx.currency, ctx.externalCurrency) runBlocking { if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) @@ -326,7 +326,7 @@ class ChangePw : CliktCommand("Change account password", name = "passwd") { val cfg = talerConfig(configFile) val ctx = cfg.loadBankConfig() val dbCfg = cfg.loadDbConfig() - val db = Database(dbCfg.dbConnStr, ctx.currency, ctx.fiatCurrency) + val db = Database(dbCfg.dbConnStr, ctx.currency, ctx.externalCurrency) runBlocking { if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper exitProcess(1) diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Params.kt b/bank/src/main/kotlin/tech/libeufin/bank/Params.kt @@ -36,21 +36,21 @@ import java.time.temporal.* import java.util.* fun Parameters.expect(name: String): String - = get(name) ?: throw badRequest("Missing '$name' parameter") + = get(name) ?: throw badRequest("Missing '$name' parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) fun Parameters.int(name: String): Int? - = get(name)?.run { toIntOrNull() ?: throw badRequest("Param 'which' not a number") } + = get(name)?.run { toIntOrNull() ?: throw badRequest("Param 'which' not a number", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) } fun Parameters.expectInt(name: String): Int - = int(name) ?: throw badRequest("Missing '$name' number parameter") + = int(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) fun Parameters.long(name: String): Long? - = get(name)?.run { toLongOrNull() ?: throw badRequest("Param 'which' not a number") } + = get(name)?.run { toLongOrNull() ?: throw badRequest("Param 'which' not a number", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) } fun Parameters.expectLong(name: String): Long - = long(name) ?: throw badRequest("Missing '$name' number parameter") + = long(name) ?: throw badRequest("Missing '$name' number parameter", TalerErrorCode.GENERIC_PARAMETER_MISSING) fun Parameters.amount(name: String): TalerAmount? = get(name)?.run { try { TalerAmount(this) } catch (e: Exception) { - throw badRequest("Param '$name' not a taler amount") + throw badRequest("Param '$name' not a taler amount", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) } } @@ -114,8 +114,10 @@ data class RateParams( val debit = params.amount("amount_debit") val credit = params.amount("amount_credit") if (debit == null && credit == null) { - throw badRequest("Either param 'amount_debit' or 'amount_credit' is required") - } + throw badRequest("Either param 'amount_debit' or 'amount_credit' is required", TalerErrorCode.GENERIC_PARAMETER_MISSING) + } else if (debit != null && credit != null) { + throw badRequest("Cannot have both 'amount_debit' and 'amount_credit' params", TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + } return RateParams(debit, credit) } } diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerMessage.kt @@ -254,7 +254,7 @@ data class TalerWithdrawalOperation( data class Config( val currency: CurrencySpecification, val have_cashout: Boolean, - val fiat_currency: String?, + val external_currency: String?, val conversion_info: ConversionInfo?, val allow_registrations: Boolean, val allow_deletions: Boolean diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/CashoutDAO.kt @@ -234,7 +234,7 @@ class CashoutDAO(private val db: Database) { amount_credit = TalerAmount( value = it.getLong("amount_credit_val"), frac = it.getInt("amount_credit_frac"), - db.fiatCurrency!! + db.externalCurrency!! ), subject = it.getString("subject"), creation_time = TalerProtocolTimestamp( diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt @@ -82,6 +82,6 @@ class ConversionDAO(private val db: Database) { } } - suspend fun internalToFiat(amount: TalerAmount): TalerAmount? = conversion(amount, "sell", db.fiatCurrency!!) - suspend fun fiatToInternal(amount: TalerAmount): TalerAmount? = conversion(amount, "buy", db.bankCurrency) + suspend fun internalToExternal(amount: TalerAmount): TalerAmount? = conversion(amount, "sell", db.externalCurrency!!) + suspend fun externalToInternal(amount: TalerAmount): TalerAmount? = conversion(amount, "buy", db.bankCurrency) } \ No newline at end of file diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/Database.kt @@ -55,7 +55,7 @@ private val logger: Logger = LoggerFactory.getLogger("tech.libeufin.bank.Databas internal fun faultyTimestampByBank() = internalServerError("Bank took overflowing timestamp") internal fun faultyDurationByClient() = badRequest("Overflowing duration, please specify 'forever' instead.") -class Database(dbConfig: String, internal val bankCurrency: String, internal val fiatCurrency: String?): java.io.Closeable { +class Database(dbConfig: String, internal val bankCurrency: String, internal val externalCurrency: String?): java.io.Closeable { val dbPool: HikariDataSource internal val notifWatcher: NotificationWatcher @@ -871,7 +871,7 @@ class Database(dbConfig: String, internal val bankCurrency: String, internal val stmt.setNull(2, java.sql.Types.INTEGER) } stmt.oneOrNull { - fiatCurrency?.run { + externalCurrency?.run { MonitorWithConversion( cashinCount = it.getLong("cashin_count"), cashinInternalVolume = TalerAmount( diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt @@ -1506,11 +1506,20 @@ class CoreBankCashoutApiTest { assertEquals(TalerAmount("FIAT:1.247"), resp.amount_credit) } - // No amount - client.get("/cashout-rate").assertBadRequest() // Too small client.get("/cashout-rate?amount_debit=KUDOS:0.08") .assertConflict(TalerErrorCode.BANK_BAD_CONVERSION) + // No amount + client.get("/cashout-rate") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) + // Both amount + client.get("/cashout-rate?amount_debit=FIAT:1&amount_credit=KUDOS:1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + // Wrong format + client.get("/cashout-rate?amount_debit=1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + client.get("/cashout-rate?amount_credit=1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // Wrong currency client.get("/cashout-rate?amount_debit=FIAT:1") .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) @@ -1532,7 +1541,16 @@ class CoreBankCashoutApiTest { } // No amount - client.get("/cashin-rate").assertBadRequest() + client.get("/cashin-rate") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MISSING) + // Both amount + client.get("/cashin-rate?amount_debit=KUDOS:1&amount_credit=FIAT:1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + // Wrong format + client.get("/cashin-rate?amount_debit=1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) + client.get("/cashin-rate?amount_credit=1") + .assertBadRequest(TalerErrorCode.GENERIC_PARAMETER_MALFORMED) // Wrong currency client.get("/cashin-rate?amount_debit=KUDOS:1") .assertBadRequest(TalerErrorCode.GENERIC_CURRENCY_MISMATCH) diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt @@ -25,7 +25,7 @@ fun setup( resetDatabaseTables(dbCfg, "libeufin-bank") initializeDatabaseTables(dbCfg, "libeufin-bank") val ctx = config.loadBankConfig() - Database(dbCfg.dbConnStr, ctx.currency, ctx.fiatCurrency).use { + Database(dbCfg.dbConnStr, ctx.currency, ctx.externalCurrency).use { runBlocking { ctx.conversionInfo?.run { it.conversion.updateConfig(this) } lambda(it, ctx) diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS customers ); 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.'; + IS 'RFC 8905 payto URI to collect external payments that come from the conversion of internal currency cash-out operations.'; COMMENT ON COLUMN customers.name IS 'Full name of the customer.';