gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[libeufin] branch master updated (e58faac7 -> d7064c85)


From: gnunet
Subject: [libeufin] branch master updated (e58faac7 -> d7064c85)
Date: Fri, 13 Oct 2023 12:46:19 +0200

This is an automated email from the git hooks/post-receive script.

antoine pushed a change to branch master
in repository libeufin.

    from e58faac7 Fix amount serialization
     new 11d4d41b Don't block the server during JDBC calls
     new d7064c85 Improve and fix config logic

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 Makefile                                           |   1 +
 bank/conf/test.conf                                |  11 --
 bank/conf/test_restrict.conf                       |  11 --
 .../kotlin/tech/libeufin/bank/Authentication.kt    |   2 +-
 bank/src/main/kotlin/tech/libeufin/bank/Config.kt  | 117 +++++++++++++
 .../src/main/kotlin/tech/libeufin/bank/Database.kt | 192 ++++++++++-----------
 bank/src/main/kotlin/tech/libeufin/bank/Main.kt    | 128 ++++----------
 bank/src/main/kotlin/tech/libeufin/bank/helpers.kt |   8 +-
 bank/src/test/kotlin/TalerApiTest.kt               |   2 +-
 bank/src/test/kotlin/helpers.kt                    |  29 ++--
 contrib/currencies.conf                            |  99 +++++++++++
 11 files changed, 363 insertions(+), 237 deletions(-)
 create mode 100644 bank/src/main/kotlin/tech/libeufin/bank/Config.kt
 create mode 100644 contrib/currencies.conf

diff --git a/Makefile b/Makefile
index 50458c6f..9e8c08fd 100644
--- a/Makefile
+++ b/Makefile
@@ -46,6 +46,7 @@ deb: exec-arch copy-spa
 install:
        install -d $(config_dir)
        install contrib/libeufin-bank.conf $(config_dir)/
+       install contrib/currencies.conf $(config_dir)/
        install -D database-versioning/libeufin-bank*.sql -t $(sql_dir)
        install -D database-versioning/versioning.sql -t $(sql_dir)
        install -D database-versioning/procedures.sql -t $(sql_dir)
diff --git a/bank/conf/test.conf b/bank/conf/test.conf
index 703ab6c5..80bfe99a 100644
--- a/bank/conf/test.conf
+++ b/bank/conf/test.conf
@@ -5,17 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000
 REGISTRATION_BONUS_ENABLED = NO
 SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com
 
-[currency-kudos]
-ENABLED = YES
-name = "Kudos (Taler Demonstrator)"
-code = "KUDOS"
-decimal_separator = ","
-fractional_input_digits = 2
-fractional_normal_digits = 2
-fractional_trailing_zero_digits = 2
-is_currency_name_leading = NO
-alt_unit_names = {"0":"ク"}
-
 [libeufin-bankdb-postgres]
 SQL_DIR = $DATADIR/sql/
 CONFIG = postgresql:///libeufincheck
\ No newline at end of file
diff --git a/bank/conf/test_restrict.conf b/bank/conf/test_restrict.conf
index 9a74f807..eda2037c 100644
--- a/bank/conf/test_restrict.conf
+++ b/bank/conf/test_restrict.conf
@@ -5,17 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000
 REGISTRATION_BONUS_ENABLED = NO
 restrict_registration = YES
 
-[currency-kudos]
-ENABLED = YES
-name = "Kudos (Taler Demonstrator)"
-code = "KUDOS"
-decimal_separator = ","
-fractional_input_digits = 2
-fractional_normal_digits = 2
-fractional_trailing_zero_digits = 2
-is_currency_name_leading = NO
-alt_unit_names = {"0":"ク"}
-
 [libeufin-bankdb-postgres]
 SQL_DIR = $DATADIR/sql/
 CONFIG = postgresql:///libeufincheck
\ No newline at end of file
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
index 6054877e..36031e97 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Authentication.kt
@@ -15,7 +15,7 @@ import tech.libeufin.util.getAuthorizationRawHeader
  *
  * Returns the authenticated customer, or null if they failed.
  */
-fun ApplicationCall.authenticateBankRequest(db: Database, requiredScope: 
TokenScope): Customer? {
+suspend fun ApplicationCall.authenticateBankRequest(db: Database, 
requiredScope: TokenScope): Customer? {
     // Extracting the Authorization header.
     val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
         "Authorization header not found.",
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Config.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
new file mode 100644
index 00000000..ba9edb7f
--- /dev/null
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Config.kt
@@ -0,0 +1,117 @@
+/*
+ * This file is part of LibEuFin.
+ * Copyright (C) 2019 Stanisci and Dold.
+
+ * LibEuFin is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation; either version 3, or
+ * (at your option) any later version.
+
+ * LibEuFin is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Affero General
+ * Public License for more details.
+
+ * You should have received a copy of the GNU Affero General Public
+ * License along with LibEuFin; see the file COPYING.  If not, see
+ * <http://www.gnu.org/licenses/>
+ */
+package tech.libeufin.bank
+
+import ConfigSource
+import TalerConfig
+import TalerConfigError
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import kotlinx.serialization.json.Json
+
+private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.Config")
+private val BANK_CONFIG_SOURCE = ConfigSource("libeufin-bank", "libeufin-bank")
+
+data class DatabaseConfig(
+    val dbConnStr: String,
+    val sqlDir: String
+)
+
+data class ServerConfig(
+    val method: String,
+    val port: Int
+)
+
+fun talerConfig(configPath: String?): TalerConfig = catchError {
+    val config = TalerConfig(BANK_CONFIG_SOURCE)
+    config.load(configPath)
+    config
+}
+
+fun TalerConfig.loadDbConfig(): DatabaseConfig = catchError  {
+    DatabaseConfig(
+        dbConnStr = requireString("libeufin-bankdb-postgres", "config"),
+        sqlDir = requirePath("libeufin-bankdb-postgres", "sql_dir")
+    )
+}
+
+fun TalerConfig.loadServerConfig(): ServerConfig = catchError  {
+    ServerConfig(
+        method = requireString("libeufin-bank", "serve"),
+        port = requireNumber("libeufin-bank", "port")
+    )
+}
+
+fun TalerConfig.loadBankApplicationContext(): BankApplicationContext = 
catchError  {
+    val currency = requireString("libeufin-bank", "currency")
+    val currencySpecification = sections.find {
+        it.startsWith("CURRENCY-") && requireBoolean(it, "enabled") && 
requireString(it, "code") == currency
+    }?.let { loadCurrencySpecification(it) } ?: throw 
TalerConfigError("missing currency specification for $currency")
+    BankApplicationContext(
+        currency = currency,
+        restrictRegistration = lookupBoolean("libeufin-bank", 
"restrict_registration") ?: false,
+        cashoutCurrency = lookupString("libeufin-bank", "cashout_currency"),
+        defaultCustomerDebtLimit = requireAmount("libeufin-bank", 
"default_customer_debt_limit", currency),
+        registrationBonusEnabled = lookupBoolean("libeufin-bank", 
"registration_bonus_enabled") ?: false,
+        registrationBonus = requireAmount("libeufin-bank", 
"registration_bonus", currency),
+        suggestedWithdrawalExchange = lookupString("libeufin-bank", 
"suggested_withdrawal_exchange"),
+        defaultAdminDebtLimit = requireAmount("libeufin-bank", 
"default_admin_debt_limit", currency),
+        spaCaptchaURL = lookupString("libeufin-bank", "spa_captcha_url"),
+        restrictAccountDeletion = lookupBoolean("libeufin-bank", 
"restrict_account_deletion") ?: true,
+        currencySpecification = currencySpecification
+    )
+}
+
+private fun TalerConfig.loadCurrencySpecification(section: String): 
CurrencySpecification = catchError {
+    CurrencySpecification(
+        name = requireString(section, "name"),
+        decimal_separator = requireString(section, "decimal_separator"),
+        num_fractional_input_digits = requireNumber(section, 
"fractional_input_digits"),
+        num_fractional_normal_digits = requireNumber(section, 
"fractional_normal_digits"),
+        num_fractional_trailing_zero_digits = requireNumber(section, 
"fractional_trailing_zero_digits"),
+        is_currency_name_leading = requireBoolean(section, 
"is_currency_name_leading"),
+        alt_unit_names = Json.decodeFromString(requireString(section, 
"alt_unit_names"))
+    )
+}
+
+private fun TalerConfig.requireAmount(section: String, option: String, 
currency: String): TalerAmount = catchError {
+    val amountStr = lookupString(section, option) ?:
+        throw TalerConfigError("expected amount for section $section, option 
$option, but config value is empty")
+    val amount = try {
+        TalerAmount(amountStr)
+    } catch (e: Exception) {
+        throw TalerConfigError("expected amount for section $section, option 
$option, but amount is malformed")
+    }
+
+    if (amount.currency != currency) {
+        throw TalerConfigError(
+            "expected amount for section $section, option $option, but 
currency is wrong (got ${amount.currency} expected $currency"
+        )
+    }
+    amount
+}
+
+private fun <R> catchError(lambda: () -> R): R {
+    try {
+        return lambda()
+    } catch (e: TalerConfigError) {
+        logger.error(e.message)
+        kotlin.system.exitProcess(1)
+    }
+}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
index ad60ed85..14cc1666 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Database.kt
@@ -96,10 +96,10 @@ private fun <R> PgConnection.transaction(lambda: 
(PgConnection) -> R): R {
     }
 }
 
-fun initializeDatabaseTables(dbConfig: String, sqlDir: String) {
-    logger.info("doing DB initialization, sqldir $sqlDir, dbConfig $dbConfig")
-    pgDataSource(dbConfig).pgConnection().use { conn ->
-        val sqlVersioning = File("$sqlDir/versioning.sql").readText()
+fun initializeDatabaseTables(cfg: DatabaseConfig) {
+    logger.info("doing DB initialization, sqldir ${cfg.sqlDir}, dbConnStr 
${cfg.dbConnStr}")
+    pgDataSource(cfg.dbConnStr).pgConnection().use { conn ->
+        val sqlVersioning = File("${cfg.sqlDir}/versioning.sql").readText()
         conn.execSQLUpdate(sqlVersioning)
 
         val checkStmt = conn.prepareStatement("SELECT count(*) as n FROM 
_v.patches where patch_name = ?")
@@ -115,7 +115,7 @@ fun initializeDatabaseTables(dbConfig: String, sqlDir: 
String) {
                 continue
             }
 
-            val path = File("$sqlDir/libeufin-bank-$numStr.sql")
+            val path = File("${cfg.sqlDir}/libeufin-bank-$numStr.sql")
             if (!path.exists()) {
                 logger.info("path $path doesn't exist anymore, stopping")
                 break
@@ -124,14 +124,14 @@ fun initializeDatabaseTables(dbConfig: String, sqlDir: 
String) {
             val sqlPatchText = path.readText()
             conn.execSQLUpdate(sqlPatchText)
         }
-        val sqlProcedures = File("$sqlDir/procedures.sql").readText()
+        val sqlProcedures = File("${cfg.sqlDir}/procedures.sql").readText()
         conn.execSQLUpdate(sqlProcedures)
     }
 }
 
-fun resetDatabaseTables(dbConfig: String, sqlDir: String) {
-    logger.info("doing DB initialization, sqldir $sqlDir, dbConfig $dbConfig")
-    pgDataSource(dbConfig).pgConnection().use { conn ->
+fun resetDatabaseTables(cfg: DatabaseConfig) {
+    logger.info("reset DB, sqldir ${cfg.sqlDir}, dbConnStr ${cfg.dbConnStr}")
+    pgDataSource(cfg.dbConnStr).pgConnection().use { conn ->
         val count = conn.prepareStatement("SELECT count(*) FROM 
information_schema.schemata WHERE schema_name='_v'").oneOrNull { 
             it.getInt(1)
         } ?: 0
@@ -140,12 +140,8 @@ fun resetDatabaseTables(dbConfig: String, sqlDir: String) {
             return
         }
 
-        val sqlDrop = File("$sqlDir/libeufin-bank-drop.sql").readText()
-        try {
-        conn.execSQLUpdate(sqlDrop)
-        } catch (e: Exception) {
-            
-        }
+        val sqlDrop = File("${cfg.sqlDir}/libeufin-bank-drop.sql").readText()
+        conn.execSQLUpdate(sqlDrop) // TODO can fail ?
     }
 }
 
@@ -211,9 +207,12 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         dbPool.close()
     }
 
-    private fun <R> conn(lambda: (PgConnection) -> R): R {
-        val conn = dbPool.getConnection()
-        return conn.use{ it -> lambda(it.unwrap(PgConnection::class.java)) }
+    private suspend fun <R> conn(lambda: suspend (PgConnection) -> R): R {
+        // Use a coroutine dispatcher that we can block as JDBC API is blocking
+        return withContext(Dispatchers.IO) {
+            val conn = dbPool.getConnection()
+            conn.use{ it -> lambda(it.unwrap(PgConnection::class.java)) }
+        }
     }
 
     // CUSTOMERS
@@ -225,7 +224,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      *
      * In case of conflict, this method returns null.
      */
-    fun customerCreate(customer: Customer): Long? = conn { conn ->
+    suspend fun customerCreate(customer: Customer): Long? = conn { conn ->
         val stmt = conn.prepareStatement("""
             INSERT INTO customers (
               login
@@ -267,7 +266,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      * Deletes a customer (including its bank account row) from
      * the database.  The bank account gets deleted by the cascade.
      */
-    fun customerDeleteIfBalanceIsZero(login: String): CustomerDeletionResult = 
conn { conn ->
+    suspend fun customerDeleteIfBalanceIsZero(login: String): 
CustomerDeletionResult = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               out_nx_customer,
@@ -286,7 +285,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
     }
 
     // Mostly used to get customers out of bearer tokens.
-    fun customerGetFromRowId(customer_id: Long): Customer? = conn { conn ->
+    suspend fun customerGetFromRowId(customer_id: Long): Customer? = conn { 
conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               login,
@@ -314,7 +313,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         }
     }
 
-    fun customerChangePassword(customerName: String, passwordHash: String): 
Boolean = conn { conn ->
+    suspend fun customerChangePassword(customerName: String, passwordHash: 
String): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
             UPDATE customers SET password_hash=? where login=?
         """)
@@ -323,7 +322,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         stmt.executeUpdateCheck()
     }
 
-    fun customerGetFromLogin(login: String): Customer? = conn { conn ->
+    suspend fun customerGetFromLogin(login: String): Customer? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               customer_id,
@@ -354,7 +353,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
     // Possibly more "customerGetFrom*()" to come.
 
     // BEARER TOKEN
-    fun bearerTokenCreate(token: BearerToken): Boolean = conn { conn ->
+    suspend fun bearerTokenCreate(token: BearerToken): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
              INSERT INTO bearer_tokens
                (content,
@@ -374,7 +373,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         stmt.setBoolean(6, token.isRefreshable)
         stmt.executeUpdateViolation()
     }
-    fun bearerTokenGet(token: ByteArray): BearerToken? = conn { conn ->
+    suspend fun bearerTokenGet(token: ByteArray): BearerToken? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               expiration_time,
@@ -406,7 +405,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      * if deletion succeeds or false if the token could not be
      * deleted (= not found).
      */
-    fun bearerTokenDelete(token: ByteArray): Boolean = conn { conn ->
+    suspend fun bearerTokenDelete(token: ByteArray): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
             DELETE FROM bearer_tokens
               WHERE content = ?
@@ -432,7 +431,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      * The return type expresses either success, or that the target rows
      * could not be found.
      */
-    fun accountReconfig(
+    suspend fun accountReconfig(
         login: String,
         name: String?,
         cashoutPayto: String?,
@@ -473,7 +472,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      *
      * Returns an empty list, if no public account was found.
      */
-    fun accountsGetPublic(internalCurrency: String, loginFilter: String = 
"%"): List<PublicAccount> = conn { conn ->
+    suspend fun accountsGetPublic(internalCurrency: String, loginFilter: 
String = "%"): List<PublicAccount> = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               (balance).val AS balance_val,
@@ -512,7 +511,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      * LIKE operator.  If it's null, it defaults to the "%" wildcard, meaning
      * that it returns ALL the existing accounts.
      */
-    fun accountsGetForAdmin(nameFilter: String = "%"): 
List<AccountMinimalData> = conn { conn ->
+    suspend fun accountsGetForAdmin(nameFilter: String = "%"): 
List<AccountMinimalData> = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               login,
@@ -559,7 +558,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      * row ID in the successful case.  If of unique constrain violation,
      * it returns null and any other error will be thrown as 500.
      */
-    fun bankAccountCreate(bankAccount: BankAccount): Long? = conn { conn ->
+    suspend fun bankAccountCreate(bankAccount: BankAccount): Long? = conn { 
conn ->
         if (bankAccount.balance != null)
             throw internalServerError(
                 "Do not pass a balance upon bank account creation, do a wire 
transfer instead."
@@ -598,7 +597,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         }
     }
 
-    fun bankAccountSetMaxDebt(
+    suspend fun bankAccountSetMaxDebt(
         owningCustomerId: Long,
         maxDebt: TalerAmount
     ): Boolean = conn { conn ->
@@ -617,7 +616,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         return bankCurrency
     }
 
-    fun bankAccountGetFromOwnerId(ownerId: Long): BankAccount? = conn { conn ->
+    suspend fun bankAccountGetFromOwnerId(ownerId: Long): BankAccount? = conn 
{ conn ->
         val stmt = conn.prepareStatement("""
             SELECT
              internal_payto_uri
@@ -665,7 +664,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         val internalPaytoUri: String
     )
     
-    fun bankAccountInfoFromCustomerLogin(login: String): BankInfo? = conn { 
conn ->
+    suspend fun bankAccountInfoFromCustomerLogin(login: String): BankInfo? = 
conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 bank_account_id
@@ -686,7 +685,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
         }
     }
 
-    fun bankAccountGetFromInternalPayto(internalPayto: String): BankAccount? = 
conn { conn ->
+    suspend fun bankAccountGetFromInternalPayto(internalPayto: String): 
BankAccount? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
              bank_account_id
@@ -728,7 +727,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
 
     // BANK ACCOUNT TRANSACTIONS
 
-    fun bankTransactionCreate(
+    suspend fun bankTransactionCreate(
         tx: BankInternalTransaction
     ): BankTransactionResult = conn { conn ->
         conn.transaction {
@@ -828,7 +827,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
      *
      * Returns the row ID if found, null otherwise.
      */
-    fun bankTransactionCheckExists(subject: String): Long? = conn { conn ->
+    suspend fun bankTransactionCheckExists(subject: String): Long? = conn { 
conn ->
         val stmt = conn.prepareStatement("""
             SELECT bank_transaction_id
             FROM bank_account_transactions
@@ -839,7 +838,7 @@ class Database(dbConfig: String, private val bankCurrency: 
String): java.io.Clos
     }
 
     // Get the bank transaction whose row ID is rowId
-    fun bankTransactionGetFromInternalId(rowId: Long): BankAccountTransaction? 
= conn { conn ->
+    suspend fun bankTransactionGetFromInternalId(rowId: Long): 
BankAccountTransaction? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT 
               creditor_payto_uri
@@ -902,69 +901,70 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         val nbTx = abs(params.delta) // Number of transaction to query
         // Range of transaction ids to check
         var (min, max) = if (backward) Pair(0L, params.start) else 
Pair(params.start, Long.MAX_VALUE)
-
-        return dbPool.getConnection().use { conn ->
-            // Prepare statement
-            val stmt = conn.prepareStatement("""
-                $query
-                WHERE bank_account_id=? AND
-                bank_transaction_id > ? AND  bank_transaction_id < ?
-                ORDER BY bank_transaction_id ${if (backward) "DESC" else "ASC"}
-                LIMIT ?
-            """)
-            stmt.setLong(1, bankAccountId)
-
-            fun load(amount: Int): List<T> {
+        val query = """
+            $query
+            WHERE bank_account_id=? AND
+            bank_transaction_id > ? AND  bank_transaction_id < ?
+            ORDER BY bank_transaction_id ${if (backward) "DESC" else "ASC"}
+            LIMIT ?
+        """
+        
+        suspend fun load(amount: Int): List<T> = conn { conn ->
+            conn.prepareStatement(query).use { stmt ->
+                stmt.setLong(1, bankAccountId)
                 stmt.setLong(2, min)
                 stmt.setLong(3, max)
                 stmt.setInt(4, amount)
-                return stmt.all {
+                stmt.all {
                     // Remember not to check this transaction again
                     min = kotlin.math.max(it.getLong("bank_transaction_id"), 
min)
                     map(it)
                 }
             }
+        }
 
-            val shoudPoll = when {
-                params.poll_ms <= 0 -> false
-                backward -> {
-                    val maxId = conn.prepareStatement("SELECT 
MAX(bank_transaction_id) FROM bank_account_transactions")
-                                    .oneOrNull { it.getLong(1) } ?: 0;
-                    // Check if a new transaction could appear within the 
chosen interval
-                    max > maxId + 1
-                }
-                else -> true
+        val shoudPoll = when {
+            params.poll_ms <= 0 -> false
+            backward && max == Long.MAX_VALUE -> true
+            backward -> {
+                val maxId = conn {
+                    it.prepareStatement("SELECT MAX(bank_transaction_id) FROM 
bank_account_transactions")
+                        .oneOrNull { it.getLong(1) } ?: 0
+                };
+                // Check if a new transaction could appear within the chosen 
interval
+                max > maxId + 1
             }
+            else -> true
+        }
 
-            if (shoudPoll) {
-                var history = listOf<T>()
-                notifWatcher.(listen)(bankAccountId) { flow ->
-                    // Start buffering notification before loading 
transactions to not miss any
-                    val buffered = flow.buffer()
-                    // Initial load
-                    history += load(nbTx)
-                    // Long polling if transactions are missing
-                    val missing = nbTx - history.size
-                    if (missing > 0) {
-                        withTimeoutOrNull(params.poll_ms) {
-                            buffered
-                                .filter { it.rowId > min } // Skip 
transactions already checked
-                                .take(missing).count() // Wait for missing 
transactions
-                        }
+        if (shoudPoll) {
+            var history = listOf<T>()
+            notifWatcher.(listen)(bankAccountId) { flow ->
+                // Start buffering notification before loading transactions to 
not miss any
+                val buffered = flow.buffer()
+                // Initial load
+                history += load(nbTx)
+                // Long polling if transactions are missing
+                val missing = nbTx - history.size
+                if (missing > 0) {
+                    withTimeoutOrNull(params.poll_ms) {
+                        buffered
+                            .filter { it.rowId > min } // Skip transactions 
already checked
+                            .take(missing).count() // Wait for missing 
transactions
+                    }
 
-                        if (backward) {
-                            // When going backward, we could find more 
transactions than we need
-                            history = (load(nbTx) + history).take(nbTx)
-                        } else {
-                            // Only load missing ones
-                            history += load(missing)
-                        }
+                    if (backward) {
+                        // When going backward, we could find more 
transactions than we need
+                        history = (load(nbTx) + history).take(nbTx)
+                    } else {
+                        // Only load missing ones
+                        history += load(missing)
                     }
                 }
-                history
-            } else {
-                load(nbTx)
             }
+            return history
+        } else {
+            return load(nbTx)
         }
     }
 
@@ -1081,7 +1081,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * moment, only the TWG uses the direction, to provide the /incoming
      * and /outgoing endpoints.
      */
-    fun bankTransactionGetHistory(
+    suspend fun bankTransactionGetHistory(
         start: Long,
         delta: Int,
         bankAccountId: Long
@@ -1140,7 +1140,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
     }
 
     // WITHDRAWALS
-    fun talerWithdrawalCreate(
+    suspend fun talerWithdrawalCreate(
         opUUID: UUID,
         walletBankAccount: Long,
         amount: TalerAmount
@@ -1157,7 +1157,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
         stmt.setInt(4, amount.frac)
         stmt.executeUpdateViolation()
     }
-    fun talerWithdrawalGet(opUUID: UUID): TalerWithdrawalOperation? = conn { 
conn ->
+    suspend fun talerWithdrawalGet(opUUID: UUID): TalerWithdrawalOperation? = 
conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
               (amount).val as amount_val
@@ -1195,7 +1195,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * Aborts one Taler withdrawal, only if it wasn't previously
      * confirmed.  It returns false if the UPDATE didn't succeed.
      */
-    fun talerWithdrawalAbort(opUUID: UUID): Boolean = conn { conn ->
+    suspend fun talerWithdrawalAbort(opUUID: UUID): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
             UPDATE taler_withdrawal_operations
             SET aborted = true
@@ -1214,7 +1214,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      *
      * Checking for idempotency is entirely on the Kotlin side.
      */
-    fun talerWithdrawalSetDetails(
+    suspend fun talerWithdrawalSetDetails(
         opUuid: UUID,
         exchangePayto: String,
         reservePub: String
@@ -1235,7 +1235,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * Confirms a Taler withdrawal: flags the operation as
      * confirmed and performs the related wire transfer.
      */
-    fun talerWithdrawalConfirm(
+    suspend fun talerWithdrawalConfirm(
         opUuid: UUID,
         timestamp: Instant,
         accountServicerReference: String = "NOT-USED",
@@ -1272,7 +1272,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
     /**
      * Creates a cashout operation in the database.
      */
-    fun cashoutCreate(op: Cashout): Boolean = conn { conn ->
+    suspend fun cashoutCreate(op: Cashout): Boolean = conn { conn ->
         val stmt = conn.prepareStatement("""
             INSERT INTO cashout_operations (
               cashout_uuid
@@ -1333,7 +1333,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * payment should already have taken place, before calling
      * this function.
      */
-    fun cashoutConfirm(
+    suspend fun cashoutConfirm(
         opUuid: UUID,
         tanConfirmationTimestamp: Long,
         bankTransaction: Long // regional payment backing the operation
@@ -1360,7 +1360,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
     /**
      * Deletes a cashout operation from the database.
      */
-    fun cashoutDelete(opUuid: UUID): CashoutDeleteResult = conn { conn ->
+    suspend fun cashoutDelete(opUuid: UUID): CashoutDeleteResult = conn { conn 
->
         val stmt = conn.prepareStatement("""
            SELECT out_already_confirmed
              FROM cashout_delete(?)
@@ -1379,7 +1379,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * Gets a cashout operation from the database, according
      * to its uuid.
      */
-    fun cashoutGetFromUuid(opUuid: UUID): Cashout? = conn { conn ->
+    suspend fun cashoutGetFromUuid(opUuid: UUID): Cashout? = conn { conn ->
         val stmt = conn.prepareStatement("""
             SELECT
                 (amount_debit).val as amount_debit_val
@@ -1490,7 +1490,7 @@ class Database(dbConfig: String, private val 
bankCurrency: String): java.io.Clos
      * is the same returned by "bank_wire_transfer()" where however
      * the NO_DEBTOR error will hardly take place.
      */
-    fun talerTransferCreate(
+    suspend fun talerTransferCreate(
         req: TransferRequest,
         username: String,
         timestamp: Instant,
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index aba0c823..60eae30f 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -20,9 +20,6 @@
 
 package tech.libeufin.bank
 
-import ConfigSource
-import TalerConfig
-import TalerConfigError
 import com.github.ajalt.clikt.core.CliktCommand
 import com.github.ajalt.clikt.core.context
 import com.github.ajalt.clikt.core.subcommands
@@ -47,8 +44,7 @@ import io.ktor.server.response.*
 import io.ktor.server.routing.*
 import io.ktor.utils.io.*
 import io.ktor.utils.io.jvm.javaio.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
+import kotlinx.coroutines.*
 import kotlinx.serialization.ExperimentalSerializationApi
 import kotlinx.serialization.KSerializer
 import kotlinx.serialization.descriptors.*
@@ -73,8 +69,6 @@ private val logger: Logger = 
LoggerFactory.getLogger("tech.libeufin.bank.Main")
 const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
 val TOKEN_DEFAULT_DURATION: java.time.Duration = Duration.ofDays(1L)
 
-val BANK_CONFIG_SOURCE = ConfigSource("libeufin-bank", "libeufin-bank")
-
 /**
  * Application context with the parsed configuration.
  */
@@ -125,43 +119,7 @@ data class BankApplicationContext(
      * SPA is located.
      */
     val spaCaptchaURL: String?,
-) {
-    companion object {
-        /**
-        * Read the configuration of the bank from a config file.
-        * Throws an exception if the configuration is malformed.
-        */
-        fun readFromConfig(cfg: TalerConfig): BankApplicationContext {
-            val currency = cfg.requireString("libeufin-bank", "currency")
-            val currencySpecification = cfg.sections.find {
-                it.startsWith("CURRENCY-") && cfg.requireBoolean(it, 
"enabled") && cfg.requireString(it, "code") == currency
-            }?.let {
-                CurrencySpecification(
-                    name = cfg.requireString(it, "name"),
-                    decimal_separator = cfg.requireString(it, 
"decimal_separator"),
-                    num_fractional_input_digits = cfg.requireNumber(it, 
"fractional_input_digits"),
-                    num_fractional_normal_digits = cfg.requireNumber(it, 
"fractional_normal_digits"),
-                    num_fractional_trailing_zero_digits = 
cfg.requireNumber(it, "fractional_trailing_zero_digits"),
-                    is_currency_name_leading = cfg.requireBoolean(it, 
"is_currency_name_leading"),
-                    alt_unit_names = 
Json.decodeFromString(cfg.requireString(it, "alt_unit_names"))
-                )
-            } ?: throw TalerConfigError("missing currency config for 
$currency")
-            return BankApplicationContext(
-                currency = currency,
-                restrictRegistration = cfg.lookupBoolean("libeufin-bank", 
"restrict_registration") ?: false,
-                cashoutCurrency = cfg.lookupString("libeufin-bank", 
"cashout_currency"),
-                defaultCustomerDebtLimit = cfg.requireAmount("libeufin-bank", 
"default_customer_debt_limit", currency),
-                registrationBonusEnabled = cfg.lookupBoolean("libeufin-bank", 
"registration_bonus_enabled") ?: false,
-                registrationBonus = cfg.requireAmount("libeufin-bank", 
"registration_bonus", currency),
-                suggestedWithdrawalExchange = 
cfg.lookupString("libeufin-bank", "suggested_withdrawal_exchange"),
-                defaultAdminDebtLimit = cfg.requireAmount("libeufin-bank", 
"default_admin_debt_limit", currency),
-                spaCaptchaURL = cfg.lookupString("libeufin-bank", 
"spa_captcha_url"),
-                restrictAccountDeletion = cfg.lookupBoolean("libeufin-bank", 
"restrict_account_deletion") ?: true,
-                currencySpecification = currencySpecification
-            )
-        }
-    }
-}
+)
 
 /**
  * This plugin inflates the requests that have "Content-Encoding: deflate"
@@ -365,23 +323,6 @@ fun durationFromPretty(s: String): Long {
     return durationUs
 }
 
-fun TalerConfig.requireAmount(section: String, option: String, currency: 
String): TalerAmount {
-    val amountStr = lookupString(section, option) ?:
-        throw TalerConfigError("expected amount for section $section, option 
$option, but config value is empty")
-    val amount = try {
-        TalerAmount(amountStr)
-    } catch (e: Exception) {
-        throw TalerConfigError("expected amount for section $section, option 
$option, but amount is malformed")
-    }
-
-    if (amount.currency != currency) {
-        throw TalerConfigError(
-            "expected amount for section $section, option $option, but 
currency is wrong (got ${amount.currency} expected $currency"
-        )
-    }
-    return amount
-}
-
 class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name 
= "dbinit") {
     private val configFile by option(
         "--config", "-c",
@@ -400,14 +341,11 @@ class BankDbInit : CliktCommand("Initialize the 
libeufin-bank database", name =
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
-        config.load(this.configFile)
-        val dbConnStr = config.requireString("libeufin-bankdb-postgres", 
"config")
-        val sqlDir = config.requirePath("libeufin-bankdb-postgres", "sql_dir")
+        val cfg = talerConfig(configFile).loadDbConfig()
         if (requestReset) {
-            resetDatabaseTables(dbConnStr, sqlDir)
+            resetDatabaseTables(cfg)
         }
-        initializeDatabaseTables(dbConnStr, sqlDir)
+        initializeDatabaseTables(cfg)
     }
 }
 
@@ -424,22 +362,20 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP 
server", name = "serve")
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
-        config.load(this.configFile)
-        val ctx = BankApplicationContext.readFromConfig(config)
-        val dbConnStr = config.requireString("libeufin-bankdb-postgres", 
"config")
-        logger.info("using database '$dbConnStr'")
-        val serveMethod = config.requireString("libeufin-bank", "serve")
-        if (serveMethod.lowercase() != "tcp") {
+        val cfg = talerConfig(configFile)
+        val ctx = cfg.loadBankApplicationContext()
+        val dbCfg = cfg.loadDbConfig()
+        val serverCfg = cfg.loadServerConfig()
+        if (serverCfg.method.lowercase() != "tcp") {
             logger.info("Can only serve libeufin-bank via TCP")
             exitProcess(1)
         }
-        val servePortLong = config.requireNumber("libeufin-bank", "port")
-        val servePort = servePortLong.toInt()
-        val db = Database(dbConnStr, ctx.currency)
-        if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper
-            exitProcess(1)
-        embeddedServer(Netty, port = servePort) {
+        val db = Database(dbCfg.dbConnStr, ctx.currency)
+        runBlocking {
+            if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the 
helper
+                exitProcess(1)
+        }
+        embeddedServer(Netty, port = serverCfg.port) {
             corebankWebApp(db, ctx)
         }.start(wait = true)
     }
@@ -460,20 +396,20 @@ class ChangePw : CliktCommand("Change account password", 
name = "passwd") {
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
-        config.load(this.configFile)
-        val ctx = BankApplicationContext.readFromConfig(config)
-        val dbConnStr = config.requireString("libeufin-bankdb-postgres", 
"config")
-        config.requireNumber("libeufin-bank", "port")
-        val db = Database(dbConnStr, ctx.currency)
-        if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper
+        val cfg = talerConfig(configFile)
+        val ctx = cfg.loadBankApplicationContext() 
+        val dbCfg = cfg.loadDbConfig()
+        val db = Database(dbCfg.dbConnStr, ctx.currency)
+        runBlocking {
+            if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the 
helper
             exitProcess(1)
 
-        if (!db.customerChangePassword(account, CryptoUtil.hashpw(password))) {
-            println("password change failed")
-            exitProcess(1)
-        } else {
-            println("password change succeeded")
+            if (!db.customerChangePassword(account, 
CryptoUtil.hashpw(password))) {
+                println("password change failed")
+                exitProcess(1)
+            } else {
+                println("password change succeeded")
+            }
         }
     }
 }
@@ -491,7 +427,7 @@ class BankConfigDump : CliktCommand("Dump the 
configuration", name = "dump") {
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
+        val config = talerConfig(configFile)
         println("# install path: ${config.getInstallPath()}")
         config.load(this.configFile)
         println(config.stringify())
@@ -513,8 +449,7 @@ class BankConfigPathsub : CliktCommand("Substitute 
variables in a path", name =
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
-        config.load(this.configFile)
+        val config = talerConfig(configFile)
         println(config.pathsub(pathExpr))
     }
 }
@@ -541,8 +476,7 @@ class BankConfigGet : CliktCommand("Lookup config value", 
name = "get") {
     }
 
     override fun run() {
-        val config = TalerConfig(BANK_CONFIG_SOURCE)
-        config.load(this.configFile)
+        val config = talerConfig(configFile)
         if (isPath) {
             val res = config.lookupPath(sectionName, optionName)
             if (res == null) {
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt 
b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
index 2b750858..5e1211c7 100644
--- a/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
+++ b/bank/src/main/kotlin/tech/libeufin/bank/helpers.kt
@@ -58,7 +58,7 @@ fun ApplicationCall.getAuthToken(): String? {
  * Performs the HTTP basic authentication.  Returns the
  * authenticated customer on success, or null otherwise.
  */
-fun doBasicAuth(db: Database, encodedCredentials: String): Customer? {
+suspend fun doBasicAuth(db: Database, encodedCredentials: String): Customer? {
     val plainUserAndPass = String(base64ToBytes(encodedCredentials), 
Charsets.UTF_8) // :-separated
     val userAndPassSplit = plainUserAndPass.split(
         ":",
@@ -96,7 +96,7 @@ private fun splitBearerToken(tok: String): String? {
 
 /* Performs the secret-token authentication.  Returns the
  * authenticated customer on success, null otherwise. */
-fun doTokenAuth(
+suspend fun doTokenAuth(
     db: Database,
     token: String,
     requiredScope: TokenScope,
@@ -268,7 +268,7 @@ fun getWithdrawalConfirmUrl(
  * if the query param doesn't parse into a UUID.  Currently
  * used by the Taler Web/SPA and Integration API handlers.
  */
-fun getWithdrawal(db: Database, opIdParam: String): TalerWithdrawalOperation {
+suspend fun getWithdrawal(db: Database, opIdParam: String): 
TalerWithdrawalOperation {
     val opId = try {
         UUID.fromString(opIdParam)
     } catch (e: Exception) {
@@ -327,7 +327,7 @@ fun getHistoryParams(params: Parameters): HistoryParams {
  *
  * It returns false in case of problems, true otherwise.
  */
-fun maybeCreateAdminAccount(db: Database, ctx: BankApplicationContext): 
Boolean {
+suspend fun maybeCreateAdminAccount(db: Database, ctx: 
BankApplicationContext): Boolean {
     val maybeAdminCustomer = db.customerGetFromLogin("admin")
     val adminCustomerId: Long = if (maybeAdminCustomer == null) {
         logger.debug("Creating admin's customer row")
diff --git a/bank/src/test/kotlin/TalerApiTest.kt 
b/bank/src/test/kotlin/TalerApiTest.kt
index a9cbf711..0cdd450a 100644
--- a/bank/src/test/kotlin/TalerApiTest.kt
+++ b/bank/src/test/kotlin/TalerApiTest.kt
@@ -84,7 +84,7 @@ class TalerApiTest {
         ).assertSuccess()
     }
 
-    fun commonSetup(lambda: (Database, BankApplicationContext) -> Unit) {
+    fun commonSetup(lambda: suspend (Database, BankApplicationContext) -> 
Unit) {
         setup { db, ctx -> 
             // Creating the exchange and merchant accounts first.
             assertNotNull(db.customerCreate(customerFoo))
diff --git a/bank/src/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index 5ac3ba2e..d878ad2c 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -1,11 +1,8 @@
 import io.ktor.http.*
 import io.ktor.client.statement.*
 import io.ktor.client.request.*
-import kotlinx.serialization.json.Json
-import kotlinx.serialization.json.JsonObjectBuilder
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.coroutines.*
+import kotlinx.serialization.json.*
 import net.taler.wallet.crypto.Base32Crockford
 import kotlin.test.assertEquals
 import tech.libeufin.bank.*
@@ -16,21 +13,21 @@ import java.util.zip.DeflaterOutputStream
 
 fun setup(
     conf: String = "test.conf",
-    lambda: (Database, BankApplicationContext) -> Unit
+    lambda: suspend (Database, BankApplicationContext) -> Unit
 ){
-    val config = TalerConfig(BANK_CONFIG_SOURCE)
-    config.load("conf/$conf")
-    val dbConnStr = config.requireString("libeufin-bankdb-postgres", "config")
-    val sqlPath = config.requirePath("libeufin-bankdb-postgres", "SQL_DIR")
-    resetDatabaseTables(dbConnStr, sqlPath)
-    initializeDatabaseTables(dbConnStr, sqlPath)
-    val ctx = BankApplicationContext.readFromConfig(config)
-    Database(dbConnStr, ctx.currency).use {
-        lambda(it, ctx)
+    val config = talerConfig("conf/$conf")
+    val dbCfg = config.loadDbConfig()
+    resetDatabaseTables(dbCfg)
+    initializeDatabaseTables(dbCfg)
+    val ctx = config.loadBankApplicationContext()
+    Database(dbCfg.dbConnStr, ctx.currency).use {
+        runBlocking {
+            lambda(it, ctx)
+        }
     }
 }
 
-fun setupDb(lambda: (Database) -> Unit) {
+fun setupDb(lambda: suspend (Database) -> Unit) {
     setup() { db, _ -> lambda(db) }
 }
 
diff --git a/contrib/currencies.conf b/contrib/currencies.conf
new file mode 100644
index 00000000..3341a9a7
--- /dev/null
+++ b/contrib/currencies.conf
@@ -0,0 +1,99 @@
+[currency-euro]
+ENABLED = YES
+name = "Euro"
+code = "EUR"
+decimal_separator = ","
+fractional_input_digits = 2
+fractional_normal_digits = 2
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = NO
+alt_unit_names = {"0":"€"}
+
+[currency-swiss-francs]
+ENABLED = YES
+name = "Swiss Francs"
+code = "CHF"
+decimal_separator = "."
+fractional_input_digits = 2
+fractional_normal_digits = 2
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = YES
+alt_unit_names = {"0":"Fr.","-2":"Rp."}
+
+[currency-forint]
+ENABLED = NO
+name = "Hungarian Forint"
+code = "HUF"
+decimal_separator = ","
+fractional_input_digits = 0
+fractional_normal_digits = 0
+fractional_trailing_zero_digits = 0
+is_currency_name_leading = NO
+alt_unit_names = {"0":"Ft"}
+
+[currency-us-dollar]
+ENABLED = NO
+name = "US Dollar"
+code = "USD"
+decimal_separator = "."
+fractional_input_digits = 2
+fractional_normal_digits = 2
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = YES
+alt_unit_names = {"0":"$"}
+
+[currency-kudos]
+ENABLED = YES
+name = "Kudos (Taler Demonstrator)"
+code = "KUDOS"
+decimal_separator = ","
+fractional_input_digits = 2
+fractional_normal_digits = 2
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = NO
+alt_unit_names = {"0":"ク"}
+
+[currency-testkudos]
+ENABLED = YES
+name = "Test-kudos (Taler Demonstrator)"
+code = "TESTKUDOS"
+decimal_separator = "."
+fractional_input_digits = 2
+fractional_normal_digits = 2
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = NO
+alt_unit_names = {"0":"テ","3":"kテ","-3":"mテ"}
+
+[currency-japanese-yen]
+ENABLED = NO
+name = "Japanese Yen"
+code = "JPY"
+decimal_separator = "."
+fractional_input_digits = 2
+fractional_normal_digits = 0
+fractional_trailing_zero_digits = 2
+is_currency_name_leading = YES
+alt_unit_names = {"0":"¥"}
+
+[currency-bitcoin-mainnet]
+ENABLED = NO
+name = "Bitcoin (Mainnet)"
+code = "BITCOINBTC"
+decimal_separator = "."
+fractional_input_digits = 8
+fractional_normal_digits = 3
+fractional_trailing_zero_digits = 0
+is_currency_name_leading = NO
+alt_unit_names = {"0":"BTC","-3":"mBTC"}
+
+[currency-ethereum]
+ENABLED = NO
+name = "WAI-ETHER (Ethereum)"
+code = "EthereumWAI"
+decimal_separator = "."
+fractional_input_digits = 0
+fractional_normal_digits = 0
+fractional_trailing_zero_digits = 0
+is_currency_name_leading = NO
+alt_unit_names = 
{"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":"Finney","18":"Ether","21":"KEther","24":"MEther"}
+

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]