commit 3679a4b32b777767857dd10b2b19740ce7469ddd
parent 449d1bcf56972717f59d1ab3082f1adc28c341b4
Author: Antoine A <>
Date: Thu, 23 Nov 2023 15:06:35 +0000
Improve bank and nexus integration
Diffstat:
9 files changed, 162 insertions(+), 147 deletions(-)
diff --git a/Makefile b/Makefile
@@ -41,7 +41,7 @@ install-bank-files:
install contrib/libeufin-bank.conf $(bank_config_dir)/
install contrib/currencies.conf $(bank_config_dir)/
install -D database-versioning/libeufin-bank*.sql -t $(bank_sql_dir)
- install -D database-versioning/libeufin-conversion.sql -t $(bank_sql_dir)
+ install -D database-versioning/libeufin-conversion*.sql -t $(bank_sql_dir)
install -D database-versioning/versioning.sql -t $(bank_sql_dir)
.PHONY: install-bank
diff --git a/bank/conf/test.conf b/bank/conf/test.conf
@@ -5,7 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000
REGISTRATION_BONUS_ENABLED = NO
SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com
allow_conversion = YES
-fiat_currency = EUR
tan_sms = libeufin-tan-file.sh
tan_email = libeufin-tan-fail.sh
@@ -22,6 +21,9 @@ cashout_ratio = 1.25
cashout_fee = EUR:0.003
cashout_min_amount = KUDOS:0.1
+[nexus-ebics]
+currency = EUR
+
[nexus-postgres]
CONFIG = postgres:///libeufincheck
diff --git a/bank/conf/test_no_tan.conf b/bank/conf/test_no_tan.conf
@@ -5,7 +5,6 @@ DEFAULT_ADMIN_DEBT_LIMIT = KUDOS:10000
REGISTRATION_BONUS_ENABLED = NO
SUGGESTED_WITHDRAWAL_EXCHANGE = https://exchange.example.com
allow_conversion = YES
-fiat_currency = EUR
[libeufin-bankdb-postgres]
SQL_DIR = $DATADIR/sql/
@@ -19,3 +18,6 @@ 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
@@ -130,7 +130,7 @@ fun TalerConfig.loadBankConfig(): BankConfig = catchError {
var conversionInfo: ConversionInfo? = null;
val allowConversion = lookupBoolean("libeufin-bank", "allow_conversion") ?: false;
if (allowConversion) {
- fiatCurrency = requireString("libeufin-bank", "fiat_currency");
+ fiatCurrency = requireString("nexus-ebics", "currency");
fiatCurrencySpec = currencySpecificationFor(fiatCurrency)
conversionInfo = loadConversionInfo(regionalCurrency, fiatCurrency)
}
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -321,35 +321,6 @@ class BankDbInit : CliktCommand("Initialize the libeufin-bank database", name =
}
}
-class ConversionSetupCmd : CliktCommand("Setup conversion support", name = "conversion-setup") {
- private val configFile by option(
- "--config", "-c",
- help = "set the configuration file"
- )
-
- override fun run() {
- val config = talerConfig(configFile)
- val cfg = config.loadDbConfig()
- val ctx = config.loadBankConfig();
- val db = Database(cfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency)
- runBlocking {
- logger.info("doing DB initialization, sqldir ${cfg.sqlDir}, dbConnStr ${cfg.dbConnStr}")
- val sqlProcedures = File("${cfg.sqlDir}/libeufin-conversion.sql")
- if (!sqlProcedures.exists()) {
- logger.info("Missing libeufin-conversion.sql file")
- exitProcess(1)
- }
- pgDataSource(cfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText())
-
- // Load conversion config
- ctx.conversionInfo?.run {
- logger.info("loading conversion config in DB")
- db.conversion.updateConfig(this)
- }
- }
- }
-}
-
class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve") {
private val configFile by option(
"--config", "-c",
@@ -366,6 +337,32 @@ class ServeBank : CliktCommand("Run libeufin-bank HTTP server", name = "serve")
exitProcess(1)
}
val db = Database(dbCfg.dbConnStr, ctx.regionalCurrency, ctx.fiatCurrency)
+ runBlocking {
+ if (ctx.allowConversion) {
+ 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")
+ exitProcess(1)
+ }
+ pgDataSource(dbCfg.dbConnStr).pgConnection().execSQLUpdate(sqlProcedures.readText())
+ } else {
+ 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")
+ exitProcess(1)
+ }
+ 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)
}.start(wait = true)
@@ -467,7 +464,7 @@ class BankConfigCmd : CliktCommand("Dump the configuration", name = "config") {
class LibeufinBankCommand : CliktCommand() {
init {
versionOption(getVersion())
- subcommands(ServeBank(), BankDbInit(), ConversionSetupCmd(), ChangePw(), BankConfigCmd())
+ subcommands(ServeBank(), BankDbInit(), ChangePw(), BankConfigCmd())
}
override fun run() = Unit
diff --git a/database-versioning/libeufin-conversion-drop.sql b/database-versioning/libeufin-conversion-drop.sql
@@ -0,0 +1,9 @@
+BEGIN;
+SET search_path TO libeufin_bank;
+
+DROP TRIGGER IF EXISTS cashin_link;
+DROP FUNCTION IF EXISTS cashin_link;
+DROP TRIGGER IF EXISTS cashout_link;
+DROP FUNCTION IF EXISTS cashout_link;
+
+COMMIT;
+\ No newline at end of file
diff --git a/database-versioning/libeufin-conversion-setup.sql b/database-versioning/libeufin-conversion-setup.sql
@@ -0,0 +1,78 @@
+BEGIN;
+SET search_path TO libeufin_bank;
+
+CREATE OR REPLACE FUNCTION cashout_link()
+RETURNS trigger
+LANGUAGE plpgsql AS $$
+ DECLARE
+ now_date BIGINT;
+ payto_uri TEXT;
+ BEGIN
+ IF NEW.local_transaction IS NOT NULL THEN
+ SELECT transaction_date INTO now_date
+ FROM libeufin_bank.bank_account_transactions
+ WHERE bank_transaction_id = NEW.local_transaction;
+ SELECT cashout_payto INTO payto_uri
+ FROM libeufin_bank.bank_accounts
+ JOIN libeufin_bank.customers ON customer_id=owning_customer_id
+ WHERE bank_account_id=NEW.bank_account;
+ INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
+ amount
+ ,wire_transfer_subject
+ ,credit_payto_uri
+ ,initiation_time
+ ,request_uid
+ ) VALUES (
+ ((NEW.amount_credit).val, (NEW.amount_credit).frac)::libeufin_nexus.taler_amount
+ ,NEW.subject
+ ,payto_uri
+ ,now_date
+ ,LEFT(gen_random_uuid()::text, 35)
+ );
+ END IF;
+ RETURN NEW;
+ END;
+$$;
+
+CREATE OR REPLACE TRIGGER cashout_link BEFORE INSERT OR UPDATE ON cashout_operations
+ FOR EACH ROW EXECUTE FUNCTION cashout_link();
+
+CREATE OR REPLACE FUNCTION cashin_link()
+RETURNS trigger
+LANGUAGE plpgsql AS $$
+ DECLARE
+ now_date BIGINT;
+ payto_uri TEXT;
+ local_amount libeufin_bank.taler_amount;
+ subject TEXT;
+ too_small BOOLEAN;
+ balance_insufficient BOOLEAN;
+ no_account BOOLEAN;
+ BEGIN
+ SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time, debit_payto_uri
+ INTO local_amount.val, local_amount.frac, subject, now_date, payto_uri
+ FROM libeufin_nexus.incoming_transactions
+ WHERE incoming_transaction_id = NEW.incoming_transaction_id;
+ SET search_path TO libeufin_bank;
+ SELECT out_too_small, out_balance_insufficient, out_no_account
+ INTO too_small, balance_insufficient, no_account
+ FROM libeufin_bank.cashin(now_date, payto_uri, local_amount, subject);
+ SET search_path TO libeufin_nexus;
+
+ IF no_account THEN
+ RAISE EXCEPTION 'TODO soft error bounce: unknown account';
+ END IF;
+ IF too_small THEN
+ RAISE EXCEPTION 'TODO soft error bounce: too small amount';
+ END IF;
+ IF balance_insufficient THEN
+ RAISE EXCEPTION 'TODO hard error bounce';
+ END IF;
+ RETURN NEW;
+ END;
+$$;
+
+CREATE OR REPLACE TRIGGER cashin_link BEFORE INSERT ON libeufin_nexus.talerable_incoming_transactions
+ FOR EACH ROW EXECUTE FUNCTION cashin_link();
+
+COMMIT;
+\ No newline at end of file
diff --git a/database-versioning/libeufin-conversion.sql b/database-versioning/libeufin-conversion.sql
@@ -1,78 +0,0 @@
-BEGIN;
-SET search_path TO libeufin_conversion;
-
-CREATE OR REPLACE FUNCTION cashout()
-RETURNS trigger
-LANGUAGE plpgsql AS $$
- DECLARE
- now_date BIGINT;
- payto_uri TEXT;
- BEGIN
- IF NEW.local_transaction IS NOT NULL THEN
- SELECT transaction_date INTO now_date
- FROM libeufin_bank.bank_account_transactions
- WHERE bank_transaction_id = NEW.local_transaction;
- SELECT cashout_payto INTO payto_uri
- FROM libeufin_bank.bank_accounts
- JOIN libeufin_bank.customers ON customer_id=owning_customer_id
- WHERE bank_account_id=NEW.bank_account;
- INSERT INTO libeufin_nexus.initiated_outgoing_transactions (
- amount
- ,wire_transfer_subject
- ,credit_payto_uri
- ,initiation_time
- ,request_uid
- ) VALUES (
- ((NEW.amount_credit).val, (NEW.amount_credit).frac)::libeufin_nexus.taler_amount
- ,NEW.subject
- ,payto_uri
- ,now_date
- ,'TODO' -- How to generate this
- );
- END IF;
- RETURN NEW;
- END;
-$$;
-
-CREATE OR REPLACE TRIGGER cashout BEFORE INSERT OR UPDATE ON libeufin_bank.cashout_operations
- FOR EACH ROW EXECUTE FUNCTION cashout();
-
-CREATE OR REPLACE FUNCTION cashin()
-RETURNS trigger
-LANGUAGE plpgsql AS $$
- DECLARE
- now_date BIGINT;
- payto_uri TEXT;
- local_amount libeufin_bank.taler_amount;
- subject TEXT;
- too_small BOOLEAN;
- balance_insufficient BOOLEAN;
- no_account BOOLEAN;
- BEGIN
- SELECT (amount).val, (amount).frac, wire_transfer_subject, execution_time, debit_payto_uri
- INTO local_amount.val, local_amount.frac, subject, now_date, payto_uri
- FROM libeufin_nexus.incoming_transactions
- WHERE incoming_transaction_id = NEW.incoming_transaction_id;
- SET search_path TO libeufin_bank;
- SELECT out_too_small, out_balance_insufficient, out_no_account
- INTO too_small, balance_insufficient, no_account
- FROM libeufin_bank.cashin(now_date, payto_uri, local_amount, subject);
- SET search_path TO libeufin_conversion;
-
- IF no_account THEN
- RAISE EXCEPTION 'TODO soft error bounce: unknown account';
- END IF;
- IF too_small THEN
- RAISE EXCEPTION 'TODO soft error bounce: too small amount';
- END IF;
- IF balance_insufficient THEN
- RAISE EXCEPTION 'TODO hard error bounce';
- END IF;
- RETURN NEW;
- END;
-$$;
-
-CREATE OR REPLACE TRIGGER cashin BEFORE INSERT ON libeufin_nexus.talerable_incoming_transactions
- FOR EACH ROW EXECUTE FUNCTION cashin();
-
-COMMIT;
-\ No newline at end of file
diff --git a/integration/test/IntegrationTest.kt b/integration/test/IntegrationTest.kt
@@ -69,7 +69,6 @@ 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("conversion-setup -c ../bank/conf/test.conf")
kotlin.concurrent.thread(isDaemon = true) {
bankCmd.run("serve -c ../bank/conf/test.conf")
}
@@ -99,43 +98,48 @@ class IntegrationTest {
}.assertCreated()
// Cashin
- val reservePub = randBytes(32);
- nexusDb.incomingTalerablePaymentCreate(IncomingPayment(
- amount = NexusAmount(44, 0, "EUR"),
- debitPaytoUri = userPayTo.canonical,
- wireTransferSubject = "cashin test",
- executionTime = Instant.now(),
- bankTransferId = "entropic"),
- reservePub)
- val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:44.0")
- .assertOkJson<ConversionResponse>().amount_credit
- client.get("http://0.0.0.0:8080/accounts/customer/transactions") {
- basicAuth("customer", "password")
- }.assertOkJson<BankAccountTransactionsResponse> {
- val tx = it.transactions[0]
- assertEquals(userPayTo.canonical, tx.creditor_payto_uri)
- assertEquals("cashin test", tx.subject)
- assertEquals(converted, tx.amount)
+ repeat(3) { i ->
+ val reservePub = randBytes(32);
+ val amount = NexusAmount(20L + i, 0, "EUR")
+ nexusDb.incomingTalerablePaymentCreate(IncomingPayment(
+ amount = amount,
+ debitPaytoUri = userPayTo.canonical,
+ wireTransferSubject = "cashin test $i",
+ executionTime = Instant.now(),
+ bankTransferId = "entropic"),
+ reservePub)
+ val converted = client.get("http://0.0.0.0:8080/conversion-info/cashin-rate?amount_debit=EUR:${20 + i}")
+ .assertOkJson<ConversionResponse>().amount_credit
+ client.get("http://0.0.0.0:8080/accounts/customer/transactions") {
+ basicAuth("customer", "password")
+ }.assertOkJson<BankAccountTransactionsResponse> {
+ val tx = it.transactions.first()
+ assertEquals(userPayTo.canonical, tx.creditor_payto_uri)
+ assertEquals("cashin test $i", tx.subject)
+ assertEquals(converted, tx.amount)
+ }
}
// Cashout
- val requestUid = randBytes(32);
- val amount = BankAmount("KUDOS:25")
- val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount")
- .assertOkJson<ConversionResponse>().amount_credit;
- client.post("http://0.0.0.0:8080/accounts/customer/cashouts") {
- basicAuth("customer", "password")
- json {
- "request_uid" to ShortHashCode(requestUid)
- "amount_debit" to amount
- "amount_credit" to convert
- }
- }.assertOkJson<CashoutPending> {
- val code = File("/tmp/tan-+99.txt").readText()
- client.post("http://0.0.0.0:8080/accounts/customer/cashouts/${it.cashout_id}/confirm") {
+ repeat(3) { i ->
+ val requestUid = randBytes(32);
+ val amount = BankAmount("KUDOS:${10+i}")
+ val convert = client.get("http://0.0.0.0:8080/conversion-info/cashout-rate?amount_debit=$amount")
+ .assertOkJson<ConversionResponse>().amount_credit;
+ client.post("http://0.0.0.0:8080/accounts/customer/cashouts") {
basicAuth("customer", "password")
- json { "tan" to code }
- }.assertNoContent()
+ json {
+ "request_uid" to ShortHashCode(requestUid)
+ "amount_debit" to amount
+ "amount_credit" to convert
+ }
+ }.assertOkJson<CashoutPending> {
+ val code = File("/tmp/tan-+99.txt").readText()
+ client.post("http://0.0.0.0:8080/accounts/customer/cashouts/${it.cashout_id}/confirm") {
+ basicAuth("customer", "password")
+ json { "tan" to code }
+ }.assertNoContent()
+ }
}
}
}