libeufin

Integration and sandbox testing for FinTech APIs and data formats
Log | Files | Refs | Submodules | README | LICENSE

commit e4c46845b3314239f4cd354e15d4b7207830fd28
parent 736c3998648ad249577f8930b616e1f27647f938
Author: Antoine A <>
Date:   Mon, 27 Nov 2023 17:37:09 +0000

Check exchange account when conversion is enabled

Diffstat:
Mbank/src/main/kotlin/tech/libeufin/bank/Main.kt | 32++++++++++++++++++++------------
Mcontrib/libeufin-bank.conf | 30++++++++++++++----------------
Mintegration/test/IntegrationTest.kt | 42+++++++++++++++++++++---------------------
Mutil/src/main/kotlin/DB.kt | 12++++--------
4 files changed, 59 insertions(+), 57 deletions(-)

diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt @@ -228,7 +228,7 @@ fun Application.corebankWebApp(db: Database, ctx: BankConfig) { bankIntegrationApi(db, ctx) wireGatewayApi(db, ctx) revenueApi(db) - ctx.spaPath?.let { + ctx.spaPath?.let { staticFiles("/", File(it)) } } @@ -332,31 +332,39 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") val dbCfg = cfg.loadDbConfig() val serverCfg = cfg.loadServerConfig() if (serverCfg.method.lowercase() != "tcp") { - logger.info("Can only serve libeufin-bank via TCP") + logger.error("Can only serve libeufin-bank via TCP") exitProcess(1) } val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency) runBlocking { if (ctx.allowConversion) { - // TODO check exchange account - logger.info("ensure conversion is enabled") + logger.info("Ensure exchange account exists") + val info = db.account.bankInfo("exchange") + if (info == null) { + logger.error("Exchange account missing: an exchange account named 'exchange' is required for conversion to be enabled") + exitProcess(1) + } else if (!info.isTalerExchange) { + logger.error("Account is not an exchange: an exchange account named 'exchange' is required for conversion to be enabled") + exitProcess(1) + } + logger.info("Ensure conversion is enabled") val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-setup.sql") if (!sqlProcedures.exists()) { - logger.info("Missing libeufin-conversion-setup.sql file") + logger.error("Missing libeufin-conversion-setup.sql file") exitProcess(1) } - pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText()) + db.conn { it.execSQLUpdate(sqlProcedures.readText()) } } else { - logger.info("ensure conversion is disabled") + logger.info("Ensure conversion is disabled") val sqlProcedures = File("${dbCfg.sqlDir}/libeufin-conversion-drop.sql") if (!sqlProcedures.exists()) { - logger.info("Missing libeufin-conversion-drop.sql file") + logger.error("Missing libeufin-conversion-drop.sql file") exitProcess(1) } - pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText()) + db.conn { it.execSQLUpdate(sqlProcedures.readText()) } // Remove conversion info from the database ? } - } + } embeddedServer(Netty, port = serverCfg.port) { corebankWebApp(db, ctx) }.start(wait = true) @@ -433,14 +441,14 @@ class BankConfigGet : CliktCommand("Lookup config value", name = "get") { if (isPath) { val res = config.lookupPath(sectionName, optionName) if (res == null) { - logger.info("value not found in config") + logger.error("value not found in config") exitProcess(2) } println(res) } else { val res = config.lookupString(sectionName, optionName) if (res == null) { - logger.info("value not found in config") + logger.error("value not found in config") exitProcess(2) } println(res) diff --git a/contrib/libeufin-bank.conf b/contrib/libeufin-bank.conf @@ -3,15 +3,13 @@ # Internal currency of the libeufin-bank CURRENCY = KUDOS -# Debt limit for newly created customer accounts +# Default debt limit for newly created customer accounts DEFAULT_CUSTOMER_DEBT_LIMIT = KUDOS:200 -# Debt limit of the admin. Typically higher, -# since sign-up bonuses are deducted from the -# admin account. -DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:2000 +# Default debt limit of the admin. Typically higher, since sign-up bonuses and cashin are deducted from the admin account. +DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:20000000 -# Value of the registration bonus for new users. +# Value of the registration bonus for new users. Default is "CURRENCY:0" REGISTRATION_BONUS = KUDOS:100 # Allow account registration by anyone. @@ -20,8 +18,14 @@ ALLOW_REGISTRATION = yes # Allow an account to delete itself ALLOW_ACCOUNT_DELETION = yes -# Exchange that is suggested to wallets when withdrawing. -SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/ +# Enable regional currency conversion +ALLOW_CONVERSION = no + +# Path to TAN challenge transmission script via sms. If not specified, this TAN channel wil be unuspported. +TAN_SMS = + +# Path to TAN challenge transmission script via email. If not specified, this TAN channel wil be unuspported. +TAN_EMAIL = # Where "libeufin-bank serve" serves its API SERVE = tcp @@ -37,14 +41,8 @@ SPA = $DATADIR/spa/ # or something similar? SPA_CAPTCHA_URL = https://bank.demo.taler.net/webui/#/operation/{woid} -# Enable regional currency conversion -ALLOW_CONVERSION = no - -# Path to the script TAN challenge transmission via sms -TAN_SMS = - -# Path to the script TAN challenge transmission via email -TAN_EMAIL = +# Exchange that is suggested to wallets when withdrawing. +SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.demo.taler.net/ [libeufin-bankdb-postgres] # Where are the SQL files to setup our tables? diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt @@ -47,16 +47,10 @@ fun CliktCommand.run(cmd: String) { println(result.output) } -suspend fun HttpResponse.assertStatus(status: HttpStatusCode): HttpResponse { - assertEquals(status, this.status); - return this +fun HttpResponse.assertNoContent() { + assertEquals(HttpStatusCode.NoContent, this.status) } -suspend fun HttpResponse.assertCreated(): HttpResponse - = assertStatus(HttpStatusCode.Created) -suspend fun HttpResponse.assertNoContent(): HttpResponse - = assertStatus(HttpStatusCode.NoContent) - fun randBytes(lenght: Int): ByteArray { val bytes = ByteArray(lenght) kotlin.random.Random.nextBytes(bytes) @@ -71,9 +65,7 @@ class IntegrationTest { 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") - } + runBlocking { val client = HttpClient(CIO) { @@ -82,10 +74,28 @@ class IntegrationTest { constantDelay(200, 100) } } + val nexusDb = NexusDb("postgresql:///libeufincheck") + val bankDb = BankDb("postgresql:///libeufincheck", "KUDOS", "EUR") + + bankDb.account.create( + login = "exchange", + password = "password", + name = "Mr Money", + internalPaytoUri = IbanPayTo(genIbanPaytoUri()), + isPublic = false, + isTalerExchange = true, + maxDebt = BankAmount("KUDOS:0"), + bonus = BankAmount("KUDOS:0") + ) + val userPayTo = IbanPayTo(genIbanPaytoUri()) val fiatPayTo = IbanPayTo(genIbanPaytoUri()) + kotlin.concurrent.thread(isDaemon = true) { + bankCmd.run("serve -c ../bank/conf/test.conf") + } + // Create user client.post("http://0.0.0.0:8080/accounts") { json { @@ -100,16 +110,6 @@ class IntegrationTest { } }.assertOkJson<RegisterAccountResponse>() - // Create exchange - client.post("http://0.0.0.0:8080/accounts") { - json { - "username" to "exchange" - "password" to "password" - "name" to "Mr Money" - "is_taler_exchange" to true - } - }.assertOkJson<RegisterAccountResponse>() - // Set conversion rates client.post("http://0.0.0.0:8080/conversion-info/conversion-rate") { basicAuth("admin", "password") diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt @@ -234,19 +234,15 @@ fun initializeDatabaseTables(cfg: DatabaseConfig, sqlFilePrefix: String) { fun resetDatabaseTables(cfg: DatabaseConfig, sqlFilePrefix: String) { 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 - if (count == 0) { + val isInitialized = conn.prepareStatement("SELECT EXISTS(SELECT 1 FROM information_schema.schemata WHERE schema_name='_v')").oneOrNull { + it.getBoolean(1) + }!! + if (!isInitialized) { logger.info("versioning schema not present, not running drop sql") return } val sqlDrop = File("${cfg.sqlDir}/$sqlFilePrefix-drop.sql").readText() - try { conn.execSQLUpdate(sqlDrop) // TODO can fail ? - } catch (e: Exception) { - - } } } \ No newline at end of file