commit 48cfa63e024f3b7a8b5c0473cf982fe87a2ee673
parent 9439260e0b60b5a5fa4a1d636e9f0075396a6b76
Author: Antoine A <>
Date: Tue, 31 Oct 2023 16:27:23 +0000
Add min_amount to conversion info
Diffstat:
6 files changed, 74 insertions(+), 42 deletions(-)
diff --git a/bank/conf/test.conf b/bank/conf/test.conf
@@ -12,9 +12,10 @@ SQL_DIR = $DATADIR/sql/
CONFIG = postgresql:///libeufincheck
[libeufin-bank-conversion]
-buy_at_ratio = 0.8
-buy_in_fee = 0.02
+buy_ratio = 0.8
+buy_fee = 0.02
buy_tiny_amount = KUDOS:0.01
buy_rounding_mode = nearest
-sell_at_ratio = 1.25
-sell_out_fee = 0.003
-\ No newline at end of file
+sell_ratio = 1.25
+sell_fee = 0.003
+sell_min_amount = KUDOS:0.1
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -84,14 +84,16 @@ data class BankConfig(
@Serializable
data class ConversionInfo (
- val buy_at_ratio: DecimalNumber,
- val buy_in_fee: DecimalNumber,
+ val buy_ratio: DecimalNumber,
+ val buy_fee: DecimalNumber,
val buy_tiny_amount: TalerAmount,
val buy_rounding_mode: RoundingMode,
- val sell_at_ratio: DecimalNumber,
- val sell_out_fee: DecimalNumber,
+ val buy_min_amount: TalerAmount,
+ val sell_ratio: DecimalNumber,
+ val sell_fee: DecimalNumber,
val sell_tiny_amount: TalerAmount,
val sell_rounding_mode: RoundingMode,
+ val sell_min_amount: TalerAmount,
)
data class ServerConfig(
@@ -150,14 +152,16 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError {
private fun TalerConfig.loadConversionInfo(currency: String, fiatCurrency: String): ConversionInfo = catchError {
ConversionInfo(
- buy_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "buy_at_ratio"),
- buy_in_fee = requireDecimalNumber("libeufin-bank-conversion", "buy_in_fee"),
+ 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,
- sell_at_ratio = requireDecimalNumber("libeufin-bank-conversion", "sell_at_ratio"),
- sell_out_fee = requireDecimalNumber("libeufin-bank-conversion", "sell_out_fee"),
+ buy_min_amount = amount("libeufin-bank-conversion", "buy_min_amount", fiatCurrency) ?: TalerAmount(0, 0, fiatCurrency),
+ 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_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
@@ -567,7 +567,11 @@ fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) {
params.credit?.let { ctx.checkFiatCurrency(it) }
if (params.debit != null) {
- val credit = db.conversionInternalToFiat(params.debit)
+ val credit = db.conversionInternalToFiat(params.debit) ?:
+ throw conflict(
+ "${params.debit} is too small to be converted",
+ TalerErrorCode.BANK_BAD_CONVERSION
+ )
call.respond(ConversionResponse(params.debit, credit))
} else {
call.respond(HttpStatusCode.NotImplemented) // TODO
@@ -580,7 +584,11 @@ fun Routing.coreBankCashoutApi(db: Database, ctx: BankConfig) {
params.credit?.let { ctx.checkInternalCurrency(it) }
if (params.debit != null) {
- val credit = db.conversionFiatToInternal(params.debit)
+ val credit = db.conversionFiatToInternal(params.debit) ?:
+ throw conflict(
+ "${params.debit} is too small to be converted",
+ TalerErrorCode.BANK_BAD_CONVERSION
+ )
call.respond(ConversionResponse(params.debit, credit))
} else {
call.respond(HttpStatusCode.NotImplemented) // TODO
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -1544,10 +1544,10 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
it.transaction { conn ->
var stmt = conn.prepareStatement("CALL config_set_amount(?, (?, ?)::taler_amount)")
for ((name, amount) in listOf(
- Pair("buy_ratio", cfg.buy_at_ratio),
- Pair("buy_fee", cfg.buy_in_fee),
- Pair("sell_ratio", cfg.sell_at_ratio),
- Pair("sell_fee", cfg.sell_out_fee),
+ Pair("buy_ratio", cfg.buy_ratio),
+ Pair("buy_fee", cfg.buy_fee),
+ Pair("sell_ratio", cfg.sell_ratio),
+ Pair("sell_fee", cfg.sell_fee),
)) {
stmt.setString(1, name)
stmt.setLong(2, amount.value)
@@ -1556,7 +1556,9 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
}
for ((name, amount) in listOf(
Pair("buy_tiny_amount", cfg.buy_tiny_amount),
- Pair("sell_tiny_amount", cfg.sell_tiny_amount)
+ Pair("buy_min_amount", cfg.buy_min_amount),
+ Pair("sell_tiny_amount", cfg.sell_tiny_amount),
+ Pair("sell_min_amount", cfg.sell_min_amount),
)) {
stmt.setString(1, name)
stmt.setLong(2, amount.value)
@@ -1575,22 +1577,27 @@ class Database(dbConfig: String, private val bankCurrency: String, private val f
}
}
- private suspend fun conversionTo(amount: TalerAmount, name: String, currency: String): TalerAmount = conn { conn ->
- val stmt = conn.prepareStatement("SELECT amount.val AS amount_val, amount.frac AS amount_frac FROM conversion_to((?, ?)::taler_amount, ?) as amount")
+ private suspend fun conversionTo(amount: TalerAmount, name: String, currency: String): TalerAmount? = conn { conn ->
+ val stmt = conn.prepareStatement("SELECT too_small, (to_amount).val AS amount_val, (to_amount).frac AS amount_frac FROM conversion_to((?, ?)::taler_amount, ?)")
stmt.setLong(1, amount.value)
stmt.setInt(2, amount.frac)
stmt.setString(3, name)
- stmt.oneOrNull {
- TalerAmount(
- value = it.getLong("amount_val"),
- frac = it.getInt("amount_frac"),
- currency = currency
- )
- }!!
+ stmt.executeQuery().use {
+ it.next()
+ if (!it.getBoolean("too_small")) {
+ TalerAmount(
+ value = it.getLong("amount_val"),
+ frac = it.getInt("amount_frac"),
+ currency = currency
+ )
+ } else {
+ null
+ }
+ }
}
- suspend fun conversionInternalToFiat(amount: TalerAmount): TalerAmount = conversionTo(amount, "sell", fiatCurrency!!)
- suspend fun conversionFiatToInternal(amount: TalerAmount): TalerAmount = conversionTo(amount, "buy", bankCurrency)
+ suspend fun conversionInternalToFiat(amount: TalerAmount): TalerAmount? = conversionTo(amount, "sell", fiatCurrency!!)
+ suspend fun conversionFiatToInternal(amount: TalerAmount): TalerAmount? = conversionTo(amount, "buy", bankCurrency)
}
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -1259,10 +1259,13 @@ class CoreBankCashoutApiTest {
// No amount
client.get("/cashout-rate").assertBadRequest()
+ // Too small
+ client.get("/cashout-rate?amount_debit=KUDOS:0.08")
+ .assertConflict().assertErr(TalerErrorCode.BANK_BAD_CONVERSION)
// Wrong currency
- client.get("/cashout-rate?amount_debit=FIAT:1").assertBadRequest()
+ client.get("/cashout-rate?amount_debit=FIAT:1")
.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
- client.get("/cashout-rate?amount_credit=KUDOS:1").assertBadRequest()
+ client.get("/cashout-rate?amount_credit=KUDOS:1")
.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
}
@@ -1282,9 +1285,9 @@ class CoreBankCashoutApiTest {
// No amount
client.get("/cashin-rate").assertBadRequest()
// Wrong currency
- client.get("/cashin-rate?amount_debit=KUDOS:1").assertBadRequest()
+ client.get("/cashin-rate?amount_debit=KUDOS:1")
.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
- client.get("/cashin-rate?amount_credit=FIAT:1").assertBadRequest()
+ client.get("/cashin-rate?amount_credit=FIAT:1")
.assertBadRequest().assertErr(TalerErrorCode.GENERIC_CURRENCY_MISMATCH)
}
}
\ No newline at end of file
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -1053,7 +1053,7 @@ DECLARE
account_id BIGINT;
BEGIN
-- check conversion
-SELECT in_amount_credit!=expected INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'sell'::text) AS expected;
+SELECT too_small OR in_amount_credit!=to_amount INTO out_bad_conversion FROM conversion_to(in_amount_debit, 'sell'::text);
IF out_bad_conversion THEN
RETURN;
END IF;
@@ -1284,26 +1284,36 @@ $$;
CREATE OR REPLACE FUNCTION conversion_to(
IN from_amount taler_amount,
IN name TEXT,
- OUT to_amount taler_amount
+ OUT to_amount taler_amount,
+ OUT too_small BOOLEAN
)
LANGUAGE plpgsql AS $$
DECLARE
at_ratio taler_amount;
out_fee taler_amount;
tiny_amount taler_amount;
+ min_amount taler_amount;
mode rounding_mode;
- calculation_ok BOOLEAN;
BEGIN
+ -- Check min amount
+ SELECT value['val']::int8, value['frac']::int4 INTO min_amount.val, min_amount.frac FROM config WHERE key=name||'_min_amount';
+ SELECT NOT ok INTO too_small FROM amount_left_minus_right(from_amount, min_amount);
+ IF too_small THEN
+ to_amount = (0, 0);
+ RETURN;
+ END IF;
+
+ -- Perform conversion
SELECT value['val']::int8, value['frac']::int4 INTO at_ratio.val, at_ratio.frac FROM config WHERE key=name||'_ratio';
SELECT value['val']::int8, value['frac']::int4 INTO out_fee.val, out_fee.frac FROM config WHERE key=name||'_fee';
SELECT value['val']::int8, value['frac']::int4 INTO tiny_amount.val, tiny_amount.frac FROM config WHERE key=name||'_tiny_amount';
SELECT (value->>'mode')::rounding_mode INTO mode FROM config WHERE key=name||'_rounding_mode';
- -- TODO minimum amount
+
SELECT product.val, product.frac INTO to_amount.val, to_amount.frac FROM amount_mul(from_amount, at_ratio, tiny_amount, mode) as product;
- SELECT (diff).val, (diff).frac, ok INTO to_amount.val, to_amount.frac, calculation_ok FROM amount_left_minus_right(to_amount, out_fee);
+ SELECT (diff).val, (diff).frac, NOT ok INTO to_amount.val, to_amount.frac, too_small FROM amount_left_minus_right(to_amount, out_fee);
- IF NOT calculation_ok THEN
- to_amount = (0, 0); -- TODO how to handle zero and less than zero ?
+ IF too_small THEN
+ to_amount = (0, 0);
END IF;
END $$;