commit 603925cadb993ccebef1fb5186aa102c09b62d06
parent 3679a4b32b777767857dd10b2b19740ce7469ddd
Author: Antoine A <>
Date: Thu, 23 Nov 2023 15:56:15 +0000
Handle missing conversion rate in the database
Diffstat:
6 files changed, 100 insertions(+), 49 deletions(-)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt
@@ -25,6 +25,7 @@ import io.ktor.server.response.*
import io.ktor.server.routing.*
import java.util.*
import tech.libeufin.util.*
+import tech.libeufin.bank.ConversionDAO.*
import net.taler.common.errorcodes.TalerErrorCode
fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allowConversion) {
@@ -39,6 +40,24 @@ fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allow
)
)
}
+ suspend fun ApplicationCall.convert(
+ input: TalerAmount,
+ conversion: suspend ConversionDAO.(TalerAmount) -> ConversionResult,
+ output: (TalerAmount) -> ConversionResponse
+ ) {
+ when (val res = db.conversion.(conversion)(input)) {
+ is ConversionResult.Success -> respond(output(res.converted))
+ is ConversionResult.ToSmall -> throw conflict(
+ "$input is too small to be converted",
+ TalerErrorCode.BANK_BAD_CONVERSION
+ )
+ is ConversionResult.MissingConfig -> throw libeufinError(
+ HttpStatusCode.NotImplemented,
+ "conversion rate not configured yet",
+ TalerErrorCode.END
+ )
+ }
+ }
get("/conversion-info/cashout-rate") {
val params = RateParams.extract(call.request.queryParameters)
@@ -46,40 +65,29 @@ fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allow
params.credit?.let { ctx.checkFiatCurrency(it) }
if (params.debit != null) {
- val credit = db.conversion.toCashout(params.debit) ?:
- throw conflict(
- "${params.debit} is too small to be converted",
- TalerErrorCode.BANK_BAD_CONVERSION
- )
- call.respond(ConversionResponse(params.debit, credit))
+ call.convert(params.debit, ConversionDAO::toCashout) {
+ ConversionResponse(params.debit, it)
+ }
} else {
- val debit = db.conversion.fromCashout(params.credit!!) ?:
- throw conflict(
- "${params.debit} is too small to be converted",
- TalerErrorCode.BANK_BAD_CONVERSION
- )
- call.respond(ConversionResponse(debit, params.credit))
+ call.convert(params.credit!!, ConversionDAO::fromCashout) {
+ ConversionResponse(it, params.credit)
+ }
}
}
get("/conversion-info/cashin-rate") {
val params = RateParams.extract(call.request.queryParameters)
+ params.debit?.let { ctx.checkFiatCurrency(it) }
+ params.credit?.let { ctx.checkRegionalCurrency(it) }
+
if (params.debit != null) {
- ctx.checkFiatCurrency(params.debit)
- val credit = db.conversion.toCashin(params.debit) ?:
- throw conflict(
- "${params.debit} is too small to be converted",
- TalerErrorCode.BANK_BAD_CONVERSION
- )
- call.respond(ConversionResponse(params.debit, credit))
+ call.convert(params.debit, ConversionDAO::toCashin) {
+ ConversionResponse(params.debit, it)
+ }
} else {
- ctx.checkRegionalCurrency(params.credit!!)
- val debit = db.conversion.fromCashin(params.credit) ?:
- throw conflict(
- "${params.credit} is too small to be converted",
- TalerErrorCode.BANK_BAD_CONVERSION
- )
- call.respond(ConversionResponse(debit, params.credit))
+ call.convert(params.credit!!, ConversionDAO::fromCashin) {
+ ConversionResponse(it, params.credit)
+ }
}
}
}
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -61,28 +61,43 @@ class ConversionDAO(private val db: Database) {
}
}
+ /** Clear in-db conversion config */
+ suspend fun clearConfig() = db.serializable { conn ->
+ conn.prepareStatement("DELETE FROM config WHERE key LIKE 'cashin%' OR key like 'cashout%'").executeUpdate()
+ }
+
+ /** Result of conversions operations */
+ sealed class ConversionResult {
+ data class Success(val converted: TalerAmount): ConversionResult()
+ object ToSmall: ConversionResult()
+ object MissingConfig: ConversionResult()
+ }
+
/** Perform [direction] conversion of [amount] using in-db [function] */
- private suspend fun conversion(amount: TalerAmount, direction: String, function: String): TalerAmount? = db.conn { conn ->
- val stmt = conn.prepareStatement("SELECT too_small, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?)")
+ private suspend fun conversion(amount: TalerAmount, direction: String, function: String): ConversionResult = db.conn { conn ->
+ val stmt = conn.prepareStatement("SELECT too_small, no_config, (converted).val AS amount_val, (converted).frac AS amount_frac FROM $function((?, ?)::taler_amount, ?)")
stmt.setLong(1, amount.value)
stmt.setInt(2, amount.frac)
stmt.setString(3, direction)
stmt.executeQuery().use {
- it.next()
- if (!it.getBoolean("too_small")) {
- it.getAmount("amount", if (amount.currency == db.bankCurrency) db.fiatCurrency!! else db.bankCurrency)
- } else {
- null
+ when {
+ !it.next() ->
+ throw internalServerError("No result from DB procedure $function")
+ it.getBoolean("no_config") -> ConversionResult.MissingConfig
+ it.getBoolean("too_small") -> ConversionResult.ToSmall
+ else -> ConversionResult.Success(
+ it.getAmount("amount", if (amount.currency == db.bankCurrency) db.fiatCurrency!! else db.bankCurrency)
+ )
}
}
}
-
+
/** Convert [regional] amount to fiat using cashout rate */
- suspend fun toCashout(regional: TalerAmount): TalerAmount? = conversion(regional, "cashout", "conversion_to")
+ suspend fun toCashout(regional: TalerAmount): ConversionResult = conversion(regional, "cashout", "conversion_to")
/** Convert [fiat] amount to regional using cashin rate */
- suspend fun toCashin(fiat: TalerAmount): TalerAmount? = conversion(fiat, "cashin", "conversion_to")
+ suspend fun toCashin(fiat: TalerAmount): ConversionResult = conversion(fiat, "cashin", "conversion_to")
/** Convert [fiat] amount to regional using inverse cashout rate */
- suspend fun fromCashout(fiat: TalerAmount): TalerAmount? = conversion(fiat, "cashout", "conversion_from")
+ suspend fun fromCashout(fiat: TalerAmount): ConversionResult = conversion(fiat, "cashout", "conversion_from")
/** Convert [regional] amount to fiat using inverse cashin rate */
- suspend fun fromCashin(regional: TalerAmount): TalerAmount? = conversion(regional, "cashin", "conversion_from")
+ suspend fun fromCashin(regional: TalerAmount): ConversionResult = conversion(regional, "cashin", "conversion_from")
}
\ No newline at end of file
diff --git a/bank/src/test/kotlin/ConversionApiTest.kt b/bank/src/test/kotlin/ConversionApiTest.kt
@@ -109,6 +109,19 @@ class ConversionApiTest {
}
@Test
+ fun noRate() = bankSetup { db ->
+ db.conversion.clearConfig()
+ client.get("/conversion-info/cashin-rate")
+ .assertBadRequest()
+ client.get("/conversion-info/cashout-rate")
+ .assertBadRequest()
+ client.get("/conversion-info/cashin-rate?amount_credit=KUDOS:1")
+ .assertNotImplemented()
+ client.get("/conversion-info/cashout-rate?amount_credit=EUR:1")
+ .assertNotImplemented()
+ }
+
+ @Test
fun notImplemented() = bankSetup("test_restrict.conf") { _ ->
client.get("/conversion-info/cashin-rate")
.assertNotImplemented()
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -975,6 +975,7 @@ CREATE OR REPLACE FUNCTION cashin(
-- Error status
OUT out_no_account BOOLEAN,
OUT out_too_small BOOLEAN,
+ OUT out_no_config BOOLEAN,
OUT out_balance_insufficient BOOLEAN
)
LANGUAGE plpgsql AS $$
@@ -1002,10 +1003,10 @@ SELECT bank_account_id
WHERE login = 'admin';
-- Perform conversion
-SELECT (converted).val, (converted).frac, too_small
- INTO converted_amount.val, converted_amount.frac, out_too_small
+SELECT (converted).val, (converted).frac, too_small, no_config
+ INTO converted_amount.val, converted_amount.frac, out_too_small, out_no_config
FROM conversion_to(in_amount, 'cashin'::text);
-IF out_too_small THEN
+IF out_too_small OR out_no_config THEN
RETURN;
END IF;
@@ -1061,7 +1062,7 @@ account_id BIGINT;
challenge_id BIGINT;
BEGIN
-- check conversion
-SELECT too_small OR in_amount_credit!=converted INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'cashout'::text);
+SELECT too_small OR no_config OR in_amount_credit!=converted INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'cashout'::text);
IF out_bad_conversion THEN
RETURN;
END IF;
@@ -1183,7 +1184,7 @@ ELSIF already_confirmed OR out_aborted OR out_no_cashout_payto THEN
END IF;
-- check conversion
-SELECT too_small OR amount_credit_local!=converted INTO out_bad_conversion FROM conversion_to(amount_debit_local, 'cashout'::text);
+SELECT too_small OR no_config OR amount_credit_local!=converted INTO out_bad_conversion FROM conversion_to(amount_debit_local, 'cashout'::text);
IF out_bad_conversion THEN
RETURN;
END IF;
@@ -1517,7 +1518,8 @@ CREATE OR REPLACE FUNCTION conversion_to(
IN amount taler_amount,
IN direction TEXT,
OUT converted taler_amount,
- OUT too_small BOOLEAN
+ OUT too_small BOOLEAN,
+ OUT no_config BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
@@ -1529,6 +1531,10 @@ DECLARE
BEGIN
-- Check min amount
SELECT value['val']::int8, value['frac']::int4 INTO min_amount.val, min_amount.frac FROM config WHERE key=direction||'_min_amount';
+ IF NOT FOUND THEN
+ no_config = true;
+ RETURN;
+ END IF;
SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount);
IF too_small THEN
converted = (0, 0);
@@ -1553,7 +1559,8 @@ CREATE OR REPLACE FUNCTION conversion_from(
IN amount taler_amount,
IN direction TEXT,
OUT converted taler_amount,
- OUT too_small BOOLEAN
+ OUT too_small BOOLEAN,
+ OUT no_config BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
@@ -1568,7 +1575,10 @@ BEGIN
SELECT value['val']::int8, value['frac']::int4 INTO out_fee.val, out_fee.frac FROM config WHERE key=direction||'_fee';
SELECT value['val']::int8, value['frac']::int4 INTO tiny_amount.val, tiny_amount.frac FROM config WHERE key=direction||'_tiny_amount';
SELECT (value->>'mode')::rounding_mode INTO mode FROM config WHERE key=direction||'_rounding_mode';
-
+ IF NOT FOUND THEN
+ no_config = true;
+ RETURN;
+ END IF;
SELECT result.val, result.frac INTO converted.val, converted.frac
FROM conversion_revert_ratio(amount_add(amount, out_fee), at_ratio, tiny_amount, mode) as result;
diff --git a/database-versioning/libeufin-conversion-setup.sql b/database-versioning/libeufin-conversion-setup.sql
@@ -48,14 +48,15 @@ LANGUAGE plpgsql AS $$
too_small BOOLEAN;
balance_insufficient BOOLEAN;
no_account BOOLEAN;
+ no_config BOOLEAN;
BEGIN
SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time, debit_payto_uri
INTO local_amount.val, local_amount.frac, subject, now_date, payto_uri
FROM libeufin_nexus.incoming_transactions
WHERE incoming_transaction_id = NEW.incoming_transaction_id;
SET search_path TO libeufin_bank;
- SELECT out_too_small, out_balance_insufficient, out_no_account
- INTO too_small, balance_insufficient, no_account
+ SELECT out_too_small, out_balance_insufficient, out_no_account, out_no_config
+ INTO too_small, balance_insufficient, no_account, no_config
FROM libeufin_bank.cashin(now_date, payto_uri, local_amount, subject);
SET search_path TO libeufin_nexus;
@@ -66,7 +67,10 @@ LANGUAGE plpgsql AS $$
RAISE EXCEPTION 'TODO soft error bounce: too small amount';
END IF;
IF balance_insufficient THEN
- RAISE EXCEPTION 'TODO hard error bounce';
+ RAISE EXCEPTION 'TODO hard error bounce: admin balance insufficient';
+ END IF;
+ IF no_config THEN
+ RAISE EXCEPTION 'TODO hard error bounce: missing conversion rate config';
END IF;
RETURN NEW;
END;
diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt
@@ -72,6 +72,7 @@ class IntegrationTest {
kotlin.concurrent.thread(isDaemon = true) {
bankCmd.run("serve -c ../bank/conf/test.conf")
}
+
runBlocking {
val client = HttpClient(CIO) {
install(HttpRequestRetry) {