summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAntoine A <>2023-10-13 10:43:11 +0000
committerAntoine A <>2023-10-13 10:45:59 +0000
commitd7064c851ae27cd5495f509fbc191df710e9e724 (patch)
treed0e7a05360286ec82ba8a4c6783f77887170ec3b
parent11d4d41bf20465259d245c64d50c676308216976 (diff)
downloadlibeufin-d7064c851ae27cd5495f509fbc191df710e9e724.tar.gz
libeufin-d7064c851ae27cd5495f509fbc191df710e9e724.tar.bz2
libeufin-d7064c851ae27cd5495f509fbc191df710e9e724.zip
Improve and fix config logic
move all config logic in a single file log and exit on config error add currencies.conf
-rw-r--r--Makefile1
-rw-r--r--bank/conf/test.conf11
-rw-r--r--bank/conf/test_restrict.conf11
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Config.kt117
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Database.kt26
-rw-r--r--bank/src/main/kotlin/tech/libeufin/bank/Main.kt105
-rw-r--r--bank/src/test/kotlin/helpers.kt14
-rw-r--r--contrib/currencies.conf99
8 files changed, 252 insertions, 132 deletions
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/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 72f45c3d..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 ?
}
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
index ea4ce211..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
@@ -72,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.
*/
@@ -124,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"
@@ -364,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",
@@ -399,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)
}
}
@@ -423,24 +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)
+ val db = Database(dbCfg.dbConnStr, ctx.currency)
runBlocking {
if (!maybeCreateAdminAccount(db, ctx)) // logs provided by the helper
exitProcess(1)
}
- embeddedServer(Netty, port = servePort) {
+ embeddedServer(Netty, port = serverCfg.port) {
corebankWebApp(db, ctx)
}.start(wait = true)
}
@@ -461,12 +396,10 @@ 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)
+ 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)
@@ -494,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())
@@ -516,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))
}
}
@@ -544,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/test/kotlin/helpers.kt b/bank/src/test/kotlin/helpers.kt
index a096158c..d878ad2c 100644
--- a/bank/src/test/kotlin/helpers.kt
+++ b/bank/src/test/kotlin/helpers.kt
@@ -15,14 +15,12 @@ fun setup(
conf: String = "test.conf",
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 {
+ 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)
}
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"}
+