From 457edfa47c7bf6bb49fa7cce6576ec46b524d38e Mon Sep 17 00:00:00 2001 From: swrup Date: Tue, 10 Feb 2026 07:59:24 +0100 Subject: [PATCH] --- .gitignore | 2 + dbinit_sql/drop.sql | 38 + dbinit_sql/init.sql | 14774 ++++++++++++++++++++++++++++++++++++++++++ tools/dbinit.sh | 99 + 4 files changed, 14913 insertions(+) create mode 100644 dbinit_sql/drop.sql create mode 100644 dbinit_sql/init.sql create mode 100755 tools/dbinit.sh diff --git a/.gitignore b/.gitignore index dbd4866f..d82974b0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ _build data/secmod/* !data/secmod/.gitkeep + +dbinit_sql diff --git a/dbinit_sql/drop.sql b/dbinit_sql/drop.sql new file mode 100644 index 00000000..e996e05f --- /dev/null +++ b/dbinit_sql/drop.sql @@ -0,0 +1,38 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see +-- + +-- Everything in one big transaction +BEGIN; + +WITH xpatches AS ( + SELECT patch_name + FROM _v.patches + WHERE starts_with(patch_name,'exchange-') +) + SELECT _v.unregister_patch(xpatches.patch_name) + FROM xpatches; + +WITH xpatches AS ( + SELECT patch_name + FROM _v.patches + WHERE starts_with(patch_name,'auditor-triggers-') +) + SELECT _v.unregister_patch(xpatches.patch_name) + FROM xpatches; + +DROP SCHEMA exchange CASCADE; + +COMMIT; diff --git a/dbinit_sql/init.sql b/dbinit_sql/init.sql new file mode 100644 index 00000000..5faebfec --- /dev/null +++ b/dbinit_sql/init.sql @@ -0,0 +1,14774 @@ + +-- LICENSE AND COPYRIGHT +-- +-- Copyright (C) 2010 Hubert depesz Lubaczewski +-- +-- This program is distributed under the (Revised) BSD License: +-- L +-- +-- Redistribution and use in source and binary forms, with or without +-- modification, are permitted provided that the following conditions +-- are met: +-- +-- * Redistributions of source code must retain the above copyright +-- notice, this list of conditions and the following disclaimer. +-- +-- * Redistributions in binary form must reproduce the above copyright +-- notice, this list of conditions and the following disclaimer in the +-- documentation and/or other materials provided with the distribution. +-- +-- * Neither the name of Hubert depesz Lubaczewski's Organization +-- nor the names of its contributors may be used to endorse or +-- promote products derived from this software without specific +-- prior written permission. +-- +-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +-- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +-- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +-- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +-- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +-- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +-- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-- +-- Code origin: https://gitlab.com/depesz/Versioning/blob/master/install.versioning.sql +-- +-- +-- # NAME +-- +-- **Versioning** - simplistic take on tracking and applying changes to databases. +-- +-- # DESCRIPTION +-- +-- This project strives to provide simple way to manage changes to +-- database. +-- +-- Instead of making changes on development server, then finding +-- differences between production and development, deciding which ones +-- should be installed on production, and finding a way to install them - +-- you start with writing diffs themselves! +-- +-- # INSTALLATION +-- +-- To install versioning simply run install.versioning.sql in your database +-- (all of them: production, stage, test, devel, ...). +-- +-- # USAGE +-- +-- In your files with patches to database, put whole logic in single +-- transaction, and use \_v.\* functions - usually \_v.register_patch() at +-- least to make sure everything is OK. +-- +-- For example. Let's assume you have patch files: +-- +-- ## 0001.sql: +-- +-- ``` +-- create table users (id serial primary key, username text); +-- ``` +-- +-- ## 0002.sql: +-- +-- ``` +-- insert into users (username) values ('depesz'); +-- ``` +-- To change it to use versioning you would change the files, to this +-- state: +-- +-- 0000.sql: +-- +-- ``` +-- BEGIN; +-- select _v.register_patch('000-base', NULL, NULL); +-- create table users (id serial primary key, username text); +-- COMMIT; +-- ``` +-- +-- ## 0002.sql: +-- +-- ``` +-- BEGIN; +-- select _v.register_patch('001-users', ARRAY['000-base'], NULL); +-- insert into users (username) values ('depesz'); +-- COMMIT; +-- ``` +-- +-- This will make sure that patch 001-users can only be applied after +-- 000-base. +-- +-- # AVAILABLE FUNCTIONS +-- +-- ## \_v.register_patch( TEXT ) +-- +-- Registers named patch, or dies if it is already registered. +-- +-- Returns integer which is id of patch in \_v.patches table - only if it +-- succeeded. +-- +-- ## \_v.register_patch( TEXT, TEXT[] ) +-- +-- Same as \_v.register_patch( TEXT ), but checks is all given patches (given as +-- array in second argument) are already registered. +-- +-- ## \_v.register_patch( TEXT, TEXT[], TEXT[] ) +-- +-- Same as \_v.register_patch( TEXT, TEXT[] ), but also checks if there are no conflicts with preexisting patches. +-- +-- Third argument is array of names of patches that conflict with current one. So +-- if any of them is installed - register_patch will error out. +-- +-- ## \_v.unregister_patch( TEXT ) +-- +-- Removes information about given patch from the versioning data. +-- +-- It doesn't remove objects that were created by this patch - just removes +-- metainformation. +-- +-- ## \_v.assert_user_is_superuser() +-- +-- Make sure that current patch is being loaded by superuser. +-- +-- If it's not - it will raise exception, and break transaction. +-- +-- ## \_v.assert_user_is_not_superuser() +-- +-- Make sure that current patch is not being loaded by superuser. +-- +-- If it is - it will raise exception, and break transaction. +-- +-- ## \_v.assert_user_is_one_of(TEXT, TEXT, ... ) +-- +-- Make sure that current patch is being loaded by one of listed users. +-- +-- If ```current_user``` is not listed as one of arguments - function will raise +-- exception and break the transaction. + +BEGIN; + + +-- This file adds versioning support to database it will be loaded to. +-- It requires that PL/pgSQL is already loaded - will raise exception otherwise. +-- All versioning "stuff" (tables, functions) is in "_v" schema. + +-- All functions are defined as 'RETURNS SETOF INT4' to be able to make them to RETURN literally nothing (0 rows). +-- >> RETURNS VOID<< IS similar, but it still outputs "empty line" in psql when calling +CREATE SCHEMA IF NOT EXISTS _v; +COMMENT ON SCHEMA _v IS 'Schema for versioning data and functionality.'; + +CREATE TABLE IF NOT EXISTS _v.patches ( + patch_name TEXT PRIMARY KEY, + applied_tsz TIMESTAMPTZ NOT NULL DEFAULT now(), + applied_by TEXT NOT NULL, + requires TEXT[], + conflicts TEXT[] +); +COMMENT ON TABLE _v.patches IS 'Contains information about what patches are currently applied on database.'; +COMMENT ON COLUMN _v.patches.patch_name IS 'Name of patch, has to be unique for every patch.'; +COMMENT ON COLUMN _v.patches.applied_tsz IS 'When the patch was applied.'; +COMMENT ON COLUMN _v.patches.applied_by IS 'Who applied this patch (PostgreSQL username)'; +COMMENT ON COLUMN _v.patches.requires IS 'List of patches that are required for given patch.'; +COMMENT ON COLUMN _v.patches.conflicts IS 'List of patches that conflict with given patch.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( IN in_patch_name TEXT, IN in_requirements TEXT[], in_conflicts TEXT[], OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + t_text TEXT; + t_text_a TEXT[]; + i INT4; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name; + IF FOUND THEN + RAISE EXCEPTION 'Patch % is already applied!', in_patch_name; + END IF; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE patch_name = any( in_conflicts ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Versioning patches conflict. Conflicting patche(s) installed: %.', array_to_string( t_text_a, ', ' ); + END IF; + + IF array_upper( in_requirements, 1 ) IS NOT NULL THEN + t_text_a := '{}'; + FOR i IN array_lower( in_requirements, 1 ) .. array_upper( in_requirements, 1 ) LOOP + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_requirements[i]; + IF NOT FOUND THEN + t_text_a := t_text_a || in_requirements[i]; + END IF; + END LOOP; + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Missing prerequisite(s): %.', array_to_string( t_text_a, ', ' ); + END IF; + END IF; + + INSERT INTO _v.patches (patch_name, applied_tsz, applied_by, requires, conflicts ) VALUES ( in_patch_name, now(), current_user, coalesce( in_requirements, '{}' ), coalesce( in_conflicts, '{}' ) ); + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[], TEXT[] ) IS 'Function to register patches in database. Raises exception if there are conflicts, prerequisites are not installed or the migration has already been installed.'; + +CREATE OR REPLACE FUNCTION _v.register_patch( TEXT, TEXT[] ) RETURNS setof INT4 AS $$ + SELECT _v.register_patch( $1, $2, NULL ); +$$ language sql; +COMMENT ON FUNCTION _v.register_patch( TEXT, TEXT[] ) IS 'Wrapper to allow registration of patches without conflicts.'; +CREATE OR REPLACE FUNCTION _v.register_patch( TEXT ) RETURNS setof INT4 AS $$ + SELECT _v.register_patch( $1, NULL, NULL ); +$$ language sql; +COMMENT ON FUNCTION _v.register_patch( TEXT ) IS 'Wrapper to allow registration of patches without requirements and conflicts.'; + +CREATE OR REPLACE FUNCTION _v.unregister_patch( IN in_patch_name TEXT, OUT versioning INT4 ) RETURNS setof INT4 AS $$ +DECLARE + i INT4; + t_text_a TEXT[]; +BEGIN + -- Thanks to this we know only one patch will be applied at a time + LOCK TABLE _v.patches IN EXCLUSIVE MODE; + + t_text_a := ARRAY( SELECT patch_name FROM _v.patches WHERE in_patch_name = ANY( requires ) ); + IF array_upper( t_text_a, 1 ) IS NOT NULL THEN + RAISE EXCEPTION 'Cannot uninstall %, as it is required by: %.', in_patch_name, array_to_string( t_text_a, ', ' ); + END IF; + + DELETE FROM _v.patches WHERE patch_name = in_patch_name; + GET DIAGNOSTICS i = ROW_COUNT; + IF i < 1 THEN + RAISE EXCEPTION 'Patch % is not installed, so it can''t be uninstalled!', in_patch_name; + END IF; + + RETURN; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.unregister_patch( TEXT ) IS 'Function to unregister patches in database. Dies if the patch is not registered, or if unregistering it would break dependencies.'; + +CREATE OR REPLACE FUNCTION _v.assert_patch_is_applied( IN in_patch_name TEXT ) RETURNS TEXT as $$ +DECLARE + t_text TEXT; +BEGIN + SELECT patch_name INTO t_text FROM _v.patches WHERE patch_name = in_patch_name; + IF NOT FOUND THEN + RAISE EXCEPTION 'Patch % is not applied!', in_patch_name; + END IF; + RETURN format('Patch %s is applied.', in_patch_name); +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_patch_is_applied( TEXT ) IS 'Function that can be used to make sure that patch has been applied.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_superuser() RETURNS TEXT as $$ +DECLARE + v_super bool; +BEGIN + SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user; + IF v_super THEN + RETURN 'assert_user_is_superuser: OK'; + END IF; + RAISE EXCEPTION 'Current user is not superuser - cannot continue.'; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_superuser() IS 'Function that can be used to make sure that patch is being applied using superuser account.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_not_superuser() RETURNS TEXT as $$ +DECLARE + v_super bool; +BEGIN + SELECT usesuper INTO v_super FROM pg_user WHERE usename = current_user; + IF v_super THEN + RAISE EXCEPTION 'Current user is superuser - cannot continue.'; + END IF; + RETURN 'assert_user_is_not_superuser: OK'; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_not_superuser() IS 'Function that can be used to make sure that patch is being applied using normal (not superuser) account.'; + +CREATE OR REPLACE FUNCTION _v.assert_user_is_one_of(VARIADIC p_acceptable_users TEXT[] ) RETURNS TEXT as $$ +DECLARE +BEGIN + IF current_user = any( p_acceptable_users ) THEN + RETURN 'assert_user_is_one_of: OK'; + END IF; + RAISE EXCEPTION 'User is not one of: % - cannot continue.', p_acceptable_users; +END; +$$ language plpgsql; +COMMENT ON FUNCTION _v.assert_user_is_one_of(TEXT[]) IS 'Function that can be used to make sure that patch is being applied by one of defined users.'; + +COMMIT; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see +-- + +BEGIN; + +SELECT _v.register_patch('exchange-0001', NULL, NULL); + +CREATE SCHEMA exchange; +COMMENT ON SCHEMA exchange IS 'taler-exchange data'; + +SET search_path TO exchange; + +--------------------------------------------------------------------------- +-- General procedures for DB setup +--------------------------------------------------------------------------- + +CREATE TABLE exchange_tables + (table_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY + ,name TEXT NOT NULL + ,version TEXT NOT NULL + ,action TEXT NOT NULL + ,partitioned BOOL NOT NULL + ,by_range BOOL NOT NULL + ,finished BOOL NOT NULL DEFAULT(FALSE)); +COMMENT ON TABLE exchange_tables + IS 'Tables of the exchange and their status'; +COMMENT ON COLUMN exchange_tables.name + IS 'Base name of the table (without partition/shard)'; +COMMENT ON COLUMN exchange_tables.version + IS 'Version of the DB in which the given action happened'; +COMMENT ON COLUMN exchange_tables.action + IS 'Action to take on the table (e.g. create, alter, constrain, or foreign). Create is done for the master table and each partition; constrain is only for partitions or for master if there are no partitions; master only on master (takes no argument); foreign only on master if there are no partitions.'; +COMMENT ON COLUMN exchange_tables.partitioned + IS 'TRUE if the table is partitioned'; +COMMENT ON COLUMN exchange_tables.by_range + IS 'TRUE if the table is partitioned by range'; +COMMENT ON COLUMN exchange_tables.finished + IS 'TRUE if the respective migration has been run'; + +CREATE INDEX exchange_tables_by_pending + ON exchange_tables (table_serial_id) + WHERE NOT finished; +COMMENT ON INDEX exchange_tables_by_pending + IS 'Used by exchange_do_create_tables'; + + + +COMMIT; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2023 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = fee' + ,'commitment' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The sum of all contributions of all deposit that reference this policy. Invariant: The fulfilment_state must be Insufficient as long as accumulated_total < commitment' + ,'accumulated_total' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The fee for this policy, due when the policy is fulfilled or timed out' + ,'fee' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The amount that on fulfillment or timeout will be transferred to the payto-URI''s of the corresponding deposit''s. The policy fees must have been already deducted from it. Invariant: fee+transferable <= accumulated_total. The remaining amount (accumulated_total - fee - transferable) can be refreshed by the owner of the coins when the state is Timeout or Success.' + ,'transferable' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'State of the fulfillment: + - 0 (Failure) + - 1 (Insufficient) + - 2 (Ready) + - 4 (Success) + - 5 (Timeout)' + ,'fulfillment_state' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Reference to the proof of the fulfillment of this policy, if it exists. Invariant: If not NULL, this entry''s .hash_code MUST be part of the corresponding policy_fulfillments.policy_hash_codes array.' + ,'h_fulfillment_proof' + ,table_name + ,partition_suffix + ); +END +$$; +COMMENT ON FUNCTION create_table_policy_details + IS 'Creates the policy_details table'; +CREATE FUNCTION constrain_table_policy_details( + IN partition_suffix TEXT +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + partition_name TEXT; +BEGIN + partition_name = concat_ws('_', 'policy_details', partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || partition_name || + ' ADD CONSTRAINT ' || partition_name || '_unique_serial_id ' + ' UNIQUE (policy_details_serial_id)' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || partition_name || + ' ADD CONSTRAINT ' || partition_name || '_unique_hash_fulfillment_proof ' + ' UNIQUE (policy_hash_code, h_fulfillment_proof)' + ); + EXECUTE FORMAT ( + 'CREATE INDEX ' || partition_name || '_policy_hash_code' + ' ON ' || partition_name || + ' (policy_hash_code);' + ); +END +$$; +CREATE OR REPLACE FUNCTION foreign_table_policy_details() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'policy_details'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_policy_fulfillments' + ' FOREIGN KEY (h_fulfillment_proof) ' + ' REFERENCES policy_fulfillments (h_fulfillment_proof) ON DELETE RESTRICT' + ); +END +$$; +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) +VALUES + ('policy_details', 'exchange-0002', 'create', TRUE ,FALSE), + ('policy_details', 'exchange-0002', 'constrain', TRUE ,FALSE), + ('policy_details', 'exchange-0002', 'foreign', TRUE ,FALSE); +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see =0)' + ',planchets_h BYTEA CONSTRAINT planchets_h_length CHECK(LENGTH(planchets_h)=64)' + ',selected_h BYTEA CONSTRAINT selected_h_length CHECK(LENGTH(selected_h)=64)' + ',blinding_seed BYTEA CONSTRAINT blinding_seed_length CHECK(LENGTH(blinding_seed)>=32)' + ',cs_r_values BYTEA[]' + ',cs_r_choices INT8' + ',denom_serials INT8[] NOT NULL CONSTRAINT denom_serials_array_length CHECK(cardinality(denom_serials)=cardinality(denom_sigs))' + ',denom_sigs BYTEA[] NOT NULL CONSTRAINT denom_sigs_array_length CHECK(cardinality(denom_sigs)=cardinality(denom_serials))' + ') %s ;' + ,table_name + ,'PARTITION BY HASH (rc)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'The data provided by the client for the melting operation of an old coin and he choices made by the exchange ' + ' with respect to the cut-and-choose protocol: nonreveal_index and the corresponding chosen' + ' blinded coin envelope along with the denomination signatures at the moment of the melting.' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The hash over the refresh request, which serves as the primary key' + ' for the lookup during the reveal phase.' + ,'rc' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The publice nonce from which all other nonces for all n*kappa coin candidates are derived for which' + ' the old coin proves ownership via signatures' + ,'refresh_seed' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The gamma value chosen by the exchange in the cut-and-choose protocol' + ,'noreveal_index' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The date of execution of the melting operation, according to the exchange' + ,'execution_date' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Reference to the public key of the old coin which is melted' + ,'old_coin_pub' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Signature of the old coin''s private key over the melt request' + ,'old_coin_sig' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of references to the denominations' + ,'denom_serials' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The master seed for the blinding nonces, needed for blind CS signatures; maybe NULL' + ,'blinding_seed' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The public pairs of R-values provided by the exchange for the CS denominations; might be NULL' + ,'cs_r_values' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The bitvector of choices made by the exchange for each of the pairs in cs_r_values; maybe NULL.' + 'The vector is stored in network byte order and the lowest bit corresponds to the 0-th entry in cs_r_values (pair)' + ,'cs_r_choices' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The hash over all kappa*n blinded planchets that were provided by the client' + ,'planchets_h' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The hash over the n blinded planchets that were selected by the exchange.' + ,'selected_h' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of signatures, one for each blinded envelope' + ,'denom_sigs' + ,table_name + ,partition_suffix + ); +END +$$; +CREATE FUNCTION constrain_table_refresh( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'refresh'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + -- Note: index spans partitions, may need to be materialized. + EXECUTE FORMAT ( + 'CREATE INDEX ' || table_name || '_by_old_coin_pub_index ' + 'ON ' || table_name || ' ' + '(old_coin_pub);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_refresh_id_key' + ' UNIQUE (refresh_id);' + ); +END +$$; +CREATE FUNCTION foreign_table_refresh() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'refresh'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_coin_pub' + ' FOREIGN KEY (old_coin_pub) ' + ' REFERENCES known_coins (coin_pub) ON DELETE CASCADE' + ); +END +$$; +-- Trigger to update the reserve_history table +CREATE FUNCTION refresh_insert_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + INSERT INTO coin_history + (coin_pub + ,table_name + ,serial_id) + VALUES + (NEW.old_coin_pub + ,'refresh' + ,NEW.refresh_id); + RETURN NEW; +END $$; +COMMENT ON FUNCTION refresh_insert_trigger() + IS 'Keep track of a particular refresh in the coin_history table.'; +-- Trigger to update the unique_refresh_blinding_seed table +CREATE FUNCTION refresh_delete_trigger() + RETURNS trigger + LANGUAGE plpgsql + AS $$ +BEGIN + DELETE FROM unique_refresh_blinding_seed + WHERE blinding_seed = OLD.blinding_seed; + RETURN OLD; +END $$; +COMMENT ON FUNCTION refresh_delete_trigger() + IS 'Delete blinding_seed from unique_refresh_blinding_seed table.'; +-- Put the triggers into the master table +CREATE FUNCTION master_table_refresh() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + CREATE TRIGGER refresh_on_insert + AFTER INSERT + ON refresh + FOR EACH ROW EXECUTE FUNCTION refresh_insert_trigger(); + CREATE TRIGGER refresh_on_delete + AFTER DELETE + ON refresh + FOR EACH ROW EXECUTE FUNCTION refresh_delete_trigger(); +END $$; +COMMENT ON FUNCTION master_table_refresh() + IS 'Setup triggers to replicate refresh into coin_history and delete blinding_seed from unique_refresh_blinding_seed.'; +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) +VALUES + ('refresh', 'exchange-0002', 'create', TRUE ,FALSE), + ('refresh', 'exchange-0002', 'constrain',TRUE ,FALSE), + ('refresh', 'exchange-0002', 'foreign', TRUE ,FALSE), + ('refresh', 'exchange-0002', 'master', TRUE ,FALSE); +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see =0)' + ',noreveal_index SMALLINT CONSTRAINT noreveal_index_positive CHECK(noreveal_index>=0)' + ',selected_h BYTEA CONSTRAINT selected_h_length CHECK(LENGTH(selected_h)=64)' + ',blinding_seed BYTEA CONSTRAINT blinding_seed_length CHECK(LENGTH(blinding_seed)>=32)' + ',cs_r_values BYTEA[]' + ',cs_r_choices INT8' + ',denom_serials INT8[] NOT NULL CONSTRAINT denom_serials_array_length CHECK(cardinality(denom_serials)=cardinality(denom_sigs))' + ',denom_sigs BYTEA[] NOT NULL CONSTRAINT denom_sigs_array_length CHECK(cardinality(denom_sigs)=cardinality(denom_serials))' + ') %s ;' + ,table_name + ,'PARTITION BY HASH (reserve_pub)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'Commitments made when withdrawing coins and, in case of required proof of age restriction, the gamma value chosen by the exchange. ' + 'It also contains the blindly signed coins, their signatures and denominations.' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'If the client explicitly commits to age-restricted coins, the gamma value chosen by the exchange in the cut-and-choose protocol; NULL if we did not use age-withdraw.' + ,'noreveal_index' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The running hash over all committed blinded planchets. Needed for recoup and (when a proof of age-restriction was required); NULL if we did not use age-withdraw.' + ' in the subsequent cut-and-choose protocol.' + ,'planchets_h' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The date of execution of this withdrawal, according to the exchange' + ,'execution_date' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'If the clients commits to age-restricted coins, the maximum age (in years) that the client explicitly commits to with this request; NULL if we did not use age-withdraw.' + ,'max_age' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Reference to the public key of the reserve from which the coins are going to be withdrawn' + ,'reserve_pub' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Signature of the reserve''s private key over the withdraw request' + ,'reserve_sig' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of references to the denominations' + ,'denom_serials' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'In case of age restriction, the hash of the chosen (noreveal_index) blinded envelopes; NULL if we did not use age-withdraw.' + ,'selected_h' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'Array of signatures over each blinded envelope. If age-proof was not required, the signed envelopes are the ones' + ' hashed into planchet_h. Otherwise (when age-proof is required) the selected planchets (noreveal_index) were signed,' + ' hashed into selected_h.' + ,'denom_sigs' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The master seed for the blinding nonces, needed for blind CS signatures; NULL if we did not use age-withdraw or CS' + ,'blinding_seed' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The pairs of R-values (calculated by the exchange) for the coins of cipher type Clause-Schnorr, based on the blinding_seed; maybe NULL if we did not use CS.' + ,'cs_r_values' + ,table_name + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'The bitvector of choices made by the exchange for each of the pairs in cs_r_values; NULL if we did not use CS.' + 'The vector is stored in network byte order and the lowest bit corresponds to the 0-th entry in cs_r_values (pair)' + ,'cs_r_choices' + ,table_name + ,partition_suffix + ); +END +$$; +CREATE FUNCTION constrain_table_withdraw( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'withdraw'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'CREATE INDEX ' || table_name || '_by_reserve_pub_index ' + 'ON ' || table_name || ' ' + '(reserve_pub);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD PRIMARY KEY (reserve_pub, planchets_h);' + ); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_withdraw_id_key' + ' UNIQUE (withdraw_id);' + ); +END +$$; +CREATE FUNCTION foreign_table_withdraw() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'withdraw'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_foreign_reserve_pub' + ' FOREIGN KEY (reserve_pub)' + ' REFERENCES reserves(reserve_pub) ON DELETE CASCADE;' + ); +END +$$; +-- Put the triggers into the master table +CREATE FUNCTION master_table_withdraw() + RETURNS void + LANGUAGE plpgsql + AS $$ +BEGIN + CREATE TRIGGER withdraw_on_insert + AFTER INSERT + ON withdraw + FOR EACH ROW EXECUTE FUNCTION withdraw_insert_trigger(); + CREATE TRIGGER withdraw_on_delete + AFTER DELETE + ON withdraw + FOR EACH ROW EXECUTE FUNCTION withdraw_delete_trigger(); +END $$; +COMMENT ON FUNCTION master_table_withdraw() + IS 'Setup triggers to replicate withdraw into reserve_history and delete blinding_seed from unique_withdraw_blinding_seed.'; +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) +VALUES + ('withdraw', 'exchange-0002', 'create', TRUE ,FALSE), + ('withdraw', 'exchange-0002', 'constrain',TRUE ,FALSE), + ('withdraw', 'exchange-0002', 'foreign', TRUE ,FALSE), + ('withdraw', 'exchange-0002', 'master', TRUE ,FALSE); +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) + ,precisions INT8[] NOT NULL CHECK (array_length(precisions,1) > 0) + ,UNIQUE(slug,stype) + ,CONSTRAINT equal_array_length + CHECK (array_length(ranges,1) = + array_length(precisions,1)) + ); +COMMENT ON TABLE exchange_statistic_interval_meta + IS 'meta data about an interval statistic we are tracking'; +COMMENT ON COLUMN exchange_statistic_interval_meta.imeta_serial_id + IS 'unique identifier for this type of interval statistic we are tracking'; +COMMENT ON COLUMN exchange_statistic_interval_meta.origin + IS 'which customization schema does this statistic originate from (used for easy deletion)'; +COMMENT ON COLUMN exchange_statistic_interval_meta.slug + IS 'keyword (or name) of the statistic; identifies what the statistic is about; should be a slug suitable for a URI path'; +COMMENT ON COLUMN exchange_statistic_interval_meta.description + IS 'description of the statistic being tracked'; +COMMENT ON COLUMN exchange_statistic_interval_meta.stype + IS 'statistic type, what kind of data is being tracked, amount or number'; +COMMENT ON COLUMN exchange_statistic_interval_meta.ranges + IS 'range of values that is being kept for this statistic, in seconds, must be monotonically increasing'; +COMMENT ON COLUMN exchange_statistic_interval_meta.precisions + IS 'determines how precisely we track which events fall into the range at the same index (allowing us to coalesce events with timestamps in proximity close to the given precision), in seconds, 0 is not allowed'; +CREATE INDEX exchange_statistic_interval_meta_by_origin + ON exchange_statistic_interval_meta + (origin); +CREATE FUNCTION create_table_exchange_statistic_counter_event ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(nevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' + ',imeta_serial_id INT8' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',slot INT8 NOT NULL' + ',delta INT8 NOT NULL' + ',UNIQUE (h_payto,imeta_serial_id,slot)' + ') %s ;' + ,'exchange_statistic_counter_event' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'number to decrement an interval statistic by when a certain time value is reached' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'unique identifier for this number event' + ,'nevent_serial_id' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about; must be of stype number' + ,'imeta_serial_id' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies the time slot at which the given event(s) happened, rounded down by the respective precisions value' + ,'slot' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'total cumulative number that was added at the time identified by slot' + ,'delta' + ,'exchange_statistic_counter_event' + ,partition_suffix + ); +END $$; +CREATE FUNCTION constrain_table_exchange_statistic_counter_event( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT default 'exchange_statistic_counter_event'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_nevent_serial_id_key' + ' UNIQUE (nevent_serial_id)' + ); +END $$; +CREATE FUNCTION create_table_exchange_statistic_interval_counter ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(imeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',range INT8 NOT NULL' + ',event_delimiter INT8 NOT NULL' + ',cumulative_number INT8 NOT NULL' + ',UNIQUE (h_payto,imeta_serial_id,range)' + ') %s ;' + ,'exchange_statistic_interval_counter' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'various numeric statistics (cumulative counters) being tracked' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'imeta_serial_id' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' + ,'range' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'determines the last event currently included in the interval' + ,'event_delimiter' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'aggregate (sum) of tracked by the statistic; what exactly is tracked is determined by the keyword' + ,'cumulative_number' + ,'exchange_statistic_interval_counter' + ,partition_suffix + ); +END $$; +CREATE FUNCTION foreign_table_exchange_statistic_interval_counter() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'exchange_statistic_interval_counter'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' + ' FOREIGN KEY (event_delimiter) ' + ' REFERENCES exchange_statistic_counter_event (nevent_serial_id) ON DELETE RESTRICT' + ); +END $$; +CREATE FUNCTION create_table_exchange_statistic_amount_event ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(aevent_serial_id INT8 GENERATED BY DEFAULT AS IDENTITY' + ',imeta_serial_id INT8' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',slot INT8 NOT NULL' + ',delta taler_amount NOT NULL' + ',CHECK ((delta).val IS NOT NULL AND (delta).frac IS NOT NULL)' + ',CONSTRAINT event_key UNIQUE (h_payto,imeta_serial_id,slot)' + ') %s ;' + ,'exchange_statistic_amount_event' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'amount to decrement an interval statistic by when a certain time value is reached' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'unique identifier for this amount event' + ,'aevent_serial_id' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about; must be of clazz interval and of stype amount' + ,'imeta_serial_id' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies the time slot at which the given event(s) happened' + ,'slot' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'total cumulative amount that was added at the time identified by slot' + ,'delta' + ,'exchange_statistic_amount_event' + ,partition_suffix + ); +END $$; +CREATE FUNCTION constrain_table_exchange_statistic_amount_event( + IN partition_suffix TEXT +) +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT default 'exchange_statistic_amount_event'; +BEGIN + table_name = concat_ws('_', table_name, partition_suffix); + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_aevent_serial_id_key' + ' UNIQUE (aevent_serial_id)' + ); +END $$; +CREATE FUNCTION create_table_exchange_statistic_interval_amount ( + IN partition_suffix TEXT DEFAULT NULL +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + PERFORM create_partitioned_table( + 'CREATE TABLE %I' + '(imeta_serial_id INT8 NOT NULL' + ' REFERENCES exchange_statistic_interval_meta (imeta_serial_id) ON DELETE CASCADE' + ',h_payto BYTEA CHECK (LENGTH(h_payto)=32)' + ',event_delimiter INT8 NOT NULL' + ',range INT8 NOT NULL' + ',cumulative_value taler_amount NOT NULL' + ',CHECK ((cumulative_value).val IS NOT NULL AND (cumulative_value).frac IS NOT NULL)' + ',UNIQUE (h_payto,imeta_serial_id,range)' + ') %s ;' + ,'exchange_statistic_interval_amount' + ,'PARTITION BY HASH(h_payto)' + ,partition_suffix + ); + PERFORM comment_partitioned_table( + 'various amount statistics being tracked' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies what the statistic is about' + ,'imeta_serial_id' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'identifies an account (hash of normalized payto) for which the statistic is kept, NULL for global statistics' + ,'h_payto' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'for which range is this the counter; note that the cumulative_number excludes the values already stored in smaller ranges' + ,'range' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); + PERFORM comment_partitioned_column( + 'amount affected by the event' + ,'cumulative_value' + ,'exchange_statistic_interval_amount' + ,partition_suffix + ); +END $$; +CREATE FUNCTION foreign_table_exchange_statistic_interval_amount() +RETURNS VOID +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'exchange_statistic_interval_amount'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD CONSTRAINT ' || table_name || '_event_delimiter_foreign_key' + ' FOREIGN KEY (event_delimiter) ' + ' REFERENCES exchange_statistic_amount_event (aevent_serial_id) ON DELETE RESTRICT' + ); +END $$; +CREATE TYPE exchange_statistic_interval_number_get_return_value + AS + (range INT8 + ,rvalue INT8 + ); +COMMENT ON TYPE exchange_statistic_interval_number_get_return_value + IS 'Return type for exchange_statistic_interval_number_get stored procedure'; +CREATE TYPE exchange_statistic_interval_amount_get_return_value + AS + (range INT8 + ,rvalue taler_amount + ); +COMMENT ON TYPE exchange_statistic_interval_amount_get_return_value + IS 'Return type for exchange_statistic_interval_amount_get stored procedure'; +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('exchange_statistic_bucket_counter' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_bucket_amount' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_counter_event' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_counter_event' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_counter' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_counter' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE), + ('exchange_statistic_amount_event' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_amount_event' + ,'exchange-0009' + ,'constrain' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_amount' + ,'exchange-0009' + ,'create' + ,TRUE + ,FALSE), + ('exchange_statistic_interval_amount' + ,'exchange-0009' + ,'foreign' + ,TRUE + ,FALSE); +COMMIT; +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see +-- + +BEGIN; + +SELECT _v.register_patch('exchange-0005', NULL, NULL); + +SET search_path TO exchange; + +-- convert all JSON-valued fields from TEXT to JSONB + +CREATE FUNCTION alter_table_wire_accounts5() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE wire_accounts + ALTER COLUMN debit_restrictions + TYPE JSONB + USING debit_restrictions::JSONB, + ALTER COLUMN credit_restrictions + TYPE JSONB + USING credit_restrictions::JSONB; +END +$$; + + +CREATE FUNCTION alter_table_legitimization_outcomes5() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE legitimization_outcomes + ALTER COLUMN jproperties + TYPE JSONB + USING jproperties::JSONB, + ALTER COLUMN jnew_rules + TYPE JSONB + USING jnew_rules::JSONB; +END +$$; + + +CREATE FUNCTION alter_table_legitimization_measures5() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE legitimization_measures + ALTER COLUMN jmeasures + TYPE JSONB + USING jmeasures::JSONB; +END +$$; + + +CREATE FUNCTION alter_table_policy_fulfillments5() +RETURNS void +LANGUAGE plpgsql +AS $$ +BEGIN + ALTER TABLE policy_fulfillments + ALTER COLUMN fulfillment_proof + TYPE JSONB + USING fulfillment_proof::JSONB; +END +$$; + + +CREATE FUNCTION alter_table_kyc_targets5() +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE + table_name TEXT DEFAULT 'kyc_targets'; +BEGIN + EXECUTE FORMAT ( + 'ALTER TABLE ' || table_name || + ' ADD COLUMN open_time INT8 DEFAULT(NULL)' + ',ADD COLUMN close_time INT8 DEFAULT(NULL);' + ); +END +$$; + + +INSERT INTO exchange_tables + (name + ,version + ,action + ,partitioned + ,by_range) + VALUES + ('wire_accounts5' + ,'exchange-0005' + ,'alter' + ,TRUE + ,FALSE), + ('legitimization_outcomes5' + ,'exchange-0005' + ,'alter' + ,TRUE + ,FALSE), + ('legitimization_measures5' + ,'exchange-0005' + ,'alter' + ,TRUE + ,FALSE), + ('policy_fulfillments5' + ,'exchange-0005' + ,'alter' + ,TRUE + ,FALSE), + ('kyc_targets5' + ,'exchange-0005' + ,'alter' + ,TRUE + ,FALSE); + + +COMMIT; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0: sharding +) +RETURNS VOID +LANGUAGE plpgsql +AS $$ +BEGIN + IF (partition_suffix IS NULL) + THEN + -- no partitioning, disable option + main_table_partition_str = ''; + ELSE + IF (partition_suffix::int > 0) + THEN + -- sharding, add shard name + table_name=table_name || '_' || partition_suffix; + END IF; + END IF; + EXECUTE FORMAT( + table_definition, + table_name, + main_table_partition_str + ); +END $$; +COMMENT ON FUNCTION create_partitioned_table + IS 'Generic function to create a table that is partitioned or sharded.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) ) + THEN + -- sharding, add shard name + table_name=table_name || '_' || partition_suffix; + END IF; + EXECUTE FORMAT( + 'COMMENT ON TABLE %s IS %s' + ,table_name + ,quote_literal(table_comment) + ); +END $$; +COMMENT ON FUNCTION comment_partitioned_table + IS 'Generic function to create a comment on table that is partitioned.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) ) + THEN + -- sharding, add shard name + table_name=table_name || '_' || partition_suffix; + END IF; + EXECUTE FORMAT( + 'COMMENT ON COLUMN %s.%s IS %s' + ,table_name + ,column_name + ,quote_literal(table_comment) + ); +END $$; +COMMENT ON FUNCTION comment_partitioned_column + IS 'Generic function to create a comment on column of a table that is partitioned.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 1: normal partitions +) + RETURNS VOID + LANGUAGE plpgsql +AS $$ +DECLARE + tc CURSOR FOR + SELECT table_serial_id + ,name + ,action + ,partitioned + ,by_range + FROM exchange.exchange_tables + WHERE NOT finished + ORDER BY table_serial_id ASC; +BEGIN + FOR rec IN tc + LOOP + CASE rec.action + -- "create" actions apply to master and partitions, providing the partition ID to the creation function (if any) + WHEN 'create' + THEN + IF (rec.partitioned AND + (num_partitions IS NOT NULL)) + THEN + -- Create master table with partitioning. + EXECUTE FORMAT( + 'SELECT exchange.create_table_%s (%s)'::text + ,rec.name + ,quote_literal('0') + ); + IF (rec.by_range OR + (num_partitions = 0)) + THEN + -- Create default partition. + IF (rec.by_range) + THEN + -- Range partition + EXECUTE FORMAT( + 'CREATE TABLE exchange.%s_default' + ' PARTITION OF %s' + ' DEFAULT' + ,rec.name + ,rec.name + ); + ELSE + -- Hash partition + EXECUTE FORMAT( + 'CREATE TABLE exchange.%s_default' + ' PARTITION OF %s' + ' FOR VALUES WITH (MODULUS 1, REMAINDER 0)' + ,rec.name + ,rec.name + ); + END IF; + ELSE + FOR i IN 1..num_partitions LOOP + -- Create num_partitions + EXECUTE FORMAT( + 'CREATE TABLE exchange.%I' + ' PARTITION OF %I' + ' FOR VALUES WITH (MODULUS %s, REMAINDER %s)' + ,rec.name || '_' || i + ,rec.name + ,num_partitions + ,i-1 + ); + END LOOP; + END IF; + ELSE + -- Only create master table. No partitions. + EXECUTE FORMAT( + 'SELECT exchange.create_table_%s ()'::text + ,rec.name + ); + END IF; + EXECUTE FORMAT( + 'DROP FUNCTION exchange.create_table_%s'::text + ,rec.name + ); + -- "alter" actions apply to master and partitions (but are called without partition ID, as altering master applies to partitions for these); use when changing table layouts (adding or removing columns). + WHEN 'alter' + THEN + -- Alter master table. + EXECUTE FORMAT( + 'SELECT exchange.alter_table_%s ()'::text + ,rec.name + ); + EXECUTE FORMAT( + 'DROP FUNCTION exchange.alter_table_%s'::text + ,rec.name + ); + -- Constrain action apply to master OR each partition (but not on master if we have partitions); use to create (or remove) indices or constraints that apply to a partition and may not be aligned with the partition key + WHEN 'constrain' + THEN + ASSERT rec.partitioned, 'constrain action only applies to partitioned tables'; + IF (num_partitions IS NULL) + THEN + -- Constrain master table + EXECUTE FORMAT( + 'SELECT exchange.constrain_table_%s (NULL)'::text + ,rec.name + ); + ELSE + IF ( (num_partitions = 0) OR + (rec.by_range) ) + THEN + -- Constrain default table + EXECUTE FORMAT( + 'SELECT exchange.constrain_table_%s (%s)'::text + ,rec.name + ,quote_literal('default') + ); + ELSE + -- Constrain each partition + FOR i IN 1..num_partitions LOOP + EXECUTE FORMAT( + 'SELECT exchange.constrain_table_%s (%s)'::text + ,rec.name + ,quote_literal(i) + ); + END LOOP; + END IF; + END IF; + EXECUTE FORMAT( + 'DROP FUNCTION exchange.constrain_table_%s'::text + ,rec.name + ); + -- Foreign actions only apply if partitioning is off; use for foreign-key constraints that may span partitions + WHEN 'foreign' + THEN + IF (num_partitions IS NULL) + THEN + -- Add foreign constraints + EXECUTE FORMAT( + 'SELECT exchange.foreign_table_%s (%s)'::text + ,rec.name + ,NULL + ); + END IF; + EXECUTE FORMAT( + 'DROP FUNCTION exchange.foreign_table_%s'::text + ,rec.name + ); + -- "master" actions only apply to the master table + WHEN 'master' + THEN + EXECUTE FORMAT( + 'SELECT exchange.master_table_%s ()'::text + ,rec.name + ); + EXECUTE FORMAT( + 'DROP FUNCTION exchange.master_table_%s'::text + ,rec.name + ); + ELSE + ASSERT FALSE, 'unsupported action type: ' || rec.action; + END CASE; -- END CASE (rec.action) + -- Mark as finished + UPDATE exchange.exchange_tables + SET finished=TRUE + WHERE table_serial_id=rec.table_serial_id; + END LOOP; -- create/alter/drop actions +END $$; +COMMENT ON FUNCTION exchange_do_create_tables + IS 'Creates all tables for the given number of partitions that need creating. Does NOT support sharding.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see (1<<52)) + THEN + RAISE EXCEPTION 'addition overflow'; + END IF; +END $$; +COMMENT ON FUNCTION amount_add + IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52'; +CREATE OR REPLACE FUNCTION amount_left_minus_right( + IN l taler_amount + ,IN r taler_amount + ,OUT diff taler_amount + ,OUT ok BOOLEAN +) +LANGUAGE plpgsql +AS $$ +BEGIN +IF (l.val > r.val) +THEN + ok = TRUE; + IF (l.frac >= r.frac) + THEN + diff.val = l.val - r.val; + diff.frac = l.frac - r.frac; + ELSE + diff.val = l.val - r.val - 1; + diff.frac = l.frac + 100000000 - r.frac; + END IF; +ELSE + IF (l.val = r.val) AND (l.frac >= r.frac) + THEN + diff.val = 0; + diff.frac = l.frac - r.frac; + ok = TRUE; + ELSE + diff = (-1, -1); + ok = FALSE; + END IF; +END IF; +RETURN; +END $$; +COMMENT ON FUNCTION amount_left_minus_right + IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.'; +-- +-- This file is part of TALER +-- Copyright (C) 2023-2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) +THEN + my_not_before=date '1970-01-01' + my_reserve.birthday; + my_earliest_date = current_date - make_interval(in_maximum_age_committed); + -- + -- 1970-01-01 + birthday == my_not_before now + -- | | | + -- <.......not allowed......>[<.....allowed range......>] + -- | | | + -- ____*_____________________*_________*________________* timeline + -- | + -- my_earliest_date == + -- now - maximum_age_committed*year + -- + IF ( (in_maximum_age_committed IS NULL) OR + (my_earliest_date < my_not_before) ) + THEN + out_required_age = extract(year FROM age(current_date, my_not_before)); + out_age_ok = FALSE; + out_balance_ok = TRUE; -- not really + out_nonce_reuse = FALSE; -- not really + RETURN; + END IF; +END IF; +out_age_ok = TRUE; +out_required_age = 0; +-- Check reserve balance is sufficient. +SELECT * + INTO my_difference + FROM amount_left_minus_right(out_reserve_balance + ,in_amount_with_fee); +out_balance_ok = my_difference.ok; +IF NOT out_balance_ok +THEN + out_nonce_reuse = FALSE; -- not yet determined + RETURN; +END IF; +my_balance = my_difference.diff; +-- Calculate new expiration dates. +in_min_reserve_gc=GREATEST(in_min_reserve_gc,my_reserve.gc_date); +-- Update reserve balance. +UPDATE reserves SET + gc_date=in_min_reserve_gc + ,current_balance=my_balance +WHERE + reserve_pub=in_reserve_pub; +-- Ensure the uniqueness of the blinding_seed +IF in_blinding_seed IS NOT NULL +THEN + INSERT INTO unique_withdraw_blinding_seed + (blinding_seed) + VALUES + (in_blinding_seed) + ON CONFLICT DO NOTHING; + IF NOT FOUND + THEN + out_nonce_reuse = TRUE; + RETURN; + END IF; +END IF; +out_nonce_reuse = FALSE; +-- Write the data into the withdraw table +INSERT INTO withdraw + (planchets_h + ,execution_date + ,max_age + ,amount_with_fee + ,reserve_pub + ,reserve_sig + ,noreveal_index + ,denom_serials + ,selected_h + ,blinding_seed + ,cs_r_values + ,cs_r_choices + ,denom_sigs) +VALUES + (in_planchets_h + ,in_now + ,in_maximum_age_committed + ,in_amount_with_fee + ,in_reserve_pub + ,in_reserve_sig + ,in_noreveal_index + ,in_denom_serials + ,in_selected_h + ,in_blinding_seed + ,in_cs_r_values + ,in_cs_r_choices + ,in_denom_sigs) +ON CONFLICT DO NOTHING; +IF NOT FOUND +THEN + RAISE EXCEPTION 'Conflict on insert into withdraw despite idempotency check for reserve_pub(%) and planchets_h(%)!', + in_reserve_pub, + in_planchets_h; +END IF; +END $$; +COMMENT ON FUNCTION exchange_do_withdraw( + taler_amount, + BYTEA, + BYTEA, + INT8, + INT8, + BYTEA, + INT2, + INT2, + BYTEA, + INT8[], + BYTEA[], + BYTEA, + BYTEA[], + INT8) + IS 'Checks whether the reserve has sufficient balance for an withdraw operation (or the request is repeated and was previously approved) and that age requirements are met. If so updates the database with the result. Includes storing the hashes of all blinded planchets, (separately) the hashes of the chosen planchets and denomination signatures, or signaling idempotency (and previous noreveal_index) or nonce reuse'; +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see ini_amount_with_fee.val) OR + ( ((kc.remaining).frac >= ini_amount_with_fee.frac) AND + ((kc.remaining).val >= ini_amount_with_fee.val) ) ); + IF NOT FOUND + THEN + -- Insufficient balance. + -- Note: C arrays are 0 indexed, but i started at 1 + out_insufficient_balance_coin_index=i-1; + RETURN; + END IF; + END IF; +END LOOP; -- end FOR all coins +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_amount_with_fee.val) OR + ( ((kc.remaining).frac >= in_amount_with_fee.frac) AND + ((kc.remaining).val >= in_amount_with_fee.val) ) ); +IF NOT FOUND +THEN + -- Insufficient balance. + out_noreveal_index=-1; + out_balance_ok=FALSE; + RETURN; +END IF; +-- Special actions needed for a CS melt? +IF in_cs_rms IS NOT NULL +THEN + -- Get maximum denominations serial value in + -- existence, this will determine how long the + -- nonce will be locked. + SELECT + denominations_serial + INTO + denom_max + FROM exchange.denominations + ORDER BY denominations_serial DESC + LIMIT 1; + -- Cache CS signature to prevent replays in the future + -- (and check if cached signature exists at the same time). + INSERT INTO exchange.cs_nonce_locks + (nonce + ,max_denomination_serial + ,op_hash) + VALUES + (in_cs_rms + ,denom_max + ,in_rc) + ON CONFLICT DO NOTHING; + IF NOT FOUND + THEN + -- Record exists, make sure it is the same + SELECT 1 + FROM exchange.cs_nonce_locks + WHERE nonce=in_cs_rms + AND op_hash=in_rc; + IF NOT FOUND + THEN + -- Nonce reuse detected + out_balance_ok=FALSE; + out_zombie_bad=FALSE; + out_noreveal_index=42; -- FIXME: return error message more nicely! + ASSERT false, 'nonce reuse attempted by client'; + END IF; + END IF; +END IF; +-- Everything fine, return success! +out_balance_ok=TRUE; +out_noreveal_index=in_noreveal_index; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2023 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_min_serial_id + ORDER BY batch_deposit_serial_id ASC; +DECLARE + my_total_val INT8; -- all deposits without wire +DECLARE + my_total_frac INT8; -- all deposits without wire (fraction, not normalized) +DECLARE + my_total taler_amount; -- amount that was originally deposited +DECLARE + my_batch_record RECORD; +DECLARE + i RECORD; +BEGIN +OPEN missing; +LOOP + FETCH NEXT FROM missing INTO i; + EXIT WHEN NOT FOUND; + SELECT + SUM((cdep.amount_with_fee).val) AS total_val + ,SUM((cdep.amount_with_fee).frac::INT8) AS total_frac + INTO + my_batch_record + FROM coin_deposits cdep + WHERE cdep.batch_deposit_serial_id = i.batch_deposit_serial_id; + my_total_val=my_batch_record.total_val; + my_total_frac=my_batch_record.total_frac; + -- Normalize total amount + my_total.val = my_total_val + my_total_frac / 100000000; + my_total.frac = my_total_frac % 100000000; + RETURN NEXT ( + i.batch_deposit_serial_id + ,my_total + ,i.wire_target_h_payto + ,i.wire_deadline); +END LOOP; +CLOSE missing; +RETURN; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2023 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 100000000 + ELSE 0 + END, + remaining.val=(kc.remaining).val+in_amount.val + + CASE + WHEN (kc.remaining).frac+in_amount.frac >= 100000000 + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_coin_pub; +out_conflict=FALSE; +out_not_found=FALSE; +END $$; +COMMENT ON FUNCTION exchange_do_refund(taler_amount, taler_amount, taler_amount, BYTEA, INT8, INT8, INT8, BYTEA, BYTEA, BYTEA) + IS 'Executes a refund operation, checking that the corresponding deposit was sufficient to cover the refunded amount'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 100000000 + ELSE 0 + END; +new_balance.val=balance.val+tmp.val + + CASE + WHEN balance.frac+tmp.frac >= 100000000 + THEN 1 + ELSE 0 + END; +-- Credit the reserve and update reserve timers. +UPDATE reserves + SET + current_balance = new_balance, + gc_date=GREATEST(gc_date, in_reserve_gc), + expiration_date=GREATEST(expiration_date, in_reserve_expiration) + WHERE reserve_pub=in_reserve_pub; +IF NOT FOUND +THEN + RAISE NOTICE 'failed to increase reserve balance from recoup'; + out_recoup_ok=TRUE; + out_internal_failure=TRUE; + RETURN; +END IF; +INSERT INTO exchange.recoup + (coin_pub + ,coin_sig + ,coin_blind + ,amount + ,recoup_timestamp + ,withdraw_id + ) +VALUES + (in_coin_pub + ,in_coin_sig + ,in_coin_blind + ,tmp + ,in_recoup_timestamp + ,in_withdraw_id); +-- Normal end, everything is fine. +out_recoup_ok=TRUE; +out_recoup_timestamp=in_recoup_timestamp; +END $$; +-- COMMENT ON FUNCTION exchange_do_recoup_to_reserve(INT8, INT4, BYTEA, BOOLEAN, BOOLEAN) +-- IS 'Executes a recoup of a coin that was withdrawn from a reserve'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 100000000 + ELSE 0 + END, + remaining.val=(kc.remaining).val+tmp.val + + CASE + WHEN (kc.remaining).frac+tmp.frac >= 100000000 + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_old_coin_pub; +IF NOT FOUND +THEN + RAISE NOTICE 'failed to increase old coin balance from recoup'; + out_recoup_ok=TRUE; + out_internal_failure=TRUE; + RETURN; +END IF; +INSERT INTO recoup_refresh + (coin_pub + ,known_coin_id + ,coin_sig + ,coin_blind + ,amount + ,recoup_timestamp + ,refresh_id + ) +VALUES + (in_coin_pub + ,in_known_coin_id + ,in_coin_sig + ,in_coin_blind + ,tmp + ,in_recoup_timestamp + ,in_refresh_id); +-- Normal end, everything is fine. +out_recoup_ok=TRUE; +out_recoup_timestamp=in_recoup_timestamp; +END $$; +-- COMMENT ON FUNCTION exchange_do_recoup_to_coin(INT8, INT4, BYTEA, BOOLEAN, BOOLEAN) +-- IS 'Executes a recoup-refresh of a coin that was obtained from a refresh-reveal process'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 100000000 + ELSE 0 + END, + remaining.val=(kc.remaining).val+(my_deposit.amount_with_fee).val + + CASE + WHEN (kc.remaining).frac+(my_deposit.amount_with_fee).frac >= 100000000 + THEN 1 + ELSE 0 + END + WHERE coin_pub = my_deposit.coin_pub; +END LOOP; +END $$; +COMMENT ON FUNCTION exchange_do_purse_delete(BYTEA,BYTEA,INT8) + IS 'Delete a previously undecided purse and refund the coins (if any).'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_amount_with_fee.val) OR + ( ((kc.remaining).frac >= in_amount_with_fee.frac) AND + ((kc.remaining).val >= in_amount_with_fee.val) ) ); +IF NOT FOUND +THEN + -- Insufficient balance. + out_balance_ok=FALSE; + out_late=FALSE; + out_conflict=FALSE; + RETURN; +END IF; +-- Credit the purse. +UPDATE purse_requests pr + SET + balance.frac=(pr.balance).frac+in_amount_without_fee.frac + - CASE + WHEN (pr.balance).frac+in_amount_without_fee.frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + balance.val=(pr.balance).val+in_amount_without_fee.val + + CASE + WHEN (pr.balance).frac+in_amount_without_fee.frac >= 100000000 + THEN 1 + ELSE 0 + END + WHERE purse_pub=in_purse_pub; +out_conflict=FALSE; +out_balance_ok=TRUE; +-- See if we can finish the merge or need to update the trigger time and partner. +SELECT COALESCE(partner_serial_id,0) + ,reserve_pub + INTO psi + ,my_reserve_pub + FROM purse_merges + WHERE purse_pub=in_purse_pub; +IF NOT FOUND +THEN + -- Purse was not yet merged. We are done. + out_late=FALSE; + RETURN; +END IF; +SELECT + amount_with_fee + ,in_reserve_quota + INTO + rval + FROM exchange.purse_requests preq + WHERE (purse_pub=in_purse_pub) + AND ( ( ( ((preq.amount_with_fee).val <= (preq.balance).val) + AND ((preq.amount_with_fee).frac <= (preq.balance).frac) ) + OR ((preq.amount_with_fee).val < (preq.balance).val) ) ); +IF NOT FOUND +THEN + out_late=FALSE; + RETURN; +END IF; +-- We use rval as workaround as we cannot select +-- directly into the amount due to Postgres limitations. +my_amount := rval.amount_with_fee; +my_in_reserve_quota := rval.in_reserve_quota; +-- Remember how this purse was finished. +INSERT INTO purse_decision + (purse_pub + ,action_timestamp + ,refunded) +VALUES + (in_purse_pub + ,in_now + ,FALSE) +ON CONFLICT DO NOTHING; +IF NOT FOUND +THEN + -- Purse already decided, likely expired. + out_late=TRUE; + RETURN; +END IF; +out_late=FALSE; +IF (my_in_reserve_quota) +THEN + UPDATE reserves + SET purses_active=purses_active-1 + WHERE reserve_pub IN + (SELECT reserve_pub + FROM purse_merges + WHERE purse_pub=my_purse_pub + LIMIT 1); +END IF; +IF (0 != psi) +THEN + -- The taler-exchange-router will take care of this. + UPDATE purse_actions + SET action_date=0 --- "immediately" + ,partner_serial_id=psi + WHERE purse_pub=in_purse_pub; +ELSE + -- This is a local reserve, update balance immediately. + INSERT INTO reserves + (reserve_pub + ,current_balance + ,expiration_date + ,gc_date) + VALUES + (my_reserve_pub + ,my_amount + ,in_reserve_expiration + ,in_reserve_expiration) + ON CONFLICT DO NOTHING; + IF NOT FOUND + THEN + -- Reserve existed, thus UPDATE instead of INSERT. + UPDATE reserves + SET + current_balance.frac=(current_balance).frac+my_amount.frac + - CASE + WHEN (current_balance).frac + my_amount.frac >= 100000000 + THEN 100000000 + ELSE 0 + END + ,current_balance.val=(current_balance).val+my_amount.val + + CASE + WHEN (current_balance).frac + my_amount.frac >= 100000000 + THEN 1 + ELSE 0 + END + ,expiration_date=GREATEST(expiration_date,in_reserve_expiration) + ,gc_date=GREATEST(gc_date,in_reserve_expiration) + WHERE reserve_pub=my_reserve_pub; + END IF; +END IF; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_merge_timestamp; + IF NOT FOUND + THEN + out_no_partner=TRUE; + out_conflict=FALSE; + RETURN; + END IF; +END IF; +out_no_partner=FALSE; +-- Check purse is 'full'. +SELECT amount_with_fee + ,purse_fee + ,in_reserve_quota + INTO rval + FROM purse_requests pr + WHERE purse_pub=in_purse_pub + AND (pr.balance).val >= (pr.amount_with_fee).val + AND ( (pr.balance).frac >= (pr.amount_with_fee).frac OR + (pr.balance).val > (pr.amount_with_fee).val ); +IF NOT FOUND +THEN + out_no_balance=TRUE; + out_conflict=FALSE; + RETURN; +END IF; +-- We use rval as workaround as we cannot select +-- directly into the amount due to Postgres limitations. +my_amount := rval.amount_with_fee; +my_purse_fee := rval.purse_fee; +my_in_reserve_quota := rval.in_reserve_quota; +out_no_balance=FALSE; +-- Store purse merge signature, checks for purse_pub uniqueness +INSERT INTO purse_merges + (partner_serial_id + ,reserve_pub + ,purse_pub + ,merge_sig + ,merge_timestamp) + VALUES + (my_partner_serial_id + ,in_reserve_pub + ,in_purse_pub + ,in_merge_sig + ,in_merge_timestamp) + ON CONFLICT DO NOTHING; +IF NOT FOUND +THEN + -- Idempotency check: see if an identical record exists. + -- Note that by checking 'merge_sig', we implicitly check + -- identity over everything that the signature covers. + PERFORM + FROM purse_merges + WHERE purse_pub=in_purse_pub + AND merge_sig=in_merge_sig; + IF NOT FOUND + THEN + -- Purse was merged, but to some other reserve. Not allowed. + out_conflict=TRUE; + RETURN; + END IF; + -- "success" + out_conflict=FALSE; + RETURN; +END IF; +-- Remember how this purse was finished. This will conflict +-- if the purse was already decided previously. +INSERT INTO purse_decision + (purse_pub + ,action_timestamp + ,refunded) +VALUES + (in_purse_pub + ,in_merge_timestamp + ,FALSE) +ON CONFLICT DO NOTHING; +IF NOT FOUND +THEN + -- Purse was already decided (possibly deleted or merged differently). + out_conflict=TRUE; + RETURN; +END IF; +out_conflict=FALSE; +IF (my_in_reserve_quota) +THEN + UPDATE reserves + SET purses_active=purses_active-1 + WHERE reserve_pub IN + (SELECT reserve_pub + FROM purse_merges + WHERE purse_pub=my_purse_pub + LIMIT 1); +END IF; +-- Store account merge signature. +INSERT INTO account_merges + (reserve_pub + ,reserve_sig + ,purse_pub + ,wallet_h_payto) + VALUES + (in_reserve_pub + ,in_reserve_sig + ,in_purse_pub + ,in_wallet_h_payto); +-- If we need a wad transfer, mark purse ready for it. +IF (0 != my_partner_serial_id) +THEN + -- The taler-exchange-router will take care of this. + UPDATE purse_actions + SET action_date=0 --- "immediately" + ,partner_serial_id=my_partner_serial_id + WHERE purse_pub=in_purse_pub; +ELSE + -- This is a local reserve, update reserve balance immediately. + -- Refund the purse fee, by adding it to the purse value: + my_amount.val = my_amount.val + my_purse_fee.val; + my_amount.frac = my_amount.frac + my_purse_fee.frac; + -- normalize result + my_amount.val = my_amount.val + my_amount.frac / 100000000; + my_amount.frac = my_amount.frac % 100000000; + SELECT current_balance + INTO reserve_bal + FROM reserves + WHERE reserve_pub=in_reserve_pub; + balance = reserve_bal.current_balance; + balance.val=balance.val+my_amount.val + + CASE + WHEN balance.frac + my_amount.frac >= 100000000 + THEN 1 + ELSE 0 + END; + balance.frac=balance.frac+my_amount.frac + - CASE + WHEN balance.frac + my_amount.frac >= 100000000 + THEN 100000000 + ELSE 0 + END; + UPDATE reserves + SET current_balance=balance + WHERE reserve_pub=in_reserve_pub; +END IF; +RETURN; +END $$; +COMMENT ON FUNCTION exchange_do_purse_merge(BYTEA, BYTEA, INT8, BYTEA, TEXT, BYTEA, BYTEA, INT8) + IS 'Checks that the partner exists, the purse has not been merged with a different reserve and that the purse is full. If so, persists the merge data and either merges the purse with the reserve or marks it as ready for the taler-exchange-router. Caller MUST abort the transaction on failures so as to not persist data by accident.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_purse_fee.val) OR + ( ((current_balance).frac >= in_purse_fee.frac) AND + ((current_balance).val >= in_purse_fee.val) ) ); + IF NOT FOUND + THEN + out_no_funds=TRUE; + RETURN; + END IF; + END IF; +END IF; +out_no_funds=FALSE; +-- Store account merge signature. +INSERT INTO account_merges + (reserve_pub + ,reserve_sig + ,purse_pub + ,wallet_h_payto) + VALUES + (in_reserve_pub + ,in_reserve_sig + ,in_purse_pub + ,in_wallet_h_payto); +END $$; +COMMENT ON FUNCTION exchange_do_reserve_purse(BYTEA, BYTEA, INT8, INT8, INT8, BYTEA, BOOLEAN, taler_amount, BYTEA, BYTEA) + IS 'Create a purse for a reserve.'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = in_start_time) AND + (purse_expiration < in_end_time) AND + NOT was_decided + ORDER BY purse_expiration ASC + LIMIT 1; +out_found = FOUND; +IF NOT FOUND +THEN + RETURN; +END IF; +INSERT INTO purse_decision + (purse_pub + ,action_timestamp + ,refunded) +VALUES + (my_purse_pub + ,in_now + ,TRUE); +-- Code for 'TALER_DBEVENT_EXCHANGE_PURSE_REFUNDED' +NOTIFY X8DJSPNYJMNZDAP7GN6YQ4EZVSQXMF3HRP4VAR347WP9SZYP1C200; +IF (my_in_reserve_quota) +THEN + UPDATE reserves + SET purses_active=purses_active-1 + WHERE reserve_pub IN + (SELECT reserve_pub + FROM exchange.purse_merges + WHERE purse_pub=my_purse_pub + LIMIT 1); +END IF; +-- restore balance to each coin deposited into the purse +FOR my_deposit IN + SELECT coin_pub + ,amount_with_fee + FROM purse_deposits + WHERE purse_pub = my_purse_pub +LOOP + UPDATE known_coins kc SET + remaining.frac=(kc.remaining).frac+(my_deposit.amount_with_fee).frac + - CASE + WHEN (kc.remaining).frac+(my_deposit.amount_with_fee).frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + remaining.val=(kc.remaining).val+(my_deposit.amount_with_fee).val + + CASE + WHEN (kc.remaining).frac+(my_deposit.amount_with_fee).frac >= 100000000 + THEN 1 + ELSE 0 + END + WHERE coin_pub = my_deposit.coin_pub; + END LOOP; +END $$; +COMMENT ON FUNCTION exchange_do_expire_purse(INT8,INT8,INT8) + IS 'Finds an expired purse in the given time range and refunds the coins (if any).'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_coin_total.val) OR + ( ((kc.remaining).frac >= in_coin_total.frac) AND + ((kc.remaining).val >= in_coin_total.val) ) ); +IF NOT FOUND +THEN + -- Insufficient balance. + out_insufficient_funds=TRUE; + RETURN; +END IF; +-- Everything fine, return success! +out_insufficient_funds=FALSE; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) +THEN + my_cost.val = my_years * in_open_fee.val; + my_cost_tmp = my_years * in_open_fee.frac / 100000000; + IF (CAST (my_cost.val + my_cost_tmp AS INT8) < my_cost.val) + THEN + out_open_cost.val=9223372036854775807; + out_open_cost.frac=2147483647; + out_final_expiration=my_expiration_date; + out_no_funds=FALSE; + RAISE NOTICE 'arithmetic issue computing amount'; + RETURN; + END IF; + my_cost.val = CAST (my_cost.val + my_cost_tmp AS INT8); + my_cost.frac = my_years * in_open_fee.frac % 100000000; + my_needs_update = TRUE; +END IF; +-- check if we actually have something to do +IF NOT my_needs_update +THEN + out_final_expiration = reserve.expiration_date; + out_open_cost.val = 0; + out_open_cost.frac = 0; + out_no_funds=FALSE; + RAISE NOTICE 'no change required'; + RETURN; +END IF; +-- Check payment (coins and reserve) would be sufficient. +IF ( (in_total_paid.val < my_cost.val) OR + ( (in_total_paid.val = my_cost.val) AND + (in_total_paid.frac < my_cost.frac) ) ) +THEN + out_open_cost.val = my_cost.val; + out_open_cost.frac = my_cost.frac; + out_no_funds=FALSE; + -- We must return a failure, which is indicated by + -- the expiration being below the desired expiration. + IF (reserve.expiration_date >= in_desired_expiration) + THEN + -- This case is relevant especially if the purse + -- count was to be increased and the payment was + -- insufficient to cover this for the full period. + RAISE NOTICE 'forcing low expiration time'; + out_final_expiration = 0; + ELSE + out_final_expiration = reserve.expiration_date; + END IF; + RAISE NOTICE 'amount paid too low'; + RETURN; +END IF; +-- Check reserve balance is sufficient. +IF (out_reserve_balance.val > in_reserve_payment.val) +THEN + IF (out_reserve_balance.frac >= in_reserve_payment.frac) + THEN + my_balance.val=out_reserve_balance.val - in_reserve_payment.val; + my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac; + ELSE + my_balance.val=out_reserve_balance.val - in_reserve_payment.val - 1; + my_balance.frac=out_reserve_balance.frac + 100000000 - in_reserve_payment.frac; + END IF; +ELSE + IF (out_reserve_balance.val = in_reserve_payment.val) AND (out_reserve_balance.frac >= in_reserve_payment.frac) + THEN + my_balance.val=0; + my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac; + ELSE + out_final_expiration = reserve.expiration_date; + out_open_cost.val = my_cost.val; + out_open_cost.frac = my_cost.frac; + out_no_funds=TRUE; + RAISE NOTICE 'reserve balance too low'; + RETURN; + END IF; +END IF; +UPDATE reserves SET + current_balance=my_balance + ,gc_date=reserve.expiration_date + in_reserve_gc_delay + ,expiration_date=my_expiration_date + ,purses_allowed=reserve.purses_allowed +WHERE + reserve_pub=in_reserve_pub; +out_final_expiration=my_expiration_date; +out_open_cost = my_cost; +out_no_funds=FALSE; +RETURN; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see (1 << 52)) + THEN + RAISE EXCEPTION 'accumulation overflow'; + END IF; + -- Set the fulfillment_state according to the values. + -- For now, we only update the state when it was INSUFFICIENT. + -- FIXME[oec] #7999: What to do in case of Failure or other state? + IF (out_fullfillment_state = 2) -- INSUFFICIENT + THEN + IF (out_accumulated_total.val >= cur_commitment.val OR + (out_accumulated_total.val = cur_commitment.val AND + out_accumulated_total.frac >= cur_commitment.frac)) + THEN + out_fulfillment_state = 3; -- READY + END IF; + END IF; + -- Now, update the record + UPDATE exchange.policy_details + SET + accumulated = out_accumulated_total, + fulfillment_state = out_fulfillment_state + WHERE + policy_details_serial_id = out_policy_details_serial_id; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2023, 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_decision_time + THEN + -- Refuse to insert older decision for officer decisions. + RETURN; + END IF; + UPDATE legitimization_outcomes + SET is_active=FALSE + WHERE h_payto=in_h_normalized_payto + AND is_active; +ELSE + out_last_date = 0; +END IF; +SELECT access_token + ,is_wallet + INTO my_rec + FROM kyc_targets + WHERE h_normalized_payto=in_h_normalized_payto; +IF NOT FOUND +THEN + -- AML decision for previously unknown account; better includes + -- all required details about the account ... + IF in_payto_uri IS NULL + THEN + -- AML decision on an unknown account without payto_uri => fail. + out_account_unknown=TRUE; + RETURN; + END IF; + -- Well, fine, setup the account + out_is_wallet + = (LOWER (SUBSTRING (in_payto_uri, 0, 23)) = + 'payto://taler-reserve/') OR + (LOWER (SUBSTRING (in_payto_uri, 0, 28)) = + 'payto://taler-reserve-http/'); + INSERT INTO kyc_targets + (h_normalized_payto + ,is_wallet + ) VALUES ( + in_h_normalized_payto + ,out_is_wallet + ) + RETURNING access_token + INTO my_access_token; + INSERT INTO wire_targets + (wire_target_h_payto + ,h_normalized_payto + ,payto_uri + ) VALUES ( + in_h_full_payto + ,in_h_normalized_payto + ,in_payto_uri + ) + ON CONFLICT DO NOTHING; +ELSE + my_access_token = my_rec.access_token; + out_is_wallet = my_rec.is_wallet; +END IF; +-- Did KYC measures get prescribed? +IF in_jmeasures IS NOT NULL +THEN + -- First check if a perfectly equivalent legi measure + -- already exists, to avoid creating tons of duplicates. + SELECT legitimization_measure_serial_id + INTO out_legitimization_measure_serial_id + FROM legitimization_measures + WHERE access_token=my_access_token + AND jmeasures=in_jmeasures + AND NOT is_finished; + IF NOT FOUND + THEN + -- Enable new legitimization measure + INSERT INTO legitimization_measures + (access_token + ,start_time + ,jmeasures + ,display_priority + ) VALUES ( + my_access_token + ,in_decision_time + ,in_jmeasures + ,1) + RETURNING + legitimization_measure_serial_id + INTO + out_legitimization_measure_serial_id; + END IF; + -- end if for where we had in_jmeasures +END IF; +RAISE NOTICE 'marking legi measures of % as finished except for %', my_access_token, out_legitimization_measure_serial_id; +-- AML decision: mark all other active measures finished! +UPDATE legitimization_measures + SET is_finished=TRUE + WHERE access_token=my_access_token + AND NOT is_finished + AND legitimization_measure_serial_id != out_legitimization_measure_serial_id; +UPDATE legitimization_outcomes + SET is_active=FALSE + WHERE h_payto=in_h_normalized_payto + -- this clause is a minor optimization to avoid + -- updating outcomes that have long expired. + AND expiration_time >= in_decision_time; +INSERT INTO legitimization_outcomes + (h_payto + ,decision_time + ,expiration_time + ,jproperties + ,new_measure_name + ,to_investigate + ,jnew_rules + ) VALUES ( + in_h_normalized_payto + ,in_decision_time + ,in_expiration_time + ,in_properties + ,in_new_measure_name + ,in_to_investigate + ,in_new_rules + ) + RETURNING outcome_serial_id + INTO my_outcome_serial_id; +IF in_kyc_attributes_enc IS NOT NULL +THEN + IF in_kyc_attributes_hash IS NULL OR in_kyc_attributes_expiration IS NULL + THEN + RAISE EXCEPTION 'Got in_kyc_attributes_hash without hash or expiration.'; + END IF; + IF in_decider_pub IS NULL + THEN + RAISE EXCEPTION 'Got in_kyc_attributes_hash without in_decider_pub.'; + END IF; + -- Simulate a legi process for attribute insertion by AML Officer + INSERT INTO legitimization_processes + (h_payto + ,start_time + ,expiration_time + ,provider_name + ,provider_user_id + ,finished + ) VALUES ( + in_h_normalized_payto + ,in_decision_time + -- Process starts and finishes instantly + ,in_decision_time + ,'aml-officer' + ,ENCODE(in_decider_pub, 'base64') + ,TRUE + ) + RETURNING legitimization_process_serial_id + INTO my_legitimization_process_serial_id; + -- Now we can insert the attribute! + INSERT INTO kyc_attributes + (h_payto + ,collection_time + ,expiration_time + ,form_name + ,by_aml_officer + ,encrypted_attributes + ,legitimization_serial + ) VALUES ( + in_h_normalized_payto + ,in_decision_time + ,in_kyc_attributes_expiration + ,in_form_name + ,TRUE + ,in_kyc_attributes_enc + ,my_legitimization_process_serial_id + ) + RETURNING kyc_attributes_serial_id + INTO my_kyc_attributes_serial_id; + -- Wake up taler-exchange-sanctionscheck to check new attributes + -- This is value for TALER_DBEVENT_EXCHANGE_NEW_KYC_ATTRIBUTES. + NOTIFY XSX9Z5XGWWYFKXTAYCES63B62527JKNX9XD0131Z08THVV8YW5BZG; +END IF; +IF in_decider_pub IS NOT NULL +THEN + INSERT INTO aml_history + (h_payto + ,outcome_serial_id + ,justification + ,decider_pub + ,decider_sig + ,kyc_attributes_hash + ,kyc_attributes_serial_id + ) VALUES ( + in_h_normalized_payto + ,my_outcome_serial_id + ,in_justification + ,in_decider_pub + ,in_decider_sig + ,in_kyc_attributes_hash + ,my_kyc_attributes_serial_id + ); +END IF; +-- Trigger events +FOR i IN 1..COALESCE(array_length(ina_events,1),0) +LOOP + ini_event = ina_events[i]; + INSERT INTO kyc_events + (event_timestamp + ,event_type + ) VALUES ( + in_decision_time + ,ini_event); + IF (ini_event = 'ACCOUNT_OPEN') + THEN + UPDATE kyc_targets + SET open_time=in_decision_time + ,close_time=NULL + WHERE h_normalized_payto=in_h_normalized_payto; + END IF; + IF (ini_event = 'ACCOUNT_IDLE') + THEN + UPDATE kyc_targets + SET close_time=in_decision_time + WHERE h_normalized_payto=in_h_normalized_payto; + END IF; +END LOOP; +-- wake up taler-exchange-aggregator +INSERT INTO kyc_alerts + (h_payto + ,trigger_type + ) VALUES ( + in_h_normalized_payto + ,1 + ) + ON CONFLICT DO NOTHING; +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify_s); +END $$; +COMMENT ON FUNCTION exchange_do_insert_aml_decision(TEXT, BYTEA, BYTEA, INT8, INT8, JSONB, BYTEA, BYTEA, INT8, JSONB, BOOLEAN, TEXT, JSONB, TEXT, BYTEA, BYTEA, TEXT, TEXT[], TEXT) + IS 'Checks whether the AML officer is eligible to make AML decisions and if so inserts the decision into the table'; +-- +-- This file is part of TALER +-- Copyright (C) 2023, 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see in_decision_time + THEN + -- Refuse to insert older decision. + RETURN; + END IF; + UPDATE legitimization_outcomes + SET is_active=FALSE + WHERE h_payto=in_h_normalized_payto + AND is_active; +ELSE + out_last_date = 0; +END IF; +SELECT access_token + INTO my_access_token + FROM kyc_targets + WHERE h_normalized_payto=in_h_normalized_payto; +IF NOT FOUND +THEN + IF in_payto_uri IS NULL + THEN + -- AML decision on an unknown account without payto_uri => fail. + out_account_unknown=TRUE; + RETURN; + END IF; + my_is_wallet + = (LOWER (SUBSTRING (in_payto_uri, 0, 23)) = + 'payto://taler-reserve/') OR + (LOWER (SUBSTRING (in_payto_uri, 0, 28)) = + 'payto://taler-reserve-http/'); + INSERT INTO kyc_targets + (h_normalized_payto + ,is_wallet) + VALUES + (in_h_normalized_payto + ,my_is_wallet) + RETURNING access_token + INTO my_access_token; + INSERT INTO wire_targets + (wire_target_h_payto + ,h_normalized_payto + ,payto_uri) + VALUES + (in_h_full_payto + ,in_h_normalized_payto + ,in_payto_uri) + ON CONFLICT DO NOTHING; +END IF; +-- First check if a perfectly equivalent legi measure +-- already exists, to avoid creating tons of duplicates. +SELECT legitimization_measure_serial_id + INTO out_legitimization_measure_serial_id + FROM legitimization_measures + WHERE access_token=my_access_token + AND jmeasures=in_jmeasures + AND NOT is_finished; +IF NOT FOUND +THEN + -- Enable new legitimization measure + INSERT INTO legitimization_measures + (access_token + ,start_time + ,jmeasures + ,display_priority) + VALUES + (my_access_token + ,in_decision_time + ,in_jmeasures + ,1) + RETURNING + legitimization_measure_serial_id + INTO + out_legitimization_measure_serial_id; +END IF; +-- AML decision: mark all other active measures finished! +UPDATE legitimization_measures + SET is_finished=TRUE + WHERE access_token=my_access_token + AND NOT is_finished + AND legitimization_measure_serial_id != out_legitimization_measure_serial_id; +UPDATE legitimization_outcomes + SET is_active=FALSE + WHERE h_payto=in_h_normalized_payto + -- this clause is a minor optimization to avoid + -- updating outcomes that have long expired. + AND expiration_time >= in_decision_time; +INSERT INTO legitimization_outcomes + (h_payto + ,decision_time + ,expiration_time + ,jproperties + ,new_measure_name + ,to_investigate + ,jnew_rules + ) + VALUES + (in_h_normalized_payto + ,in_decision_time + ,in_expiration_time + ,'{}'::JSONB + ,in_new_measure_name + ,FALSE + ,NULL + ) + RETURNING + outcome_serial_id + INTO + my_outcome_serial_id; +END $$; +COMMENT ON FUNCTION exchange_do_insert_successor_measure(BYTEA, INT8, INT8, TEXT, JSONB) + IS 'Checks whether the AML officer is eligible to make AML decisions and if so inserts the decision into the table'; +-- +-- This file is part of TALER +-- Copyright (C) 2023 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = in_last_change +THEN + -- Refuse to insert older status + RETURN; +END IF; +-- We are more recent, update existing record. +UPDATE exchange.aml_staff + SET master_sig=in_master_sig + ,decider_name=in_decider_name + ,is_active=in_is_active + ,read_only=in_read_only + ,last_change=in_last_change + WHERE decider_pub=in_decider_pub; +END $$; +COMMENT ON FUNCTION exchange_do_insert_aml_officer(BYTEA, BYTEA, TEXT, BOOL, BOOL, INT8) + IS 'Inserts or updates AML staff record, making sure the update is more recent than the previous change'; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 100000000 + ELSE 1 + END + ,current_balance.val = (rs.current_balance).val+in_credit.val + + CASE + WHEN (rs.current_balance).frac + in_credit.frac >= 100000000 + THEN 1 + ELSE 0 + END + ,expiration_date=GREATEST(expiration_date,in_expiration_date) + ,gc_date=GREATEST(gc_date,in_expiration_date) + WHERE reserve_pub=in_reserve_pub; + EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_notify); + ELSE + out_duplicate = TRUE; + END IF; + RETURN; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2014--2022 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = in_min_serial_id + ORDER BY aggregation_serial_id ASC; +DECLARE + my_total_val INT8; -- all deposits without wire +DECLARE + my_total_frac INT8; -- all deposits without wire (fraction, not normalized) +DECLARE + my_total taler_amount; -- amount that was originally deposited +DECLARE + my_batch_record RECORD; +DECLARE + i RECORD; +BEGIN +OPEN aggregation; +LOOP + FETCH NEXT FROM aggregation INTO i; + EXIT WHEN NOT FOUND; + SELECT + SUM((cdep.amount_with_fee).val) AS total_val + ,SUM((cdep.amount_with_fee).frac::INT8) AS total_frac + INTO + my_batch_record + FROM coin_deposits cdep + WHERE cdep.batch_deposit_serial_id = i.batch_deposit_serial_id; + my_total_val=my_batch_record.total_val; + my_total_frac=my_batch_record.total_frac; + -- Normalize total amount + my_total.val = my_total_val + my_total_frac / 100000000; + my_total.frac = my_total_frac % 100000000; + RETURN NEXT ( + i.batch_deposit_serial_id + ,i.aggregation_serial_id + ,my_total + ); +END LOOP; +CLOSE aggregation; +RETURN; +END $$; +-- +-- This file is part of TALER +-- Copyright (C) 2023, 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see 0) OR + ((current_balance).val > 0 ) ) + AND (expiration_date > in_collection_time_ts); +EXECUTE FORMAT ( + 'NOTIFY %s' + ,in_kyc_completed_notify_s); +INSERT INTO kyc_alerts + (h_payto + ,trigger_type) + VALUES + (in_h_payto,1) + ON CONFLICT DO NOTHING; +END $$; +COMMENT ON FUNCTION exchange_do_persist_kyc_attributes(INT8, BYTEA, INT4, TEXT, TEXT, TEXT, INT8, INT8, INT8, BYTEA, TEXT, TEXT) + IS 'Inserts new KYC attributes and updates the status of the legitimization process'; +-- +-- This file is part of TALER +-- Copyright (C) 2023, 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = 100000000 + THEN 1 + ELSE 0 + END, + cumulative_value.frac = (cumulative_value).frac + (in_delta).frac + - CASE + WHEN (in_delta).frac + (cumulative_value).frac >= 100000000 + THEN 100000000 + ELSE 0 + END + WHERE bmeta_serial_id=my_meta + AND h_payto=in_h_payto + AND bucket_start=my_bucket_start + AND bucket_range=my_range; + IF NOT FOUND + THEN + INSERT INTO exchange_statistic_bucket_amount + (bmeta_serial_id + ,h_payto + ,bucket_start + ,bucket_range + ,cumulative_value + ) VALUES ( + my_meta + ,in_h_payto + ,my_bucket_start + ,my_range + ,in_delta); + END IF; + END LOOP; + CLOSE my_curs; +END $$; +COMMENT ON PROCEDURE exchange_do_bump_amount_bucket_stat + IS 'Updates an amount statistic tracked over buckets'; +DROP PROCEDURE IF EXISTS exchange_do_bump_number_interval_stat; +CREATE OR REPLACE PROCEDURE exchange_do_bump_number_interval_stat( + in_slug TEXT, + in_h_payto BYTEA, + in_timestamp TIMESTAMP, + in_delta INT8 +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_now INT8; + my_record RECORD; + my_meta INT8; + my_ranges INT8[]; + my_precisions INT8[]; + my_rangex INT8; + my_precisionx INT8; + my_start INT8; + my_event INT8; +BEGIN + my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + SELECT imeta_serial_id + ,ranges AS ranges + ,precisions AS precisions + INTO my_record + FROM exchange_statistic_interval_meta + WHERE slug=in_slug + AND stype='number'; + IF NOT FOUND + THEN + RETURN; + END IF; + my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds + my_precisions = my_record.precisions; + my_ranges = my_record.ranges; + my_rangex = NULL; + FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + IF my_now - my_ranges[my_x] < my_start + THEN + my_rangex = my_ranges[my_x]; + my_precisionx = my_precisions[my_x]; + EXIT; + END IF; + END LOOP; + IF my_rangex IS NULL + THEN + -- event is beyond the ranges we care about + RETURN; + END IF; + my_meta = my_record.imeta_serial_id; + my_start = my_start - my_start % my_precisionx; -- round down + INSERT INTO exchange_statistic_counter_event AS msce + (imeta_serial_id + ,h_payto + ,slot + ,delta) + VALUES + (my_meta + ,in_h_payto + ,my_start + ,in_delta) + ON CONFLICT (imeta_serial_id, h_payto, slot) + DO UPDATE SET + delta = msce.delta + in_delta + RETURNING nevent_serial_id + INTO my_event; + UPDATE exchange_statistic_interval_counter + SET cumulative_number = cumulative_number + in_delta + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range=my_rangex; + IF NOT FOUND + THEN + INSERT INTO exchange_statistic_interval_counter + (imeta_serial_id + ,h_payto + ,range + ,event_delimiter + ,cumulative_number + ) VALUES ( + my_meta + ,in_h_payto + ,my_rangex + ,my_event + ,in_delta); + END IF; +END $$; +COMMENT ON PROCEDURE exchange_do_bump_number_interval_stat + IS 'Updates a numeric statistic tracked over an interval'; +DROP PROCEDURE IF EXISTS exchange_do_bump_amount_interval_stat; +CREATE OR REPLACE PROCEDURE exchange_do_bump_amount_interval_stat( + in_slug TEXT, + in_h_payto BYTEA, + in_timestamp TIMESTAMP, + in_delta taler_amount +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_now INT8; + my_record RECORD; + my_meta INT8; + my_ranges INT8[]; + my_precisions INT8[]; + my_x INT; + my_rangex INT8; + my_precisionx INT8; + my_start INT8; + my_event INT8; +BEGIN + my_now = ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + SELECT imeta_serial_id + ,ranges + ,precisions + INTO my_record + FROM exchange_statistic_interval_meta + WHERE slug=in_slug + AND stype='amount'; + IF NOT FOUND + THEN + RETURN; + END IF; + my_start = ROUND(EXTRACT(epoch FROM in_timestamp) * 1000000)::INT8 / 1000 / 1000; -- convert to seconds since epoch + my_precisions = my_record.precisions; + my_ranges = my_record.ranges; + my_rangex = NULL; + FOR my_x IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + IF my_now - my_ranges[my_x] < my_start + THEN + my_rangex = my_ranges[my_x]; + my_precisionx = my_precisions[my_x]; + EXIT; + END IF; + END LOOP; + IF my_rangex IS NULL + THEN + -- event is beyond the ranges we care about + RETURN; + END IF; + my_start = my_start - my_start % my_precisionx; -- round down + my_meta = my_record.imeta_serial_id; + INSERT INTO exchange_statistic_amount_event AS msae + (imeta_serial_id + ,h_payto + ,slot + ,delta + ) VALUES ( + my_meta + ,in_h_payto + ,my_start + ,in_delta + ) + ON CONFLICT (imeta_serial_id, h_payto, slot) + DO UPDATE SET + delta.val = (msae.delta).val + (in_delta).val + + CASE + WHEN (in_delta).frac + (msae.delta).frac >= 100000000 + THEN 1 + ELSE 0 + END, + delta.frac = (msae.delta).frac + (in_delta).frac + - CASE + WHEN (in_delta).frac + (msae.delta).frac >= 100000000 + THEN 100000000 + ELSE 0 + END + RETURNING aevent_serial_id + INTO my_event; + UPDATE exchange_statistic_interval_amount + SET + cumulative_value.val = (cumulative_value).val + (in_delta).val + + CASE + WHEN (in_delta).frac + (cumulative_value).frac >= 100000000 + THEN 1 + ELSE 0 + END, + cumulative_value.frac = (cumulative_value).frac + (in_delta).frac + - CASE + WHEN (in_delta).frac + (cumulative_value).frac >= 100000000 + THEN 100000000 + ELSE 0 + END + WHERE imeta_serial_id=my_meta + AND h_payto=in_h_payto + AND range=my_rangex; + IF NOT FOUND + THEN + INSERT INTO exchange_statistic_interval_amount + (imeta_serial_id + ,h_payto + ,range + ,event_delimiter + ,cumulative_value + ) VALUES ( + my_meta + ,in_h_payto + ,my_rangex + ,my_event + ,in_delta); + END IF; +END $$; +COMMENT ON PROCEDURE exchange_do_bump_amount_interval_stat + IS 'Updates an amount statistic tracked over an interval'; +DROP PROCEDURE IF EXISTS exchange_do_bump_number_stat; +CREATE OR REPLACE PROCEDURE exchange_do_bump_number_stat( + in_slug TEXT, + in_h_payto BYTEA, + in_timestamp TIMESTAMP, + in_delta INT8 +) +LANGUAGE plpgsql +AS $$ +BEGIN + CALL exchange_do_bump_number_bucket_stat (in_slug, in_h_payto, in_timestamp, in_delta); + CALL exchange_do_bump_number_interval_stat (in_slug, in_h_payto, in_timestamp, in_delta); +END $$; +COMMENT ON PROCEDURE exchange_do_bump_number_stat + IS 'Updates a numeric statistic (bucket or interval)'; +DROP PROCEDURE IF EXISTS exchange_do_bump_amount_stat; +CREATE OR REPLACE PROCEDURE exchange_do_bump_amount_stat( + in_slug TEXT, + in_h_payto BYTEA, + in_timestamp TIMESTAMP, + in_delta taler_amount +) +LANGUAGE plpgsql +AS $$ +BEGIN + CALL exchange_do_bump_amount_bucket_stat (in_slug, in_h_payto, in_timestamp, in_delta); + CALL exchange_do_bump_amount_interval_stat (in_slug, in_h_payto, in_timestamp, in_delta); +END $$; +COMMENT ON PROCEDURE exchange_do_bump_amount_stat + IS 'Updates an amount statistic (bucket or interval)'; +DROP FUNCTION IF EXISTS exchange_statistic_interval_number_get; +CREATE OR REPLACE FUNCTION exchange_statistic_interval_number_get ( + IN in_slug TEXT, + IN in_h_payto BYTEA +) +RETURNS SETOF exchange_statistic_interval_number_get_return_value +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_ranges INT8[]; + my_range INT8; + my_delta INT8; + my_meta INT8; + my_next_max_serial INT8; + my_rec RECORD; + my_irec RECORD; + my_i INT; + my_min_serial INT8 DEFAULT NULL; + my_rval exchange_statistic_interval_number_get_return_value; +BEGIN + SELECT imeta_serial_id + ,ranges + ,precisions + INTO my_rec + FROM exchange_statistic_interval_meta + WHERE slug=in_slug; + IF NOT FOUND + THEN + RETURN; + END IF; + my_rval.rvalue = 0; + my_ranges = my_rec.ranges; + my_meta = my_rec.imeta_serial_id; + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_range = my_ranges[my_i]; + SELECT event_delimiter + ,cumulative_number + INTO my_irec + FROM exchange_statistic_interval_counter + WHERE imeta_serial_id = my_meta + AND range = my_range + AND h_payto = in_h_payto; + IF FOUND + THEN + my_min_serial = my_irec.event_delimiter; + my_rval.rvalue = my_rval.rvalue + my_irec.cumulative_number; + -- Check if we have events that left the applicable range + SELECT SUM(delta) AS delta_sum + INTO my_irec + FROM exchange_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot < my_time - my_range + AND nevent_serial_id >= my_min_serial; + IF FOUND AND my_irec.delta_sum IS NOT NULL + THEN + my_delta = my_irec.delta_sum; + my_rval.rvalue = my_rval.rvalue - my_delta; + -- First find out the next event delimiter value + SELECT nevent_serial_id + INTO my_next_max_serial + FROM exchange_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot >= my_time - my_range + AND nevent_serial_id >= my_min_serial + ORDER BY slot ASC + LIMIT 1; + IF FOUND + THEN + -- remove expired events from the sum of the current slot + UPDATE exchange_statistic_interval_counter + SET cumulative_number = cumulative_number - my_delta, + event_delimiter = my_next_max_serial + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range = my_range; + ELSE + -- actually, slot is now empty, remove it entirely + DELETE FROM exchange_statistic_interval_counter + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range = my_range; + END IF; + IF (my_i < array_length(my_ranges,1)) + THEN + -- carry over all events into the next slot + UPDATE exchange_statistic_interval_counter AS usic SET + cumulative_number = cumulative_number + my_delta, + event_delimiter = LEAST(usic.event_delimiter,my_min_serial) + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range=my_ranges[my_i+1]; + IF NOT FOUND + THEN + INSERT INTO exchange_statistic_interval_counter + (imeta_serial_id + ,h_payto + ,range + ,event_delimiter + ,cumulative_number + ) VALUES ( + my_meta + ,in_h_payto + ,my_ranges[my_i+1] + ,my_min_serial + ,my_delta); + END IF; + ELSE + -- events are obsolete, delete them + DELETE FROM exchange_statistic_counter_event + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot < my_time - my_range; + END IF; + END IF; + my_rval.range = my_range; + RETURN NEXT my_rval; + END IF; + END LOOP; +END $$; +COMMENT ON FUNCTION exchange_statistic_interval_number_get + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value for each range'; +DROP FUNCTION IF EXISTS exchange_statistic_interval_amount_get; +CREATE OR REPLACE FUNCTION exchange_statistic_interval_amount_get ( + IN in_slug TEXT, + IN in_h_payto BYTEA +) +RETURNS SETOF exchange_statistic_interval_amount_get_return_value +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_ranges INT8[]; + my_range INT8; + my_delta_value INT8; + my_delta_frac INT8; + my_delta taler_amount; + my_meta INT8; + my_next_max_serial INT8; + my_rec RECORD; + my_irec RECORD; + my_jrec RECORD; + my_i INT; + my_min_serial INT8 DEFAULT NULL; + my_rval exchange_statistic_interval_amount_get_return_value; +BEGIN + SELECT imeta_serial_id + ,ranges + ,precisions + INTO my_rec + FROM exchange_statistic_interval_meta + WHERE slug=in_slug; + IF NOT FOUND + THEN + RETURN; + END IF; + my_meta = my_rec.imeta_serial_id; + my_ranges = my_rec.ranges; + my_rval.rvalue.val = 0; + my_rval.rvalue.frac = 0; + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_range = my_ranges[my_i]; + SELECT event_delimiter + ,cumulative_value + INTO my_irec + FROM exchange_statistic_interval_amount + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range = my_range; + IF FOUND + THEN + my_min_serial = my_irec.event_delimiter; + my_rval.rvalue.val = (my_rval.rvalue).val + (my_irec.cumulative_value).val + (my_irec.cumulative_value).frac / 100000000; + my_rval.rvalue.frac = (my_rval.rvalue).frac + (my_irec.cumulative_value).frac % 100000000; + IF (my_rval.rvalue).frac > 100000000 + THEN + my_rval.rvalue.frac = (my_rval.rvalue).frac - 100000000; + my_rval.rvalue.val = (my_rval.rvalue).val + 1; + END IF; + -- Check if we have events that left the applicable range + SELECT SUM((esae.delta).val) AS value_sum + ,SUM((esae.delta).frac) AS frac_sum + INTO my_jrec + FROM exchange_statistic_amount_event esae + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot < my_time - my_range + AND aevent_serial_id >= my_min_serial; + IF FOUND AND my_jrec.value_sum IS NOT NULL + THEN + -- Normalize sum + my_delta_value = my_jrec.value_sum + my_jrec.frac_sum / 100000000; + my_delta_frac = my_jrec.frac_sum % 100000000; + my_rval.rvalue.val = (my_rval.rvalue).val - my_delta_value; + IF ((my_rval.rvalue).frac >= my_delta_frac) + THEN + my_rval.rvalue.frac = (my_rval.rvalue).frac - my_delta_frac; + ELSE + my_rval.rvalue.frac = 100000000 + (my_rval.rvalue).frac - my_delta_frac; + my_rval.rvalue.val = (my_rval.rvalue).val - 1; + END IF; + -- First find out the next event delimiter value + SELECT aevent_serial_id + INTO my_next_max_serial + FROM exchange_statistic_amount_event + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot >= my_time - my_range + AND aevent_serial_id >= my_min_serial + ORDER BY slot ASC + LIMIT 1; + IF FOUND + THEN + -- remove expired events from the sum of the current slot + UPDATE exchange_statistic_interval_amount SET + cumulative_value.val = (cumulative_value).val - my_delta_value + - CASE + WHEN (cumulative_value).frac < my_delta_frac + THEN 1 + ELSE 0 + END, + cumulative_value.frac = (cumulative_value).frac - my_delta_frac + + CASE + WHEN (cumulative_value).frac < my_delta_frac + THEN 100000000 + ELSE 0 + END, + event_delimiter = my_next_max_serial + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range = my_range; + ELSE + -- actually, slot is now empty, remove it entirely + DELETE FROM exchange_statistic_interval_amount + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range = my_range; + END IF; + IF (my_i < array_length(my_ranges,1)) + THEN + -- carry over all events into the next (larger) slot + UPDATE exchange_statistic_interval_amount AS msia SET + cumulative_value.val = (cumulative_value).val + my_delta_value + + CASE + WHEN (cumulative_value).frac + my_delta_frac > 100000000 + THEN 1 + ELSE 0 + END, + cumulative_value.frac = (cumulative_value).frac + my_delta_value + - CASE + WHEN (cumulative_value).frac + my_delta_frac > 100000000 + THEN 100000000 + ELSE 0 + END, + event_delimiter = LEAST (msia.event_delimiter,my_min_serial) + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND range=my_ranges[my_i+1]; + IF NOT FOUND + THEN + my_delta.val = my_delta_value; + my_delta.frac = my_delta_frac; + INSERT INTO exchange_statistic_interval_amount + (imeta_serial_id + ,h_payto + ,event_delimiter + ,range + ,cumulative_value + ) VALUES ( + my_meta + ,in_h_payto + ,my_min_serial + ,my_ranges[my_i+1] + ,my_delta); + END IF; + ELSE + -- events are obsolete, delete them + DELETE FROM exchange_statistic_amount_event + WHERE imeta_serial_id = my_meta + AND h_payto = in_h_payto + AND slot < my_time - my_range; + END IF; + END IF; + my_rval.range = my_range; + RETURN NEXT my_rval; + END IF; + END LOOP; -- over my_ranges +END $$; +COMMENT ON FUNCTION exchange_statistic_interval_amount_get + IS 'Returns deposit statistic tracking deposited amounts over certain time intervals; we first trim the stored data to only track what is still in-range, and then return the remaining value; multiple values are returned, one per range'; +DROP PROCEDURE IF EXISTS exchange_statistic_counter_gc; +CREATE OR REPLACE PROCEDURE exchange_statistic_counter_gc () +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_h_payto BYTEA; + my_rec RECORD; + my_sum RECORD; + my_meta INT8; + my_ranges INT8[]; + my_precisions INT8[]; + my_precision INT4; + my_i INT4; + min_slot INT8; + max_slot INT8; + end_slot INT8; + my_total INT8; +BEGIN + -- GC for all instances + FOR my_h_payto IN + SELECT DISTINCT h_payto + FROM exchange_statistic_counter_event + LOOP + -- Do combination work for all numeric statistic events + FOR my_rec IN + SELECT imeta_serial_id + ,ranges + ,precisions + ,slug + FROM exchange_statistic_interval_meta + LOOP + -- First, we query the current interval statistic to update its counters + PERFORM FROM exchange_statistic_interval_number_get (my_rec.slug, my_h_payto); + my_meta = my_rec.imeta_serial_id; + my_ranges = my_rec.ranges; + my_precisions = my_rec.precisions; + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_precision = my_precisions[my_i]; + IF 1 >= my_precision + THEN + -- Cannot coarsen in this case + CONTINUE; + END IF; + IF 1 = my_i + THEN + min_slot = 0; + ELSE + min_slot = my_ranges[my_i - 1]; + END IF; + end_slot = my_ranges[my_i]; +-- RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision; + LOOP + EXIT WHEN min_slot >= end_slot; + max_slot = min_slot + my_precision; + SELECT SUM(delta) AS total, + COUNT(*) AS matches, + MIN(nevent_serial_id) AS rep_serial_id + INTO my_sum + FROM exchange_statistic_counter_event + WHERE h_payto=my_h_payto + AND imeta_serial_id=my_meta + AND slot >= my_time - max_slot + AND slot < my_time - min_slot; +-- RAISE NOTICE 'Found % entries between [%,%)', my_sum.matches, my_time - max_slot, my_time - min_slot; + -- we only proceed if we had more then one match (optimization) + IF FOUND AND my_sum.matches > 1 + THEN + my_total = my_sum.total; +-- RAISE NOTICE 'combining % entries to representative % for slots [%-%)', my_sum.matches, my_sum.rep_serial_id, my_time - max_slot, my_time - min_slot; + -- combine entries + DELETE FROM exchange_statistic_counter_event + WHERE h_payto=my_h_payto + AND imeta_serial_id=my_meta + AND slot >= my_time - max_slot + AND slot < my_time - min_slot + AND nevent_serial_id > my_sum.rep_serial_id; + -- Now update the representative to the sum + UPDATE exchange_statistic_counter_event SET + delta = my_total + WHERE imeta_serial_id = my_meta + AND h_payto = my_h_payto + AND nevent_serial_id = my_sum.rep_serial_id; + END IF; + min_slot = min_slot + my_precision; + END LOOP; -- min_slot to end_slot by precision loop + END LOOP; -- my_i loop + -- Finally, delete all events beyond the range we care about +-- RAISE NOTICE 'deleting entries of %/% before % - % = %', my_h_payto, my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)]; + DELETE FROM exchange_statistic_counter_event + WHERE h_payto=my_h_payto + AND imeta_serial_id=my_meta + AND slot < my_time - my_ranges[array_length(my_ranges,1)]; + END LOOP; -- my_rec loop + END LOOP; -- my_h_payto loop +END $$; +COMMENT ON PROCEDURE exchange_statistic_counter_gc + IS 'Performs garbage collection and compaction of the exchange_statistic_counter_event table'; +DROP PROCEDURE IF EXISTS exchange_statistic_amount_gc; +CREATE OR REPLACE PROCEDURE exchange_statistic_amount_gc () +LANGUAGE plpgsql +AS $$ +DECLARE + my_time INT8 DEFAULT ROUND(EXTRACT(epoch FROM CURRENT_TIMESTAMP(0)::TIMESTAMP) * 1000000)::INT8 / 1000 / 1000; + my_h_payto BYTEA; + my_rec RECORD; + my_sum RECORD; + my_meta INT8; + my_ranges INT8[]; + my_precisions INT8[]; + my_precision INT4; + my_i INT4; + min_slot INT8; + max_slot INT8; + end_slot INT8; + my_total_val INT8; + my_total_frac INT8; +BEGIN + -- GC for all accounts + FOR my_h_payto IN + SELECT DISTINCT h_payto + FROM exchange_statistic_counter_event + LOOP + -- Do combination work for all numeric statistic events + FOR my_rec IN + SELECT imeta_serial_id + ,ranges + ,precisions + ,slug + FROM exchange_statistic_interval_meta + LOOP + -- First, we query the current interval statistic to update its counters + PERFORM FROM exchange_statistic_interval_amount_get (my_rec.slug, my_h_payto); + my_meta = my_rec.imeta_serial_id; + my_ranges = my_rec.ranges; + my_precisions = my_rec.precisions; + FOR my_i IN 1..COALESCE(array_length(my_ranges,1),0) + LOOP + my_precision = my_precisions[my_i]; + IF 1 >= my_precision + THEN + -- Cannot coarsen in this case + CONTINUE; + END IF; + IF 1 = my_i + THEN + min_slot = 0; + ELSE + min_slot = my_ranges[my_i - 1]; + END IF; + end_slot = my_ranges[my_i]; +-- RAISE NOTICE 'Coarsening from [%,%) at %', my_time - end_slot, my_time - min_slot, my_precision; + LOOP + EXIT WHEN min_slot >= end_slot; + max_slot = min_slot + my_precision; + SELECT SUM((delta).val) AS total_val, + SUM((delta).frac) AS total_frac, + COUNT(*) AS matches, + MIN(aevent_serial_id) AS rep_serial_id + INTO my_sum + FROM exchange_statistic_amount_event + WHERE imeta_serial_id=my_meta + AND h_payto=my_h_payto + AND slot >= my_time - max_slot + AND slot < my_time - max_slot; + -- we only proceed if we had more then one match (optimization) + IF FOUND AND my_sum.matches > 1 + THEN + -- normalize new total + my_total_frac = my_sum.total_frac % 100000000; + my_total_val = my_sum.total_val + my_sum.total_frac / 100000000; + -- combine entries + DELETE FROM exchange_statistic_amount_event + WHERE imeta_serial_id=my_meta + AND h_payto=my_h_payto + AND slot >= my_time - max_slot + AND slot < my_time - max_slot + AND aevent_serial_id > my_sum.rep_serial_id; + -- Now update the representative to the sum + UPDATE exchange_statistic_amount_event SET + delta.val = my_total_value + ,delta.frac = my_total_frac + WHERE imeta_serial_id = my_meta + AND h_payto = my_h_payto + AND aevent_serial_id = my_sum.rep_serial_id; + END IF; + min_slot = min_slot + my_precision; + END LOOP; -- min_slot to end_slot by precision loop + END LOOP; -- my_i loop + -- Finally, delete all events beyond the range we care about +-- RAISE NOTICE 'deleting entries of %/% before % - % = %', my_h_payto, my_meta, my_time, my_ranges[array_length(my_ranges,1)], my_time - my_ranges[array_length(my_ranges,1)]; + DELETE FROM exchange_statistic_amount_event + WHERE h_payto=my_h_payto + AND imeta_serial_id=my_meta + AND slot < my_time - my_ranges[array_length(my_ranges,1)]; + END LOOP; -- my_rec loop + END LOOP; -- my_h_payto loop +END $$; +COMMENT ON PROCEDURE exchange_statistic_amount_gc + IS 'Performs garbage collection and compaction of the exchange_statistic_amount_event table'; +DROP PROCEDURE IF EXISTS exchange_statistic_bucket_gc; +CREATE OR REPLACE PROCEDURE exchange_statistic_bucket_gc () +LANGUAGE plpgsql +AS $$ +DECLARE + my_rec RECORD; + my_range TEXT; + my_now INT8; + my_end INT8; +BEGIN + my_now = EXTRACT(EPOCH FROM CURRENT_TIMESTAMP(0)::TIMESTAMP); -- seconds since epoch + FOR my_rec IN + SELECT bmeta_serial_id + ,stype + ,ranges[array_length(ranges,1)] AS range + ,ages[array_length(ages,1)] AS age + FROM exchange_statistic_bucket_meta + LOOP + my_range = '1 ' || my_rec.range::TEXT; + my_end = my_now - my_rec.age * EXTRACT(SECONDS FROM (SELECT my_range::INTERVAL)); -- age is given in multiples of the range (in seconds) + IF my_rec.stype = 'amount' + THEN + DELETE + FROM exchange_statistic_bucket_amount + WHERE bmeta_serial_id = my_rec.bmeta_serial_id + AND bucket_start >= my_end; + ELSE + DELETE + FROM exchange_statistic_bucket_counter + WHERE bmeta_serial_id = my_rec.bmeta_serial_id + AND bucket_start >= my_end; + END IF; + END LOOP; +END $$; +COMMENT ON PROCEDURE exchange_statistic_bucket_gc + IS 'Performs garbage collection of the exchange_statistic_bucket_counter and exchange_statistic_bucket_amount tables'; +DROP FUNCTION IF EXISTS exchange_drop_customization; +CREATE OR REPLACE FUNCTION exchange_drop_customization ( + IN in_schema TEXT, + OUT out_found BOOLEAN +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_xpatches TEXT; +BEGIN + -- Update DB versioning table. + out_found = FALSE; + FOR my_xpatches IN + SELECT patch_name + FROM _v.patches + WHERE starts_with(patch_name, in_schema || '-') + LOOP + PERFORM _v.unregister_patch(my_xpatches); + out_found = TRUE; + END LOOP; + IF out_found + THEN + -- Drop the schema with all stored procedures/functions. + -- This also removes all associated triggers, hence CASCADE. + EXECUTE FORMAT('DROP SCHEMA %s CASCADE' + ,in_schema); + END IF; + -- Finally, need to also remove entries from the statistics meta-tables. + -- Doing so also DELETEs the associated statistics, hence CASCADE. + DELETE + FROM exchange_statistic_interval_meta + WHERE origin=in_schema; + DELETE + FROM exchange_statistic_bucket_meta + WHERE origin=in_schema; +END $$; +COMMENT ON FUNCTION exchange_drop_customization + IS 'Removes all entries related to a particular exchange customization schema'; +-- +-- This file is part of TALER +-- Copyright (C) 2024 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU General Public License as published by the Free Software +-- Foundation; either version 3, or (at your option) any later version. +-- +-- TALER 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see = $2, TRUE) + AND COALESCE(is_active, TRUE) + -- technically only one should ever be active, but we can be conservative + ORDER BY expiration_time DESC + LIMIT 1; +END $$; +DROP PROCEDURE IF EXISTS exchange_do_gc; +CREATE PROCEDURE exchange_do_gc( + IN in_ancient_date INT8, + IN in_now INT8) +LANGUAGE plpgsql +AS $$ +BEGIN + CALL exchange_do_main_gc(in_ancient_date,in_now); + CALL exchange_statistic_amount_gc (); + CALL exchange_statistic_bucket_gc (); + CALL exchange_statistic_counter_gc (); +END $$; +COMMENT ON PROCEDURE exchange_do_gc + IS 'calls all other garbage collection subroutines'; +COMMIT; diff --git a/tools/dbinit.sh b/tools/dbinit.sh new file mode 100755 index 00000000..2dafed34 --- /dev/null +++ b/tools/dbinit.sh @@ -0,0 +1,99 @@ +#!/bin/bash + +# TODO use specific commit + +# usage: +# `create_database` +# create a `taler-exchange` database +# `fetch` +# fetch all *.sql and *.sql.in files from GNU Taler exchange repository (latest commit) +# `init` +# initialize taler-exchange database (create tables, ...) +# `taler-exchange` database must already be created +# + + +set -e + +taler_repo_url="https://git-www.taler.net/exchange.git/" + +tmp_dir="/tmp/taler_exchange" +out_dir="./dbinit_sql" +init_sql="${out_dir}/init.sql" +drop_sql="${out_dir}/drop.sql" + +# psql parameter +host="localhost" +port=5432 +username="mte" +dbname="taler-exchange" + +fetch() { + git clone --depth 1 $taler_repo_url $tmp_dir +} + +process() { + source_dir="${tmp_dir}/src/exchangedb" + sql_in_files=( + "procedures.sql" + "exchange-0002.sql" + "exchange-0003.sql" + "exchange-0004.sql" + ) + for file in "${sql_in_files[@]}"; do + file_in="${source_dir}/${file}.in" + file_out="${source_dir}/${file}" + gcc -E -P -undef -I "$source_dir" - < "$file_in" \ + 2>/dev/null \ + > "$file_out" + done + # order is important + files=( + "versioning.sql" + "exchange-0001.sql" + "exchange-0002.sql" + "exchange-0003.sql" + "exchange-0004.sql" + "exchange-0005.sql" + "procedures.sql" + ) + mkdir -p "$out_dir" + cat "${source_dir}/drop.sql" > "$drop_sql" + echo "" > "$init_sql" + for file in "${files[@]}"; do + cat "${source_dir}/${file}" >> "$init_sql" + done +} + +# ? "NOTICE: function xxx() does not exist, skipping" +init() { + psql --host=$host --port=$port --username=$username --password --dbname=$dbname --file=$init_sql +} + +drop_schema() { + psql --host=$host --port=$port --username=$username --password --dbname=$dbname --file=$drop_sql +} + +create_database() { + createdb --host=$host --port=$port --username=$username --password $dbname +} + +drop_database() { + dropdb --host=$host --port=$port --username=$username --password $dbname +} + +if [[ $# -eq 0 ]]; then + echo "no argument" >&2 + exit 1 +fi + +cmd=$1 +case "$cmd" in + fetch) fetch "$@"; exit 0;; + process) process "$@"; exit 0;; + init) init "$@"; exit 0;; + drop_schema) drop_schema "$@"; exit 0;; + create_database) create_database "$@"; exit 0;; + drop_database) drop_database "$@"; exit 0;; + *) echo "Unknown command: $cmd" >&2; exit 1;; +esac