commit dedac1f5c5404a68bec600aa49030851e2ef13a0
parent 844be31478752257916a9c2edca232854f225965
Author: Antoine A <>
Date: Fri, 24 Nov 2023 15:05:08 +0000
Set conversion rates through REST API
Diffstat:
12 files changed, 117 insertions(+), 73 deletions(-)
diff --git a/bank/conf/test.conf b/bank/conf/test.conf
@@ -13,15 +13,6 @@ spa = $DATADIR/spa/
SQL_DIR = $DATADIR/sql/
CONFIG = postgresql:///libeufincheck
-[libeufin-bank-conversion]
-cashin_ratio = 0.8
-cashin_fee = KUDOS:0.02
-cashin_tiny_amount = KUDOS:0.01
-cashin_rounding_mode = nearest
-cashout_ratio = 1.25
-cashout_fee = EUR:0.003
-cashout_min_amount = KUDOS:0.1
-
[nexus-ebics]
currency = EUR
diff --git a/bank/conf/test_no_tan.conf b/bank/conf/test_no_tan.conf
@@ -10,14 +10,5 @@ allow_conversion = YES
SQL_DIR = $DATADIR/sql/
CONFIG = postgresql:///libeufincheck
-[libeufin-bank-conversion]
-cashin_ratio = 0.8
-cashin_fee = KUDOS:0.02
-cashin_tiny_amount = KUDOS:0.01
-cashin_rounding_mode = nearest
-cashout_ratio = 1.25
-cashout_fee = EUR:0.003
-cashout_min_amount = KUDOS:0.1
-
[nexus-ebics]
currency = EUR
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -79,7 +79,6 @@ data class BankConfig(
val allowConversion: Boolean,
val fiatCurrency: String?,
val fiatCurrencySpec: CurrencySpecification?,
- val conversionInfo: ConversionInfo?,
val tanSms: String?,
val tanEmail: String?,
val spaPath: String?
@@ -128,12 +127,10 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError {
val regionalCurrency = requireString("libeufin-bank", "currency")
var fiatCurrency: String? = null;
var fiatCurrencySpec: CurrencySpecification? = null
- var conversionInfo: ConversionInfo? = null;
val allowConversion = lookupBoolean("libeufin-bank", "allow_conversion") ?: false;
if (allowConversion) {
fiatCurrency = requireString("nexus-ebics", "currency");
fiatCurrencySpec = currencySpecificationFor(fiatCurrency)
- conversionInfo = loadConversionInfo(regionalCurrency, fiatCurrency)
}
BankConfig(
regionalCurrency = regionalCurrency,
@@ -150,7 +147,6 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError {
allowConversion = allowConversion,
fiatCurrency = fiatCurrency,
fiatCurrencySpec = fiatCurrencySpec,
- conversionInfo = conversionInfo,
tanSms = lookupPath("libeufin-bank", "tan_sms"),
tanEmail = lookupPath("libeufin-bank", "tan_email"),
)
@@ -162,21 +158,6 @@ fun TalerConfig.currencySpecificationFor(currency: String): CurrencySpecificatio
}?.let { loadCurrencySpecification(it) } ?: throw TalerConfigError("missing currency specification for $currency")
}
-private fun TalerConfig.loadConversionInfo(currency: String, fiatCurrency: String): ConversionInfo = catchError {
- ConversionInfo(
- cashin_ratio = requireDecimalNumber("libeufin-bank-conversion", "cashin_ratio"),
- cashin_fee = requireAmount("libeufin-bank-conversion", "cashin_fee", currency),
- cashin_tiny_amount = amount("libeufin-bank-conversion", "cashin_tiny_amount", currency) ?: TalerAmount(0, 1, currency),
- cashin_rounding_mode = RoundingMode("libeufin-bank-conversion", "cashin_rounding_mode") ?: RoundingMode.zero,
- cashin_min_amount = amount("libeufin-bank-conversion", "cashin_min_amount", fiatCurrency) ?: TalerAmount(0, 0, fiatCurrency),
- cashout_ratio = requireDecimalNumber("libeufin-bank-conversion", "cashout_ratio"),
- cashout_fee = requireAmount("libeufin-bank-conversion", "cashout_fee", fiatCurrency),
- cashout_tiny_amount = amount("libeufin-bank-conversion", "cashout_tiny_amount", fiatCurrency) ?: TalerAmount(0, 1, fiatCurrency),
- cashout_rounding_mode = RoundingMode("libeufin-bank-conversion", "cashout_rounding_mode") ?: RoundingMode.zero,
- cashout_min_amount = amount("libeufin-bank-conversion", "cashout_min_amount", currency) ?: TalerAmount(0, 0, currency),
- )
-}
-
private fun TalerConfig.loadCurrencySpecification(section: String): CurrencySpecification = catchError {
CurrencySpecification(
name = requireString(section, "name"),
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt b/bank/src/main/kotlin/tech/libeufin/bank/ConversionApi.kt
@@ -30,13 +30,21 @@ import net.taler.common.errorcodes.TalerErrorCode
fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allowConversion) {
get("/conversion-info/config") {
+ val config = db.conversion.getConfig(ctx.regionalCurrency, ctx.fiatCurrency!!)
+ if (config == null) {
+ throw libeufinError(
+ HttpStatusCode.NotImplemented,
+ "conversion rate not configured yet",
+ TalerErrorCode.END
+ )
+ }
call.respond(
ConversionConfig(
regional_currency = ctx.regionalCurrency,
regional_currency_specification = ctx.regionalCurrencySpec,
fiat_currency = ctx.fiatCurrency!!,
fiat_currency_specification = ctx.fiatCurrencySpec!!,
- conversion_info = ctx.conversionInfo!!
+ conversion_info = config
)
)
}
@@ -90,4 +98,11 @@ fun Routing.conversionApi(db: Database, ctx: BankConfig) = conditional(ctx.allow
}
}
}
+ authAdmin(db, TokenScope.readwrite) {
+ post("/conversion-info/conversion-rate") {
+ val req = call.receive<ConversionInfo>()
+ db.conversion.updateConfig(req);
+ call.respond(HttpStatusCode.NoContent)
+ }
+ }
}
\ 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
@@ -316,12 +316,6 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name =
logger.info("Admin's account created")
}
}
-
- // Load conversion config
- ctx.conversionInfo?.run {
- logger.info("load conversion config in DB")
- db.conversion.updateConfig(this)
- }
}
}
}
@@ -361,12 +355,6 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve")
pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText())
// Remove conversion info from the database ?
}
-
- // Load conversion config
- ctx.conversionInfo?.run {
- logger.info("loading conversion config in DB")
- db.conversion.updateConfig(this)
- }
}
embeddedServer(Netty, port = serverCfg.port) {
corebankWebApp(db, ctx)
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt b/bank/src/main/kotlin/tech/libeufin/bank/TalerCommon.kt
@@ -300,6 +300,10 @@ class DecimalNumber {
val value: Long
val frac: Int
+ constructor(value: Long, frac: Int) {
+ this.value = value
+ this.frac = frac
+ }
constructor(encoded: String) {
val match = PATTERN.matchEntire(encoded) ?: throw badRequest("Invalid decimal number format");
val (value, frac) = match.destructured
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt b/bank/src/main/kotlin/tech/libeufin/bank/db/ConversionDAO.kt
@@ -20,6 +20,7 @@
package tech.libeufin.bank
import tech.libeufin.util.*
+import tech.libeufin.bank.*
/** Data access logic for conversion */
class ConversionDAO(private val db: Database) {
@@ -61,6 +62,35 @@ class ConversionDAO(private val db: Database) {
}
}
+ /** Get in-db conversion config */
+ suspend fun getConfig(regional: String, fiat: String): ConversionInfo? = db.conn {
+ it.transaction { conn ->
+ val amount = conn.prepareStatement("SELECT (amount).val as amount_val, (amount).frac as amount_frac FROM config_get_amount(?) as amount");
+ val roundingMode = conn.prepareStatement("SELECT config_get_rounding_mode(?)");
+ fun getAmount(name: String, currency: String): TalerAmount? {
+ amount.setString(1, name)
+ return amount.oneOrNull { it.getAmount("amount", currency) }
+ }
+ fun getRatio(name: String): DecimalNumber? = getAmount(name, "")?.run { DecimalNumber(value, frac) }
+ fun getMode(name: String): RoundingMode? {
+ roundingMode.setString(1, name)
+ return roundingMode.oneOrNull { RoundingMode.valueOf(it.getString(1)) }
+ }
+ ConversionInfo(
+ cashin_ratio = getRatio("cashin_ratio") ?: return@transaction null,
+ cashin_fee = getAmount("cashin_fee", regional) ?: return@transaction null,
+ cashin_tiny_amount = getAmount("cashin_tiny_amount", regional) ?: return@transaction null,
+ cashin_rounding_mode = getMode("cashin_rounding_mode") ?: return@transaction null,
+ cashin_min_amount = getAmount("cashin_min_amount", fiat) ?: return@transaction null,
+ cashout_ratio = getRatio("cashout_ratio") ?: return@transaction null,
+ cashout_fee = getAmount("cashout_fee", fiat) ?: return@transaction null,
+ cashout_tiny_amount = getAmount("cashout_tiny_amount", fiat) ?: return@transaction null,
+ cashout_rounding_mode = getMode("cashout_rounding_mode") ?: return@transaction null,
+ cashout_min_amount = getAmount("cashout_min_amount", regional) ?: return@transaction null,
+ )
+ }
+ }
+
/** 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()
diff --git a/bank/src/test/kotlin/CoreBankApiTest.kt b/bank/src/test/kotlin/CoreBankApiTest.kt
@@ -1020,7 +1020,7 @@ class CoreBankCashoutApiTest {
// POST /accounts/{USERNAME}/cashouts/{CASHOUT_ID}/confirm
@Test
- fun confirm() = bankSetup { db ->
+ fun confirm() = bankSetup { _ ->
authRoutine(HttpMethod.Post, "/accounts/merchant/cashouts/42/confirm")
client.patchA("/accounts/customer") {
@@ -1086,19 +1086,21 @@ class CoreBankCashoutApiTest {
json(req) { "request_uid" to randShortHashCode() }
}.assertOkJson<CashoutPending> {
val id = it.cashout_id
-
- db.conversion.updateConfig(ConversionInfo(
- cashin_ratio = DecimalNumber("1"),
- cashin_fee = TalerAmount("KUDOS:0.1"),
- cashin_tiny_amount = TalerAmount("KUDOS:0.0001"),
- cashin_rounding_mode = RoundingMode.nearest,
- cashin_min_amount = TalerAmount("EUR:0.0001"),
- cashout_ratio = DecimalNumber("1"),
- cashout_fee = TalerAmount("EUR:0.1"),
- cashout_tiny_amount = TalerAmount("EUR:0.0001"),
- cashout_rounding_mode = RoundingMode.nearest,
- cashout_min_amount = TalerAmount("KUDOS:0.0001"),
- ))
+ client.post("/conversion-info/conversion-rate") {
+ pwAuth("admin")
+ json {
+ "cashin_ratio" to "1"
+ "cashin_fee" to "KUDOS:0.1"
+ "cashin_tiny_amount" to "KUDOS:0.0001"
+ "cashin_rounding_mode" to "nearest"
+ "cashin_min_amount" to "EUR:0.0001"
+ "cashout_ratio" to "1"
+ "cashout_fee" to "EUR:0.1"
+ "cashout_tiny_amount" to "EUR:0.0001"
+ "cashout_rounding_mode" to "nearest"
+ "cashout_min_amount" to "KUDOS:0.0001"
+ }
+ }.assertNoContent()
client.postA("/accounts/customer/cashouts/$id/confirm"){
json { "tan" to smsCode("+99") }
diff --git a/bank/src/test/kotlin/DatabaseTest.kt b/bank/src/test/kotlin/DatabaseTest.kt
@@ -33,14 +33,7 @@ import tech.libeufin.bank.AccountDAO.*
import tech.libeufin.util.*
class DatabaseTest {
- // Testing the helper that update conversion config
- @Test
- fun conversionConfig() = setup { db, ctx ->
- // Check idempotent
- db.conversion.updateConfig(ctx.conversionInfo!!)
- db.conversion.updateConfig(ctx.conversionInfo!!)
- }
-
+
// Testing the helper that creates the admin account.
@Test
fun createAdmin() = setup { db, ctx ->
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
@@ -57,7 +57,6 @@ fun setup(
val ctx = config.loadBankConfig()
Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency).use {
runBlocking {
- ctx.conversionInfo?.run { it.conversion.updateConfig(this) }
lambda(it, ctx)
}
}
@@ -105,6 +104,24 @@ fun bankSetup(
application {
corebankWebApp(db, ctx)
}
+ if (ctx.allowConversion) {
+ // Set conversion rates
+ client.post("/conversion-info/conversion-rate") {
+ pwAuth("admin")
+ json {
+ "cashin_ratio" to "0.8"
+ "cashin_fee" to "KUDOS:0.02"
+ "cashin_tiny_amount" to "KUDOS:0.01"
+ "cashin_rounding_mode" to "nearest"
+ "cashin_min_amount" to "EUR:0"
+ "cashout_ratio" to "1.25"
+ "cashout_fee" to "EUR:0.003"
+ "cashout_tiny_amount" to "EUR:0.00000001"
+ "cashout_rounding_mode" to "zero"
+ "cashout_min_amount" to "KUDOS:0.1"
+ }
+ }.assertNoContent()
+ }
lambda(db)
}
}
diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql
@@ -1438,6 +1438,20 @@ LANGUAGE sql AS $$
ON CONFLICT (key) DO UPDATE SET value = excluded.value
$$;
+CREATE OR REPLACE FUNCTION config_get_amount(
+ IN name TEXT,
+ OUT amount taler_amount
+)
+LANGUAGE sql AS $$
+ SELECT (value['val']::int8, value['frac']::int4)::taler_amount FROM config WHERE key=name
+$$;
+
+CREATE OR REPLACE FUNCTION config_get_rounding_mode(
+ IN name TEXT,
+ OUT mode rounding_mode
+)
+LANGUAGE sql AS $$ SELECT (value->>'mode')::rounding_mode FROM config WHERE key=name $$;
+
CREATE OR REPLACE FUNCTION conversion_apply_ratio(
IN amount taler_amount
,IN ratio taler_amount
diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt
@@ -69,6 +69,7 @@ class IntegrationTest {
nexusCmd.run("dbinit -c ../bank/conf/test.conf -r")
val bankCmd = LibeufinBankCommand();
bankCmd.run("dbinit -c ../bank/conf/test.conf -r")
+ bankCmd.run("passwd admin password -c ../bank/conf/test.conf")
kotlin.concurrent.thread(isDaemon = true) {
bankCmd.run("serve -c ../bank/conf/test.conf")
}
@@ -98,6 +99,23 @@ class IntegrationTest {
}
}.assertCreated()
+ // Set conversion rates
+ client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") {
+ basicAuth("admin", "password")
+ json {
+ "cashin_ratio" to "0.8"
+ "cashin_fee" to "KUDOS:0.02"
+ "cashin_tiny_amount" to "KUDOS:0.01"
+ "cashin_rounding_mode" to "nearest"
+ "cashin_min_amount" to "EUR:0"
+ "cashout_ratio" to "1.25"
+ "cashout_fee" to "EUR:0.003"
+ "cashout_tiny_amount" to "EUR:0.00000001"
+ "cashout_rounding_mode" to "zero"
+ "cashout_min_amount" to "KUDOS:0.1"
+ }
+ }.assertNoContent()
+
// Cashin
repeat(3) { i ->
val reservePub = randBytes(32);