libeufin

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

commit 5ac32c8b645d52c59f9be7ef29332aa228d26645
parent ee9b262ffeafe5f5af78d5382e5033b386d78f1d
Author: Antoine A <>
Date:   Mon, 30 Oct 2023 14:06:44 +0000

Fix bank install, remove unused dependencies and clean schema

Diffstat:
MMakefile | 4+++-
Mbank/build.gradle | 26++------------------------
Mbank/src/test/kotlin/AmountTest.kt | 5+++--
Mcontrib/docker-launcher/libeufin-bank.conf | 15++-------------
Mcontrib/libeufin-bank.conf | 15++-------------
Mdatabase-versioning/libeufin-bank-0001.sql | 149+++----------------------------------------------------------------------------
Mdatabase-versioning/libeufin-bank-procedures.sql | 22+++++++++++++---------
7 files changed, 29 insertions(+), 207 deletions(-)

diff --git a/Makefile b/Makefile @@ -45,7 +45,9 @@ install-bank: install -D database-versioning/versioning.sql -t $(bank_sql_dir) install -d $(spa_dir) cp contrib/wallet-core/demobank/* $(spa_dir)/ - ./gradlew -q -Pprefix=$(abs_destdir)$(prefix) bank:installToPrefix + ./gradlew bank:installShadowDist + install -d $(abs_destdir)$(prefix) + cp -r bank/build/install/bank-shadow/* -d $(abs_destdir)$(prefix) install-nexus: install -d $(nexus_config_dir) diff --git a/bank/build.gradle b/bank/build.gradle @@ -8,8 +8,8 @@ plugins { } java { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } version = rootProject.version @@ -26,38 +26,16 @@ compileTestKotlin { } } -tasks.register('installToPrefix', Copy) { - dependsOn(installShadowDist) - from("build/install/bank-shadow") { - include("**/libeufin-bank") - include("**/*.jar") - } - /** - * Reads from command line -Pkey=value options, - * with a default (/tmp) if the key is not found. - * - * project.findProperty('prefix') ?: '/tmp' - */ - into "${project.findProperty('prefix') ?: '/tmp'}" -} -// apply plugin: 'kotlinx-serialization' - sourceSets { main.java.srcDirs = ['src/main/kotlin'] } dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' - implementation 'ch.qos.logback:logback-classic:1.4.5' implementation project(":util") - // XML: - implementation "javax.xml.bind:jaxb-api:2.3.0" - implementation "org.glassfish.jaxb:jaxb-runtime:2.3.1" - implementation 'org.postgresql:postgresql:42.6.0' implementation 'com.zaxxer:HikariCP:5.0.1' - implementation group: 'org.apache.commons', name: 'commons-compress', version: '1.21' implementation "com.github.ajalt.clikt:clikt:4.2.1" implementation "io.ktor:ktor-server-core:$ktor_version" diff --git a/bank/src/test/kotlin/AmountTest.kt b/bank/src/test/kotlin/AmountTest.kt @@ -206,7 +206,7 @@ class AmountTest { @Test fun mul() = dbSetup { db -> db.conn { conn -> - val stmt = conn.prepareStatement("SELECT product.val, product.frac FROM amount_mul((?, ?)::taler_amount, (?, ?)::taler_amount) as product") + val stmt = conn.prepareStatement("SELECT product.val, product.frac FROM amount_mul((?, ?)::taler_amount, (?, ?)::taler_amount, NULL) as product") operator fun TalerAmount.times(increment: TalerAmount): TalerAmount? { stmt.setLong(1, value) stmt.setInt(2, frac) @@ -221,9 +221,10 @@ class AmountTest { }!! } + assertEquals(TalerAmount("EUR:6.41") * TalerAmount("EUR:4.69"), TalerAmount("EUR:30.0629")) assertEquals(TalerAmount("EUR:6.41") * TalerAmount("EUR:1.000001"), TalerAmount("EUR:6.41000641")) - assertEquals(TalerAmount("EUR:0.99999999") * TalerAmount("EUR:2.5"), TalerAmount("EUR:2.49999998")) + assertEquals(TalerAmount("EUR:0.99999999") * TalerAmount("EUR:2.5"), TalerAmount("EUR:2.49999997")) assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999") * TalerAmount("EUR:1"), TalerAmount("EUR:${TalerAmount.MAX_VALUE}.99999999")) assertEquals(TalerAmount("EUR:${TalerAmount.MAX_VALUE/4}") * TalerAmount("EUR:4"), TalerAmount("EUR:${TalerAmount.MAX_VALUE}")) assertException("ERROR: amount value overflowed") { TalerAmount(TalerAmount.MAX_VALUE/3, 0, "EUR") * TalerAmount(3, 1, "EUR") } diff --git a/contrib/docker-launcher/libeufin-bank.conf b/contrib/docker-launcher/libeufin-bank.conf @@ -11,15 +11,4 @@ PORT = 8080 [libeufin-bankdb-postgres] SQL_DIR = /usr/local/share/taler/sql/libeufin-bank/ -CONFIG = postgresql:///libeufinbank - -[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":"ク"} -\ No newline at end of file +CONFIG = postgresql:///libeufinbank +\ No newline at end of file diff --git a/contrib/libeufin-bank.conf b/contrib/libeufin-bank.conf @@ -39,15 +39,4 @@ PORT = 8080 SQL_DIR = $DATADIR/sql/ # DB connection string -CONFIG = postgresql:///libeufinbank - -[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":"ク"} -\ No newline at end of file +CONFIG = postgresql:///libeufinbank +\ No newline at end of file diff --git a/database-versioning/libeufin-bank-0001.sql b/database-versioning/libeufin-bank-0001.sql @@ -50,6 +50,10 @@ CREATE TYPE subscriber_state_enum CREATE TYPE stat_timeframe_enum AS ENUM ('hour', 'day', 'month', 'year'); +CREATE TYPE rounding_mode + AS ENUM ('nearest', 'up', 'down'); -- up is toward infinity and down toward zero + + -- FIXME: comments on types (see exchange for example)! -- start of: bank accounts @@ -182,151 +186,6 @@ COMMENT ON COLUMN cashout_operations.tan_code -- end of: cashout management --- start of: EBICS management - -CREATE TABLE IF NOT EXISTS ebics_hosts - (ebics_host_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,ebics_host_name TEXT NOT NULL - ,ebics_version TEXT NOT NULL -- FIXME: This should be an enum - -- FIXME: Do we want to specify the dialect here? - ,encryption_private_key BYTEA NOT NULL - ,signature_private_key BYTEA NOT NULL - ); - -CREATE TABLE IF NOT EXISTS ebics_subscribers - (ebics_subscriber_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,ebics_user_id TEXT NOT NULL - ,ebics_partner_id TEXT NOT NULL - ,ebics_system_id TEXT - ,ebics_host_id BIGINT NOT NULL REFERENCES ebics_hosts(ebics_host_id) - ,signature_key_rsa_pub BYTEA NOT NULL - ,signature_key_state subscriber_key_state_enum NOT NULL - ,encryption_key_rsa_pub BYTEA NOT NULL - ,encryption_key_state subscriber_key_state_enum NOT NULL - ,subscriber_state subscriber_state_enum DEFAULT 'new' NOT NULL - -- FIXME: Do we need some information about the next order ID? There is a bug open. - ); -COMMENT ON COLUMN ebics_subscribers.subscriber_state - IS 'Tracks the state changes of one subscriber.'; -- Really needed? - --- FIXME: comment this table --- FIXME: indices on both columns individually -CREATE TABLE IF NOT EXISTS ebics_subscribers_of_bank_accounts - (ebics_subscriber_id BIGINT NOT NULL - REFERENCES ebics_subscribers(ebics_subscriber_id) - ,bank_account_id BIGINT NOT NULL - REFERENCES bank_accounts(bank_account_id) - ); - - -CREATE TABLE IF NOT EXISTS ebics_download_transactions - (transaction_id TEXT PRIMARY KEY - ,order_type VARCHAR(3) NOT NULL - -- EBICS3: ,btf_type TEXT -- fixme: document: in EBICS 2.x this can be NULL - -- FIXME: see what else we need for EBICS 3 - ,ebics_host_id BIGINT NOT NULL - REFERENCES ebics_hosts(ebics_host_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ,ebics_subscriber BIGINT NOT NULL - REFERENCES ebics_subscribers(ebics_subscriber_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ,encoded_response TEXT NOT NULL - ,transaction_key_enc BYTEA NOT NULL - ,num_segments INT NOT NULL - ,segment_size INT NOT NULL - ,receipt_received BOOLEAN NOT NULL DEFAULT (FALSE) -- FIXME: Do we need this field if we anyway delete the entry after the receipt? - -- FIXME: Download start time for garbage collection / timeouts - ); - -COMMENT ON TABLE ebics_download_transactions - IS 'Tracks the evolution of one EBICS download transaction'; -COMMENT ON COLUMN ebics_download_transactions.ebics_host_id - IS 'EBICS host that governs this transaction'; -- exists for a multi-host scenario. - -CREATE TABLE IF NOT EXISTS ebics_upload_transactions - (ebics_transaction_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,order_type VARCHAR(3) NOT NULL - -- EBICS3: ,btf_type TEXT -- fixme: document: in EBICS 2.x this can be NULL - -- FIXME: see what else we need for EBICS 3 - ,order_id TEXT NOT NULL - ,ebics_host BIGINT NOT NULL - REFERENCES ebics_hosts(ebics_host_id) - ON DELETE RESTRICT - ON UPDATE RESTRICT - ,ebics_subscriber BIGINT NOT NULL - REFERENCES ebics_subscribers(ebics_subscriber_id) - ON DELETE RESTRICT - ON UPDATE RESTRICT - ,num_segments INT NOT NULL - ,last_seen_segment INT NOT NULL - ,transaction_key_enc BYTEA NOT NULL - -- FIXME: Download start time for garbage collection / timeouts - ); - -CREATE TABLE IF NOT EXISTS ebics_upload_transaction_chunks - (ebics_transaction_id BIGINT - REFERENCES ebics_upload_transactions(ebics_transaction_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ,upload_chunk_index INT NOT NULL - ,upload_chunk_content BYTEA NOT NULL - ); - --- FIXME: look at the code how it's used --- I *think* this is only used for upload orders. --- I am not sure if the signature (especially with VEU) --- can be uploaded before the order itself has been uploaded -CREATE TABLE IF NOT EXISTS ebics_order_signatures - (order_signature_id SERIAL PRIMARY KEY - ,ebics_transaction_id BIGINT - REFERENCES ebics_upload_transactions(ebics_transaction_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ,ebics_subscriber_id BIGINT - REFERENCES ebics_subscribers(ebics_subscriber_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - -- FIXME: do we also need to reference the ebics host? or does the subscriber uniquely determine it? - ,order_id TEXT NOT NULL - ,order_type TEXT NOT NULL - ,signature_algorithm TEXT NOT NULL - ,signature_value BYTEA NOT NULL - ); - -COMMENT ON TABLE ebics_order_signatures - IS 'Keeps signature data collected from the subscribers.'; - --- end of: EBICS management - --- start of: accounts activity report - --- Really keep this table? It tracks the EBICS reports. -CREATE TABLE IF NOT EXISTS bank_account_reports - (report_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,creation_time BIGINT NOT NULL - ,xml_message TEXT NOT NULL - ,bank_account BIGINT NOT NULL - REFERENCES bank_accounts(bank_account_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ); - --- Really keep this table? It tracks the EBICS statements --- mostly because they are supposed never to change. Not used -CREATE TABLE IF NOT EXISTS bank_account_statements - (statement_id BIGINT GENERATED BY DEFAULT AS IDENTITY UNIQUE - ,creation_time BIGINT NOT NULL - ,xml_message TEXT NOT NULL - ,bank_account BIGINT NOT NULL - REFERENCES bank_accounts(bank_account_id) - ON DELETE CASCADE - ON UPDATE RESTRICT - ,balance_clbd TEXT NOT NULL -- FIXME: name. balance_closing? - ); --- end of: accounts activity report - -- start of: Taler integration CREATE TABLE IF NOT EXISTS taler_exchange_outgoing (exchange_outgoing_id BIGINT GENERATED BY DEFAULT AS IDENTITY diff --git a/database-versioning/libeufin-bank-procedures.sql b/database-versioning/libeufin-bank-procedures.sql @@ -34,20 +34,22 @@ COMMENT ON FUNCTION amount_add CREATE OR REPLACE FUNCTION amount_mul( IN a taler_amount ,IN b taler_amount + ,IN rounding rounding_mode ,OUT product taler_amount ) LANGUAGE plpgsql AS $$ DECLARE - tmp NUMERIC(24, 8); -- 16 digit for val and 8 for frac + tmp NUMERIC(25, 9); -- 16 digit for val, 8 for frac and 1 for rounding error + rounding_error INT2; BEGIN - -- TODO write custom multiplication logic to get more control over rounding - tmp = (a.val::numeric(24, 8) + a.frac::numeric(24, 8) / 100000000) * (b.val::numeric(24, 8) + b.frac::numeric(24, 8) / 100000000); - product = (trunc(tmp)::int8, (tmp * 100000000 % 100000000)::int4); - IF (product.val > 1::bigint<<52) THEN - RAISE EXCEPTION 'amount value overflowed'; - END IF; + tmp = (a.val::numeric(25, 9) + a.frac::numeric(25, 9) / 100000000) * (b.val::numeric(25, 9) + b.frac::numeric(25, 9) / 100000000); + product = (trunc(tmp)::int8, (tmp * 1000000000 % 1000000000)::int4); + rounding_error = product.frac % 10; + product.frac = product.frac / 10; + -- TODO handle rounding + SELECT normalized.val, normalized.frac INTO product.val, product.frac FROM amount_normalize(product) as normalized; END $$; -COMMENT ON FUNCTION amount_mul -- TODO document rounding +COMMENT ON FUNCTION amount_mul IS 'Returns the product of two amounts. It raises an exception when the resulting .val is larger than 2^52'; CREATE OR REPLACE FUNCTION amount_left_minus_right( @@ -1271,12 +1273,14 @@ LANGUAGE plpgsql AS $$ DECLARE sell_at_ratio taler_amount; sell_out_fee taler_amount; + rounding_mode rounding_mode; calculation_ok BOOLEAN; BEGIN SELECT value['val']::int8, value['frac']::int4 INTO sell_at_ratio.val, sell_at_ratio.frac FROM config WHERE key='sell_at_ratio'; SELECT value['val']::int8, value['frac']::int4 INTO sell_out_fee.val, sell_out_fee.frac FROM config WHERE key='sell_out_fee'; + rounding_mode = 'nearest'; -- TODO rounding error config - SELECT product.val, product.frac INTO fiat_amount.val, fiat_amount.frac FROM amount_mul(internal_amount, sell_at_ratio) as product; + SELECT product.val, product.frac INTO fiat_amount.val, fiat_amount.frac FROM amount_mul(internal_amount, sell_at_ratio, rounding_mode) as product; SELECT (diff).val, (diff).frac, ok INTO fiat_amount.val, fiat_amount.frac, calculation_ok FROM amount_left_minus_right(fiat_amount, sell_out_fee); IF NOT calculation_ok THEN