diff --git a/src/exchangedb/Makefile.am b/src/exchangedb/Makefile.am index 1d4ba1f5d..4d9bfcb59 100644 --- a/src/exchangedb/Makefile.am +++ b/src/exchangedb/Makefile.am @@ -15,9 +15,11 @@ pkgcfg_DATA = \ sqldir = $(prefix)/share/taler/sql/exchange/ sqlinputs = \ + exchange_do_*.sql \ + procedures.sql.in \ 0002-*.sql \ - 0003-*.sql \ exchange-0002.sql.in \ + 0003-*.sql \ exchange-0003.sql.in sql_DATA = \ @@ -38,6 +40,11 @@ CLEANFILES = \ exchange-0002.sql \ exchange-0003.sql +procedures.sql: procedures.sql.in exchange_do_*.sql + chmod +w $@ || true + gcc -E -P -undef - < procedures.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ + chmod ugo-w $@ + exchange-0002.sql: exchange-0002.sql.in 0002-*.sql chmod +w $@ || true gcc -E -P -undef - < exchange-0002.sql.in 2>/dev/null | sed -e "s/--.*//" | awk 'NF' - >$@ diff --git a/src/exchangedb/exchange_do_account_merge.sql b/src/exchangedb/exchange_do_account_merge.sql new file mode 100644 index 000000000..723154f1d --- /dev/null +++ b/src/exchangedb/exchange_do_account_merge.sql @@ -0,0 +1,15 @@ +-- +-- 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 +-- diff --git a/src/exchangedb/exchange_do_batch_reserves_in.sql b/src/exchangedb/exchange_do_batch_reserves_in.sql new file mode 100644 index 000000000..faad2ca84 --- /dev/null +++ b/src/exchangedb/exchange_do_batch_reserves_in.sql @@ -0,0 +1,130 @@ +-- +-- 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 +-- +CREATE OR REPLACE FUNCTION batch_reserves_in( + IN in_reserve_pub BYTEA, + IN in_expiration_date INT8, + IN in_gc_date INT8, + IN in_wire_ref INT8, + IN in_credit_val INT8, + IN in_credit_frac INT4, + IN in_exchange_account_name VARCHAR, + IN in_exectution_date INT8, + IN in_wire_source_h_payto BYTEA, ---h_payto + IN in_payto_uri VARCHAR, + IN in_reserve_expiration INT8, + OUT out_reserve_found BOOLEAN, + OUT transaction_duplicate BOOLEAN, + OUT ruuid INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + my_amount_val INT8; +DECLARE + my_amount_frac INT4; +BEGIN + + INSERT INTO reserves + (reserve_pub + ,current_balance_val + ,current_balance_frac + ,expiration_date + ,gc_date) + VALUES + (in_reserve_pub + ,in_credit_val + ,in_credit_frac + ,in_expiration_date + ,in_gc_date) + ON CONFLICT DO NOTHING + RETURNING reserve_uuid INTO ruuid; + + IF FOUND + THEN + -- We made a change, so the reserve did not previously exist. + out_reserve_found = FALSE; + ELSE + -- We made no change, which means the reserve existed. + out_reserve_found = TRUE; + END IF; + + --SIMPLE INSERT ON CONFLICT DO NOTHING + INSERT INTO wire_targets + (wire_target_h_payto + ,payto_uri) + VALUES + (in_wire_source_h_payto + ,in_payto_uri) + ON CONFLICT DO NOTHING; + + INSERT INTO reserves_in + (reserve_pub + ,wire_reference + ,credit_val + ,credit_frac + ,exchange_account_section + ,wire_source_h_payto + ,execution_date) + VALUES + (in_reserve_pub + ,in_wire_ref + ,in_credit_val + ,in_credit_frac + ,in_exchange_account_name + ,in_wire_source_h_payto + ,in_expiration_date); + + --IF THE INSERTION WAS A SUCCESS IT MEANS NO DUPLICATED TRANSACTION + IF FOUND + THEN + transaction_duplicate = FALSE; + IF out_reserve_found + THEN + UPDATE reserves + SET + current_balance_frac = current_balance_frac+in_credit_frac + - CASE + WHEN current_balance_frac + in_credit_frac >= 100000000 + THEN 100000000 + ELSE 1 + END + ,current_balance_val = current_balance_val+in_credit_val + + CASE + WHEN 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 reserves.reserve_pub=in_reserve_pub; + out_reserve_found = TRUE; + RETURN; + ELSE + out_reserve_found=FALSE; + RETURN; + END IF; + out_reserve_found = TRUE; + ELSE + transaction_duplicate = TRUE; + IF out_reserve_found + THEN + out_reserve_found = TRUE; + RETURN; + ELSE + out_reserve_found = FALSE; + RETURN; + END IF; + END IF; +END $$; diff --git a/src/exchangedb/exchange_do_batch_withdraw.sql b/src/exchangedb/exchange_do_batch_withdraw.sql new file mode 100644 index 000000000..fedb7e912 --- /dev/null +++ b/src/exchangedb/exchange_do_batch_withdraw.sql @@ -0,0 +1,106 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw( + IN amount_val INT8, + IN amount_frac INT4, + IN rpub BYTEA, + IN now INT8, + IN min_reserve_gc INT8, + OUT reserve_found BOOLEAN, + OUT balance_ok BOOLEAN, + OUT ruuid INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + reserve_gc INT8; +DECLARE + reserve_val INT8; +DECLARE + reserve_frac INT4; +BEGIN +-- Shards: reserves by reserve_pub (SELECT) +-- reserves_out (INSERT, with CONFLICT detection) by wih +-- reserves by reserve_pub (UPDATE) +-- reserves_in by reserve_pub (SELECT) +-- wire_targets by wire_target_h_payto + +SELECT + current_balance_val + ,current_balance_frac + ,gc_date + ,reserve_uuid + INTO + reserve_val + ,reserve_frac + ,reserve_gc + ,ruuid + FROM exchange.reserves + WHERE reserves.reserve_pub=rpub; + +IF NOT FOUND +THEN + -- reserve unknown + reserve_found=FALSE; + balance_ok=FALSE; + ruuid=2; + RETURN; +END IF; + +-- Check reserve balance is sufficient. +IF (reserve_val > amount_val) +THEN + IF (reserve_frac >= amount_frac) + THEN + reserve_val=reserve_val - amount_val; + reserve_frac=reserve_frac - amount_frac; + ELSE + reserve_val=reserve_val - amount_val - 1; + reserve_frac=reserve_frac + 100000000 - amount_frac; + END IF; +ELSE + IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac) + THEN + reserve_val=0; + reserve_frac=reserve_frac - amount_frac; + ELSE + reserve_found=TRUE; + balance_ok=FALSE; + RETURN; + END IF; +END IF; + +-- Calculate new expiration dates. +min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); + +-- Update reserve balance. +UPDATE reserves SET + gc_date=min_reserve_gc + ,current_balance_val=reserve_val + ,current_balance_frac=reserve_frac +WHERE + reserves.reserve_pub=rpub; + +reserve_found=TRUE; +balance_ok=TRUE; + +END $$; + +COMMENT ON FUNCTION exchange_do_batch_withdraw(INT8, INT4, BYTEA, INT8, INT8) + IS 'Checks whether the reserve has sufficient balance for a withdraw operation (or the request is repeated and was previously approved) and if so updates the database with the result. Excludes storing the planchets.'; + + + diff --git a/src/exchangedb/exchange_do_batch_withdraw_insert.sql b/src/exchangedb/exchange_do_batch_withdraw_insert.sql new file mode 100644 index 000000000..98db840f9 --- /dev/null +++ b/src/exchangedb/exchange_do_batch_withdraw_insert.sql @@ -0,0 +1,125 @@ +-- +-- 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 +-- + + + + +CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw_insert( + IN cs_nonce BYTEA, + IN amount_val INT8, + IN amount_frac INT4, + IN h_denom_pub BYTEA, + IN ruuid INT8, + IN reserve_sig BYTEA, + IN h_coin_envelope BYTEA, + IN denom_sig BYTEA, + IN now INT8, + OUT out_denom_unknown BOOLEAN, + OUT out_nonce_reuse BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + denom_serial INT8; +BEGIN +-- Shards: reserves by reserve_pub (SELECT) +-- reserves_out (INSERT, with CONFLICT detection) by wih +-- reserves by reserve_pub (UPDATE) +-- reserves_in by reserve_pub (SELECT) +-- wire_targets by wire_target_h_payto + +out_denom_unknown=TRUE; +out_conflict=TRUE; +out_nonce_reuse=TRUE; + +SELECT denominations_serial + INTO denom_serial + FROM exchange.denominations + WHERE denom_pub_hash=h_denom_pub; + +IF NOT FOUND +THEN + -- denomination unknown, should be impossible! + out_denom_unknown=TRUE; + ASSERT false, 'denomination unknown'; + RETURN; +END IF; +out_denom_unknown=FALSE; + +INSERT INTO exchange.reserves_out + (h_blind_ev + ,denominations_serial + ,denom_sig + ,reserve_uuid + ,reserve_sig + ,execution_date + ,amount_with_fee_val + ,amount_with_fee_frac) +VALUES + (h_coin_envelope + ,denom_serial + ,denom_sig + ,ruuid + ,reserve_sig + ,now + ,amount_val + ,amount_frac) +ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + out_conflict=TRUE; + RETURN; +END IF; +out_conflict=FALSE; + +-- Special actions needed for a CS withdraw? +out_nonce_reuse=FALSE; +IF NOT NULL cs_nonce +THEN + -- 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 + (cs_nonce + ,denom_serial + ,h_coin_envelope) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + -- See if the existing entry is identical. + SELECT 1 + FROM exchange.cs_nonce_locks + WHERE nonce=cs_nonce + AND op_hash=h_coin_envelope; + IF NOT FOUND + THEN + out_nonce_reuse=TRUE; + ASSERT false, 'nonce reuse attempted by client'; + RETURN; + END IF; + END IF; +END IF; + +END $$; + +COMMENT ON FUNCTION exchange_do_batch_withdraw_insert(BYTEA, INT8, INT4, BYTEA, INT8, BYTEA, BYTEA, BYTEA, INT8) + IS 'Stores information about a planchet for a batch withdraw operation. Checks if the planchet already exists, and in that case indicates a conflict'; + diff --git a/src/exchangedb/exchange_do_deposit.sql b/src/exchangedb/exchange_do_deposit.sql new file mode 100644 index 000000000..a2f5ba53a --- /dev/null +++ b/src/exchangedb/exchange_do_deposit.sql @@ -0,0 +1,171 @@ +-- +-- 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 +-- +CREATE OR REPLACE FUNCTION exchange_do_deposit( + IN in_amount_with_fee_val INT8, + IN in_amount_with_fee_frac INT4, + IN in_h_contract_terms BYTEA, + IN in_wire_salt BYTEA, + IN in_wallet_timestamp INT8, + IN in_exchange_timestamp INT8, + IN in_refund_deadline INT8, + IN in_wire_deadline INT8, + IN in_merchant_pub BYTEA, + IN in_receiver_wire_account VARCHAR, + IN in_h_payto BYTEA, + IN in_known_coin_id INT8, + IN in_coin_pub BYTEA, + IN in_coin_sig BYTEA, + IN in_shard INT8, + IN in_policy_blocked BOOLEAN, + IN in_policy_details_serial_id INT8, + OUT out_exchange_timestamp INT8, + OUT out_balance_ok BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + wtsi INT8; -- wire target serial id +BEGIN +-- Shards: INSERT policy_details (by policy_details_serial_id) +-- INSERT wire_targets (by h_payto), on CONFLICT DO NOTHING; +-- INSERT deposits (by coin_pub, shard), ON CONFLICT DO NOTHING; +-- UPDATE known_coins (by coin_pub) + +INSERT INTO exchange.wire_targets + (wire_target_h_payto + ,payto_uri) + VALUES + (in_h_payto + ,in_receiver_wire_account) +ON CONFLICT DO NOTHING -- for CONFLICT ON (wire_target_h_payto) + RETURNING wire_target_serial_id INTO wtsi; + +IF NOT FOUND +THEN + SELECT wire_target_serial_id + INTO wtsi + FROM exchange.wire_targets + WHERE wire_target_h_payto=in_h_payto; +END IF; + + +INSERT INTO exchange.deposits + (shard + ,coin_pub + ,known_coin_id + ,amount_with_fee_val + ,amount_with_fee_frac + ,wallet_timestamp + ,exchange_timestamp + ,refund_deadline + ,wire_deadline + ,merchant_pub + ,h_contract_terms + ,coin_sig + ,wire_salt + ,wire_target_h_payto + ,policy_blocked + ,policy_details_serial_id + ) + VALUES + (in_shard + ,in_coin_pub + ,in_known_coin_id + ,in_amount_with_fee_val + ,in_amount_with_fee_frac + ,in_wallet_timestamp + ,in_exchange_timestamp + ,in_refund_deadline + ,in_wire_deadline + ,in_merchant_pub + ,in_h_contract_terms + ,in_coin_sig + ,in_wire_salt + ,in_h_payto + ,in_policy_blocked + ,in_policy_details_serial_id) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Idempotency check: see if an identical record exists. + -- Note that by checking 'coin_sig', we implicitly check + -- identity over everything that the signature covers. + -- We do select over merchant_pub and wire_target_h_payto + -- primarily here to maximally use the existing index. + SELECT + exchange_timestamp + INTO + out_exchange_timestamp + FROM exchange.deposits + WHERE shard=in_shard + AND merchant_pub=in_merchant_pub + AND wire_target_h_payto=in_h_payto + AND coin_pub=in_coin_pub + AND coin_sig=in_coin_sig; + -- AND policy_details_serial_id=in_policy_details_serial_id; -- FIXME: is this required for idempotency? + + IF NOT FOUND + THEN + -- Deposit exists, but with differences. Not allowed. + out_balance_ok=FALSE; + out_conflict=TRUE; + RETURN; + END IF; + + -- Idempotent request known, return success. + out_balance_ok=TRUE; + out_conflict=FALSE; + + RETURN; +END IF; + + +out_exchange_timestamp=in_exchange_timestamp; + +-- Check and update balance of the coin. +UPDATE known_coins + SET + remaining_frac=remaining_frac-in_amount_with_fee_frac + + CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val-in_amount_with_fee_val + - CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_coin_pub + AND ( (remaining_val > in_amount_with_fee_val) OR + ( (remaining_frac >= in_amount_with_fee_frac) AND + (remaining_val >= in_amount_with_fee_val) ) ); + +IF NOT FOUND +THEN + -- Insufficient balance. + out_balance_ok=FALSE; + out_conflict=FALSE; + RETURN; +END IF; + +-- Everything fine, return success! +out_balance_ok=TRUE; +out_conflict=FALSE; + +END $$; diff --git a/src/exchangedb/exchange_do_expire_purse.sql b/src/exchangedb/exchange_do_expire_purse.sql new file mode 100644 index 000000000..82756abc5 --- /dev/null +++ b/src/exchangedb/exchange_do_expire_purse.sql @@ -0,0 +1,99 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_expire_purse( + IN in_start_time INT8, + IN in_end_time INT8, + IN in_now INT8, + OUT out_found BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_purse_pub BYTEA; +DECLARE + my_deposit record; +DECLARE + my_in_reserve_quota BOOLEAN; +BEGIN + +-- FIXME: we should probably do this in a loop +-- and expire all at once, instead of one per query +SELECT purse_pub + ,in_reserve_quota + INTO my_purse_pub + ,my_in_reserve_quota + FROM exchange.purse_requests + WHERE (purse_expiration >= in_start_time) AND + (purse_expiration < in_end_time) AND + purse_pub NOT IN (SELECT purse_pub + FROM purse_decision) + 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); + +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_val + ,amount_with_fee_frac + FROM exchange.purse_deposits + WHERE purse_pub = my_purse_pub +LOOP + UPDATE exchange.known_coins SET + remaining_frac=remaining_frac+my_deposit.amount_with_fee_frac + - CASE + WHEN remaining_frac+my_deposit.amount_with_fee_frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val+my_deposit.amount_with_fee_val + + CASE + WHEN 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).'; + + diff --git a/src/exchangedb/exchange_do_gc.sql b/src/exchangedb/exchange_do_gc.sql new file mode 100644 index 000000000..c6331c18e --- /dev/null +++ b/src/exchangedb/exchange_do_gc.sql @@ -0,0 +1,144 @@ +-- +-- 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 +-- + +CREATE OR REPLACE PROCEDURE exchange_do_gc( + IN in_ancient_date INT8, + IN in_now INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + reserve_uuid_min INT8; -- minimum reserve UUID still alive +DECLARE + melt_min INT8; -- minimum melt still alive +DECLARE + coin_min INT8; -- minimum known_coin still alive +DECLARE + deposit_min INT8; -- minimum deposit still alive +DECLARE + reserve_out_min INT8; -- minimum reserve_out still alive +DECLARE + denom_min INT8; -- minimum denomination still alive +BEGIN + +DELETE FROM exchange.prewire + WHERE finished=TRUE; + +DELETE FROM exchange.wire_fee + WHERE end_date < in_ancient_date; + +-- TODO: use closing fee as threshold? +DELETE FROM exchange.reserves + WHERE gc_date < in_now + AND current_balance_val = 0 + AND current_balance_frac = 0; + +SELECT + reserve_out_serial_id + INTO + reserve_out_min + FROM exchange.reserves_out + ORDER BY reserve_out_serial_id ASC + LIMIT 1; + +DELETE FROM exchange.recoup + WHERE reserve_out_serial_id < reserve_out_min; +-- FIXME: recoup_refresh lacks GC! + +SELECT + reserve_uuid + INTO + reserve_uuid_min + FROM exchange.reserves + ORDER BY reserve_uuid ASC + LIMIT 1; + +DELETE FROM exchange.reserves_out + WHERE reserve_uuid < reserve_uuid_min; + +-- FIXME: this query will be horribly slow; +-- need to find another way to formulate it... +DELETE FROM exchange.denominations + WHERE expire_legal < in_now + AND denominations_serial NOT IN + (SELECT DISTINCT denominations_serial + FROM exchange.reserves_out) + AND denominations_serial NOT IN + (SELECT DISTINCT denominations_serial + FROM exchange.known_coins + WHERE coin_pub IN + (SELECT DISTINCT coin_pub + FROM exchange.recoup)) + AND denominations_serial NOT IN + (SELECT DISTINCT denominations_serial + FROM exchange.known_coins + WHERE coin_pub IN + (SELECT DISTINCT coin_pub + FROM exchange.recoup_refresh)); + +SELECT + melt_serial_id + INTO + melt_min + FROM exchange.refresh_commitments + ORDER BY melt_serial_id ASC + LIMIT 1; + +DELETE FROM exchange.refresh_revealed_coins + WHERE melt_serial_id < melt_min; + +DELETE FROM exchange.refresh_transfer_keys + WHERE melt_serial_id < melt_min; + +SELECT + known_coin_id + INTO + coin_min + FROM exchange.known_coins + ORDER BY known_coin_id ASC + LIMIT 1; + +DELETE FROM exchange.deposits + WHERE known_coin_id < coin_min; + +SELECT + deposit_serial_id + INTO + deposit_min + FROM exchange.deposits + ORDER BY deposit_serial_id ASC + LIMIT 1; + +DELETE FROM exchange.refunds + WHERE deposit_serial_id < deposit_min; + +DELETE FROM exchange.aggregation_tracking + WHERE deposit_serial_id < deposit_min; + +SELECT + denominations_serial + INTO + denom_min + FROM exchange.denominations + ORDER BY denominations_serial ASC + LIMIT 1; + +DELETE FROM exchange.cs_nonce_locks + WHERE max_denomination_serial <= denom_min; + +END $$; + + + diff --git a/src/exchangedb/exchange_do_history_request.sql b/src/exchangedb/exchange_do_history_request.sql new file mode 100644 index 000000000..2f6041741 --- /dev/null +++ b/src/exchangedb/exchange_do_history_request.sql @@ -0,0 +1,85 @@ +-- +-- 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 +-- + + +CREATE OR REPLACE FUNCTION exchange_do_history_request( + IN in_reserve_pub BYTEA, + IN in_reserve_sig BYTEA, + IN in_request_timestamp INT8, + IN in_history_fee_val INT8, + IN in_history_fee_frac INT4, + OUT out_balance_ok BOOLEAN, + OUT out_idempotent BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + + -- Insert and check for idempotency. + INSERT INTO exchange.history_requests + (reserve_pub + ,request_timestamp + ,reserve_sig + ,history_fee_val + ,history_fee_frac) + VALUES + (in_reserve_pub + ,in_request_timestamp + ,in_reserve_sig + ,in_history_fee_val + ,in_history_fee_frac) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + out_balance_ok=TRUE; + out_idempotent=TRUE; + RETURN; + END IF; + + out_idempotent=FALSE; + + -- Update reserve balance. + UPDATE exchange.reserves + SET + current_balance_frac=current_balance_frac-in_history_fee_frac + + CASE + WHEN current_balance_frac < in_history_fee_frac + THEN 100000000 + ELSE 0 + END, + current_balance_val=current_balance_val-in_history_fee_val + - CASE + WHEN current_balance_frac < in_history_fee_frac + THEN 1 + ELSE 0 + END + WHERE + reserve_pub=in_reserve_pub + AND ( (current_balance_val > in_history_fee_val) OR + ( (current_balance_frac >= in_history_fee_frac) AND + (current_balance_val >= in_history_fee_val) ) ); + + IF NOT FOUND + THEN + -- Either reserve does not exist, or balance insufficient. + -- Both we treat the same here as balance insufficient. + out_balance_ok=FALSE; + RETURN; + END IF; + + out_balance_ok=TRUE; +END $$; + diff --git a/src/exchangedb/exchange_do_insert_or_update_policy_details.sql b/src/exchangedb/exchange_do_insert_or_update_policy_details.sql new file mode 100644 index 000000000..c7fe64d14 --- /dev/null +++ b/src/exchangedb/exchange_do_insert_or_update_policy_details.sql @@ -0,0 +1,128 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_insert_or_update_policy_details( + IN in_policy_hash_code BYTEA, + IN in_policy_json VARCHAR, + IN in_deadline INT8, + IN in_commitment_val INT8, + IN in_commitment_frac INT4, + IN in_accumulated_total_val INT8, + IN in_accumulated_total_frac INT4, + IN in_fee_val INT8, + IN in_fee_frac INT4, + IN in_transferable_val INT8, + IN in_transferable_frac INT4, + IN in_fulfillment_state SMALLINT, + OUT out_policy_details_serial_id INT8, + OUT out_accumulated_total_val INT8, + OUT out_accumulated_total_frac INT4, + OUT out_fulfillment_state SMALLINT) +LANGUAGE plpgsql +AS $$ +DECLARE + cur_commitment_val INT8; + cur_commitment_frac INT4; + cur_accumulated_total_val INT8; + cur_accumulated_total_frac INT4; +BEGIN + -- First, try to create a new entry. + INSERT INTO policy_details + (policy_hash_code, + policy_json, + deadline, + commitment_val, + commitment_frac, + accumulated_total_val, + accumulated_total_frac, + fee_val, + fee_frac, + transferable_val, + transferable_frac, + fulfillment_state) + VALUES (in_policy_hash_code, + in_policy_json, + in_deadline, + in_commitment_val, + in_commitment_frac, + in_accumulated_total_val, + in_accumulated_total_frac, + in_fee_val, + in_fee_frac, + in_transferable_val, + in_transferable_frac, + in_fulfillment_state) + ON CONFLICT (policy_hash_code) DO NOTHING + RETURNING policy_details_serial_id INTO out_policy_details_serial_id; + + -- If the insert was successful, return + -- We assume that the fullfilment_state was correct in first place. + IF FOUND THEN + out_accumulated_total_val = in_accumulated_total_val; + out_accumulated_total_frac = in_accumulated_total_frac; + out_fulfillment_state = in_fulfillment_state; + RETURN; + END IF; + + -- We had a conflict, grab the parts we need to update. + SELECT policy_details_serial_id, + commitment_val, + commitment_frac, + accumulated_total_val, + accumulated_total_frac + INTO out_policy_details_serial_id, + cur_commitment_val, + cur_commitment_frac, + cur_accumulated_total_val, + cur_accumulated_total_frac + FROM policy_details + WHERE policy_hash_code = in_policy_hash_code; + + -- calculate the new values (overflows throws exception) + out_accumulated_total_val = cur_accumulated_total_val + in_accumulated_total_val; + out_accumulated_total_frac = cur_accumulated_total_frac + in_accumulated_total_frac; + -- normalize + out_accumulated_total_val = out_accumulated_total_val + out_accumulated_total_frac / 100000000; + out_accumulated_total_frac = out_accumulated_total_frac % 100000000; + + IF (out_accumulated_total_val > (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: What to do in case of Failure or other state? + IF (out_fullfillment_state = 1) -- 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 = 2; -- READY + END IF; + END IF; + + -- Now, update the record + UPDATE exchange.policy_details + SET + accumulated_val = out_accumulated_total_val, + accumulated_frac = out_accumulated_total_frac, + fulfillment_state = out_fulfillment_state + WHERE + policy_details_serial_id = out_policy_details_serial_id; +END $$; diff --git a/src/exchangedb/exchange_do_melt.sql b/src/exchangedb/exchange_do_melt.sql new file mode 100644 index 000000000..c0290b561 --- /dev/null +++ b/src/exchangedb/exchange_do_melt.sql @@ -0,0 +1,186 @@ +-- +-- 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 +-- + + + + +CREATE OR REPLACE FUNCTION exchange_do_melt( + IN in_cs_rms BYTEA, + IN in_amount_with_fee_val INT8, + IN in_amount_with_fee_frac INT4, + IN in_rc BYTEA, + IN in_old_coin_pub BYTEA, + IN in_old_coin_sig BYTEA, + IN in_known_coin_id INT8, -- not used, but that's OK + IN in_noreveal_index INT4, + IN in_zombie_required BOOLEAN, + OUT out_balance_ok BOOLEAN, + OUT out_zombie_bad BOOLEAN, + OUT out_noreveal_index INT4) +LANGUAGE plpgsql +AS $$ +DECLARE + denom_max INT8; +BEGIN +-- Shards: INSERT refresh_commitments (by rc) +-- (rare:) SELECT refresh_commitments (by old_coin_pub) -- crosses shards! +-- (rare:) SEELCT refresh_revealed_coins (by melt_serial_id) +-- (rare:) PERFORM recoup_refresh (by rrc_serial) -- crosses shards! +-- UPDATE known_coins (by coin_pub) + +INSERT INTO exchange.refresh_commitments + (rc + ,old_coin_pub + ,old_coin_sig + ,amount_with_fee_val + ,amount_with_fee_frac + ,noreveal_index + ) + VALUES + (in_rc + ,in_old_coin_pub + ,in_old_coin_sig + ,in_amount_with_fee_val + ,in_amount_with_fee_frac + ,in_noreveal_index) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Idempotency check: see if an identical record exists. + out_noreveal_index=-1; + SELECT + noreveal_index + INTO + out_noreveal_index + FROM exchange.refresh_commitments + WHERE rc=in_rc; + out_balance_ok=FOUND; + out_zombie_bad=FALSE; -- zombie is OK + RETURN; +END IF; + + +IF in_zombie_required +THEN + -- Check if this coin was part of a refresh + -- operation that was subsequently involved + -- in a recoup operation. We begin by all + -- refresh operations our coin was involved + -- with, then find all associated reveal + -- operations, and then see if any of these + -- reveal operations was involved in a recoup. + PERFORM + FROM exchange.recoup_refresh + WHERE rrc_serial IN + (SELECT rrc_serial + FROM exchange.refresh_revealed_coins + WHERE melt_serial_id IN + (SELECT melt_serial_id + FROM exchange.refresh_commitments + WHERE old_coin_pub=in_old_coin_pub)); + IF NOT FOUND + THEN + out_zombie_bad=TRUE; + out_balance_ok=FALSE; + RETURN; + END IF; +END IF; + +out_zombie_bad=FALSE; -- zombie is OK + + +-- Check and update balance of the coin. +UPDATE known_coins + SET + remaining_frac=remaining_frac-in_amount_with_fee_frac + + CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val-in_amount_with_fee_val + - CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_old_coin_pub + AND ( (remaining_val > in_amount_with_fee_val) OR + ( (remaining_frac >= in_amount_with_fee_frac) AND + (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 NOT NULL in_cs_rms +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 + (cs_rms + ,denom_serial + ,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=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 $$; + diff --git a/src/exchangedb/exchange_do_purse_deposit.sql b/src/exchangedb/exchange_do_purse_deposit.sql new file mode 100644 index 000000000..cddbd8d46 --- /dev/null +++ b/src/exchangedb/exchange_do_purse_deposit.sql @@ -0,0 +1,244 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_purse_deposit( + IN in_partner_id INT8, + IN in_purse_pub BYTEA, + IN in_amount_with_fee_val INT8, + IN in_amount_with_fee_frac INT4, + IN in_coin_pub BYTEA, + IN in_coin_sig BYTEA, + IN in_amount_without_fee_val INT8, + IN in_amount_without_fee_frac INT4, + IN in_reserve_expiration INT8, + IN in_now INT8, + OUT out_balance_ok BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + was_merged BOOLEAN; +DECLARE + psi INT8; -- partner's serial ID (set if merged) +DECLARE + my_amount_val INT8; -- total in purse +DECLARE + my_amount_frac INT4; -- total in purse +DECLARE + was_paid BOOLEAN; +DECLARE + my_in_reserve_quota BOOLEAN; +DECLARE + my_reserve_pub BYTEA; +BEGIN + +-- Store the deposit request. +INSERT INTO exchange.purse_deposits + (partner_serial_id + ,purse_pub + ,coin_pub + ,amount_with_fee_val + ,amount_with_fee_frac + ,coin_sig) + VALUES + (in_partner_id + ,in_purse_pub + ,in_coin_pub + ,in_amount_with_fee_val + ,in_amount_with_fee_frac + ,in_coin_sig) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Idempotency check: check if coin_sig is the same, + -- if so, success, otherwise conflict! + PERFORM + FROM exchange.purse_deposits + WHERE coin_pub = in_coin_pub + AND purse_pub = in_purse_pub + AND coin_sig = in_cion_sig; + IF NOT FOUND + THEN + -- Deposit exists, but with differences. Not allowed. + out_balance_ok=FALSE; + out_conflict=TRUE; + RETURN; + END IF; +END IF; + + +-- Debit the coin +-- Check and update balance of the coin. +UPDATE known_coins + SET + remaining_frac=remaining_frac-in_amount_with_fee_frac + + CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val-in_amount_with_fee_val + - CASE + WHEN remaining_frac < in_amount_with_fee_frac + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_coin_pub + AND ( (remaining_val > in_amount_with_fee_val) OR + ( (remaining_frac >= in_amount_with_fee_frac) AND + (remaining_val >= in_amount_with_fee_val) ) ); + +IF NOT FOUND +THEN + -- Insufficient balance. + out_balance_ok=FALSE; + out_conflict=FALSE; + RETURN; +END IF; + + +-- Credit the purse. +UPDATE purse_requests + SET + balance_frac=balance_frac+in_amount_without_fee_frac + - CASE + WHEN balance_frac+in_amount_without_fee_frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + balance_val=balance_val+in_amount_without_fee_val + + CASE + WHEN 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 exchange.purse_merges + WHERE purse_pub=in_purse_pub; + +IF NOT FOUND +THEN + RETURN; +END IF; + +SELECT + amount_with_fee_val + ,amount_with_fee_frac + ,in_reserve_quota + INTO + my_amount_val + ,my_amount_frac + ,my_in_reserve_quota + FROM exchange.purse_requests + WHERE (purse_pub=in_purse_pub) + AND ( ( ( (amount_with_fee_val <= balance_val) + AND (amount_with_fee_frac <= balance_frac) ) + OR (amount_with_fee_val < balance_val) ) ); +IF NOT FOUND +THEN + RETURN; +END IF; + +-- 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 + out_conflict=TRUE; + RETURN; +END IF; + +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; + + +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_frac + ,current_balance_val + ,expiration_date + ,gc_date) + VALUES + (my_reserve_pub + ,my_amount_frac + ,my_amount_val + ,in_reserve_expiration + ,in_reserve_expiration) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + + 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 $$; + + diff --git a/src/exchangedb/exchange_do_purse_merge.sql b/src/exchangedb/exchange_do_purse_merge.sql new file mode 100644 index 000000000..2e799c9f2 --- /dev/null +++ b/src/exchangedb/exchange_do_purse_merge.sql @@ -0,0 +1,216 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_purse_merge( + IN in_purse_pub BYTEA, + IN in_merge_sig BYTEA, + IN in_merge_timestamp INT8, + IN in_reserve_sig BYTEA, + IN in_partner_url VARCHAR, + IN in_reserve_pub BYTEA, + IN in_wallet_h_payto BYTEA, + IN in_expiration_date INT8, + OUT out_no_partner BOOLEAN, + OUT out_no_balance BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_amount_val INT8; +DECLARE + my_amount_frac INT4; +DECLARE + my_purse_fee_val INT8; +DECLARE + my_purse_fee_frac INT4; +DECLARE + my_partner_serial_id INT8; +DECLARE + my_in_reserve_quota BOOLEAN; +BEGIN + +IF in_partner_url IS NULL +THEN + my_partner_serial_id=NULL; +ELSE + SELECT + partner_serial_id + INTO + my_partner_serial_id + FROM exchange.partners + WHERE partner_base_url=in_partner_url + AND start_date <= in_merge_timestamp + AND end_date > 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_val + ,amount_with_fee_frac + ,purse_fee_val + ,purse_fee_frac + ,in_reserve_quota + INTO my_amount_val + ,my_amount_frac + ,my_purse_fee_val + ,my_purse_fee_frac + ,my_in_reserve_quota + FROM exchange.purse_requests + WHERE purse_pub=in_purse_pub + AND balance_val >= amount_with_fee_val + AND ( (balance_frac >= amount_with_fee_frac) OR + (balance_val > amount_with_fee_val) ); +IF NOT FOUND +THEN + out_no_balance=TRUE; + out_conflict=FALSE; + RETURN; +END IF; +out_no_balance=FALSE; + +-- Store purse merge signature, checks for purse_pub uniqueness +INSERT INTO exchange.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 exchange.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; +out_conflict=FALSE; + + +-- Initialize reserve, if not yet exists. +INSERT INTO reserves + (reserve_pub + ,expiration_date + ,gc_date) + VALUES + (in_reserve_pub + ,in_expiration_date + ,in_expiration_date) + ON CONFLICT DO NOTHING; + +-- Remember how this purse was finished. +INSERT INTO purse_decision + (purse_pub + ,action_timestamp + ,refunded) +VALUES + (in_purse_pub + ,in_merge_timestamp + ,FALSE); + +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; + +-- Store account merge signature. +INSERT INTO exchange.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; + + UPDATE exchange.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 + WHERE reserve_pub=in_reserve_pub; + +END IF; + +RETURN; + +END $$; + +COMMENT ON FUNCTION exchange_do_purse_merge(BYTEA, BYTEA, INT8, BYTEA, VARCHAR, 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.'; + diff --git a/src/exchangedb/exchange_do_recoup_by_reserve.sql b/src/exchangedb/exchange_do_recoup_by_reserve.sql new file mode 100644 index 000000000..6a7ea725e --- /dev/null +++ b/src/exchangedb/exchange_do_recoup_by_reserve.sql @@ -0,0 +1,82 @@ +-- +-- 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 +-- + + +CREATE OR REPLACE FUNCTION exchange_do_recoup_by_reserve( + IN res_pub BYTEA +) +RETURNS TABLE +( + denom_sig BYTEA, + denominations_serial BIGINT, + coin_pub BYTEA, + coin_sig BYTEA, + coin_blind BYTEA, + amount_val BIGINT, + amount_frac INTEGER, + recoup_timestamp BIGINT +) +LANGUAGE plpgsql +AS $$ +DECLARE + res_uuid BIGINT; + blind_ev BYTEA; + c_pub BYTEA; +BEGIN + SELECT reserve_uuid + INTO res_uuid + FROM exchange.reserves + WHERE reserves.reserve_pub = res_pub; + + FOR blind_ev IN + SELECT h_blind_ev + FROM exchange.reserves_out_by_reserve + WHERE reserves_out_by_reserve.reserve_uuid = res_uuid + LOOP + SELECT robr.coin_pub + INTO c_pub + FROM exchange.recoup_by_reserve robr + WHERE robr.reserve_out_serial_id = ( + SELECT reserves_out.reserve_out_serial_id + FROM exchange.reserves_out + WHERE reserves_out.h_blind_ev = blind_ev + ); + RETURN QUERY + SELECT kc.denom_sig, + kc.denominations_serial, + rc.coin_pub, + rc.coin_sig, + rc.coin_blind, + rc.amount_val, + rc.amount_frac, + rc.recoup_timestamp + FROM ( + SELECT * + FROM exchange.known_coins + WHERE known_coins.coin_pub = c_pub + ) kc + JOIN ( + SELECT * + FROM exchange.recoup + WHERE recoup.coin_pub = c_pub + ) rc USING (coin_pub); + END LOOP; +END; +$$; + +COMMENT ON FUNCTION exchange_do_recoup_by_reserve + IS 'Recoup by reserve as a function to make sure we hit only the needed partition and not all when joining as joins on distributed tables fetch ALL rows from the shards'; + diff --git a/src/exchangedb/exchange_do_recoup_to_coin.sql b/src/exchangedb/exchange_do_recoup_to_coin.sql new file mode 100644 index 000000000..5598ec20c --- /dev/null +++ b/src/exchangedb/exchange_do_recoup_to_coin.sql @@ -0,0 +1,142 @@ +-- +-- 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 +-- + + + + +CREATE OR REPLACE FUNCTION exchange_do_recoup_to_coin( + IN in_old_coin_pub BYTEA, + IN in_rrc_serial INT8, + IN in_coin_blind BYTEA, + IN in_coin_pub BYTEA, + IN in_known_coin_id INT8, + IN in_coin_sig BYTEA, + IN in_recoup_timestamp INT8, + OUT out_recoup_ok BOOLEAN, + OUT out_internal_failure BOOLEAN, + OUT out_recoup_timestamp INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + tmp_val INT8; -- amount recouped +DECLARE + tmp_frac INT8; -- amount recouped +BEGIN + +-- Shards: UPDATE known_coins (by coin_pub) +-- SELECT recoup_refresh (by coin_pub) +-- UPDATE known_coins (by coin_pub) +-- INSERT recoup_refresh (by coin_pub) + + +out_internal_failure=FALSE; + + +-- Check remaining balance of the coin. +SELECT + remaining_frac + ,remaining_val + INTO + tmp_frac + ,tmp_val +FROM exchange.known_coins + WHERE coin_pub=in_coin_pub; + +IF NOT FOUND +THEN + out_internal_failure=TRUE; + out_recoup_ok=FALSE; + RETURN; +END IF; + +IF tmp_val + tmp_frac = 0 +THEN + -- Check for idempotency + SELECT + recoup_timestamp + INTO + out_recoup_timestamp + FROM exchange.recoup_refresh + WHERE coin_pub=in_coin_pub; + out_recoup_ok=FOUND; + RETURN; +END IF; + +-- Update balance of the coin. +UPDATE known_coins + SET + remaining_frac=0 + ,remaining_val=0 + WHERE coin_pub=in_coin_pub; + + +-- Credit the old coin. +UPDATE known_coins + SET + remaining_frac=remaining_frac+tmp_frac + - CASE + WHEN remaining_frac+tmp_frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val+tmp_val + + CASE + WHEN 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 exchange.recoup_refresh + (coin_pub + ,known_coin_id + ,coin_sig + ,coin_blind + ,amount_val + ,amount_frac + ,recoup_timestamp + ,rrc_serial + ) +VALUES + (in_coin_pub + ,in_known_coin_id + ,in_coin_sig + ,in_coin_blind + ,tmp_val + ,tmp_frac + ,in_recoup_timestamp + ,in_rrc_serial); + +-- 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'; + diff --git a/src/exchangedb/exchange_do_recoup_to_reserve.sql b/src/exchangedb/exchange_do_recoup_to_reserve.sql new file mode 100644 index 000000000..39baba8fa --- /dev/null +++ b/src/exchangedb/exchange_do_recoup_to_reserve.sql @@ -0,0 +1,144 @@ +-- +-- 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 +-- + + +CREATE OR REPLACE FUNCTION exchange_do_recoup_to_reserve( + IN in_reserve_pub BYTEA, + IN in_reserve_out_serial_id INT8, + IN in_coin_blind BYTEA, + IN in_coin_pub BYTEA, + IN in_known_coin_id INT8, + IN in_coin_sig BYTEA, + IN in_reserve_gc INT8, + IN in_reserve_expiration INT8, + IN in_recoup_timestamp INT8, + OUT out_recoup_ok BOOLEAN, + OUT out_internal_failure BOOLEAN, + OUT out_recoup_timestamp INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + tmp_val INT8; -- amount recouped +DECLARE + tmp_frac INT8; -- amount recouped +BEGIN +-- Shards: SELECT known_coins (by coin_pub) +-- SELECT recoup (by coin_pub) +-- UPDATE known_coins (by coin_pub) +-- UPDATE reserves (by reserve_pub) +-- INSERT recoup (by coin_pub) + +out_internal_failure=FALSE; + + +-- Check remaining balance of the coin. +SELECT + remaining_frac + ,remaining_val + INTO + tmp_frac + ,tmp_val +FROM exchange.known_coins + WHERE coin_pub=in_coin_pub; + +IF NOT FOUND +THEN + out_internal_failure=TRUE; + out_recoup_ok=FALSE; + RETURN; +END IF; + +IF tmp_val + tmp_frac = 0 +THEN + -- Check for idempotency + SELECT + recoup_timestamp + INTO + out_recoup_timestamp + FROM exchange.recoup + WHERE coin_pub=in_coin_pub; + + out_recoup_ok=FOUND; + RETURN; +END IF; + + +-- Update balance of the coin. +UPDATE known_coins + SET + remaining_frac=0 + ,remaining_val=0 + WHERE coin_pub=in_coin_pub; + + +-- Credit the reserve and update reserve timers. +UPDATE reserves + SET + current_balance_frac=current_balance_frac+tmp_frac + - CASE + WHEN current_balance_frac+tmp_frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + current_balance_val=current_balance_val+tmp_val + + CASE + WHEN current_balance_frac+tmp_frac >= 100000000 + THEN 1 + ELSE 0 + END, + 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_val + ,amount_frac + ,recoup_timestamp + ,reserve_out_serial_id + ) +VALUES + (in_coin_pub + ,in_coin_sig + ,in_coin_blind + ,tmp_val + ,tmp_frac + ,in_recoup_timestamp + ,in_reserve_out_serial_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'; + + + diff --git a/src/exchangedb/exchange_do_refund.sql b/src/exchangedb/exchange_do_refund.sql new file mode 100644 index 000000000..ceaabfe16 --- /dev/null +++ b/src/exchangedb/exchange_do_refund.sql @@ -0,0 +1,211 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_refund( + IN in_amount_with_fee_val INT8, + IN in_amount_with_fee_frac INT4, + IN in_amount_val INT8, + IN in_amount_frac INT4, + IN in_deposit_fee_val INT8, + IN in_deposit_fee_frac INT4, + IN in_h_contract_terms BYTEA, + IN in_rtransaction_id INT8, + IN in_deposit_shard INT8, + IN in_known_coin_id INT8, + IN in_coin_pub BYTEA, + IN in_merchant_pub BYTEA, + IN in_merchant_sig BYTEA, + OUT out_not_found BOOLEAN, + OUT out_refund_ok BOOLEAN, + OUT out_gone BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + dsi INT8; -- ID of deposit being refunded +DECLARE + tmp_val INT8; -- total amount refunded +DECLARE + tmp_frac INT8; -- total amount refunded +DECLARE + deposit_val INT8; -- amount that was originally deposited +DECLARE + deposit_frac INT8; -- amount that was originally deposited +BEGIN +-- Shards: SELECT deposits (coin_pub, shard, h_contract_terms, merchant_pub) +-- INSERT refunds (by coin_pub, rtransaction_id) ON CONFLICT DO NOTHING +-- SELECT refunds (by coin_pub) +-- UPDATE known_coins (by coin_pub) + +SELECT + deposit_serial_id + ,amount_with_fee_val + ,amount_with_fee_frac + ,done +INTO + dsi + ,deposit_val + ,deposit_frac + ,out_gone +FROM exchange.deposits + WHERE coin_pub=in_coin_pub + AND shard=in_deposit_shard + AND merchant_pub=in_merchant_pub + AND h_contract_terms=in_h_contract_terms; + +IF NOT FOUND +THEN + -- No matching deposit found! + out_refund_ok=FALSE; + out_conflict=FALSE; + out_not_found=TRUE; + out_gone=FALSE; + RETURN; +END IF; + +INSERT INTO exchange.refunds + (deposit_serial_id + ,coin_pub + ,merchant_sig + ,rtransaction_id + ,amount_with_fee_val + ,amount_with_fee_frac + ) + VALUES + (dsi + ,in_coin_pub + ,in_merchant_sig + ,in_rtransaction_id + ,in_amount_with_fee_val + ,in_amount_with_fee_frac) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Idempotency check: see if an identical record exists. + -- Note that by checking 'coin_sig', we implicitly check + -- identity over everything that the signature covers. + -- We do select over merchant_pub and h_contract_terms + -- primarily here to maximally use the existing index. + PERFORM + FROM exchange.refunds + WHERE coin_pub=in_coin_pub + AND deposit_serial_id=dsi + AND rtransaction_id=in_rtransaction_id + AND amount_with_fee_val=in_amount_with_fee_val + AND amount_with_fee_frac=in_amount_with_fee_frac; + + IF NOT FOUND + THEN + -- Deposit exists, but have conflicting refund. + out_refund_ok=FALSE; + out_conflict=TRUE; + out_not_found=FALSE; + RETURN; + END IF; + + -- Idempotent request known, return success. + out_refund_ok=TRUE; + out_conflict=FALSE; + out_not_found=FALSE; + out_gone=FALSE; + RETURN; +END IF; + +IF out_gone +THEN + -- money already sent to the merchant. Tough luck. + out_refund_ok=FALSE; + out_conflict=FALSE; + out_not_found=FALSE; + RETURN; +END IF; + +-- Check refund balance invariant. +SELECT + SUM(amount_with_fee_val) -- overflow here is not plausible + ,SUM(CAST(amount_with_fee_frac AS INT8)) -- compute using 64 bits + INTO + tmp_val + ,tmp_frac + FROM exchange.refunds + WHERE coin_pub=in_coin_pub + AND deposit_serial_id=dsi; +IF tmp_val IS NULL +THEN + RAISE NOTICE 'failed to sum up existing refunds'; + out_refund_ok=FALSE; + out_conflict=FALSE; + out_not_found=FALSE; + RETURN; +END IF; + +-- Normalize result before continuing +tmp_val = tmp_val + tmp_frac / 100000000; +tmp_frac = tmp_frac % 100000000; + +-- Actually check if the deposits are sufficient for the refund. Verbosely. ;-) +IF (tmp_val < deposit_val) +THEN + out_refund_ok=TRUE; +ELSE + IF (tmp_val = deposit_val) AND (tmp_frac <= deposit_frac) + THEN + out_refund_ok=TRUE; + ELSE + out_refund_ok=FALSE; + END IF; +END IF; + +IF (tmp_val = deposit_val) AND (tmp_frac = deposit_frac) +THEN + -- Refunds have reached the full value of the original + -- deposit. Also refund the deposit fee. + in_amount_frac = in_amount_frac + in_deposit_fee_frac; + in_amount_val = in_amount_val + in_deposit_fee_val; + + -- Normalize result before continuing + in_amount_val = in_amount_val + in_amount_frac / 100000000; + in_amount_frac = in_amount_frac % 100000000; +END IF; + +-- Update balance of the coin. +UPDATE known_coins + SET + remaining_frac=remaining_frac+in_amount_frac + - CASE + WHEN remaining_frac+in_amount_frac >= 100000000 + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val+in_amount_val + + CASE + WHEN 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(INT8, INT4, BYTEA, BOOLEAN, BOOLEAN) +-- IS 'Executes a refund operation, checking that the corresponding deposit was sufficient to cover the refunded amount'; + + diff --git a/src/exchangedb/exchange_do_reserve_open.sql b/src/exchangedb/exchange_do_reserve_open.sql new file mode 100644 index 000000000..5e80f713f --- /dev/null +++ b/src/exchangedb/exchange_do_reserve_open.sql @@ -0,0 +1,210 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_reserve_open( + IN in_reserve_pub BYTEA, + IN in_total_paid_val INT8, + IN in_total_paid_frac INT4, + IN in_reserve_payment_val INT8, + IN in_reserve_payment_frac INT4, + IN in_min_purse_limit INT4, + IN in_default_purse_limit INT4, + IN in_reserve_sig BYTEA, + IN in_desired_expiration INT8, + IN in_reserve_gc_delay INT8, + IN in_now INT8, + IN in_open_fee_val INT8, + IN in_open_fee_frac INT4, + OUT out_open_cost_val INT8, + OUT out_open_cost_frac INT4, + OUT out_final_expiration INT8, + OUT out_no_funds BOOLEAN) +LANGUAGE plpgsql +AS $$ +DECLARE + my_balance_val INT8; +DECLARE + my_balance_frac INT4; +DECLARE + my_cost_val INT8; +DECLARE + my_cost_tmp INT8; +DECLARE + my_cost_frac INT4; +DECLARE + my_years_tmp INT4; +DECLARE + my_years INT4; +DECLARE + my_needs_update BOOL; +DECLARE + my_purses_allowed INT8; +DECLARE + my_expiration_date INT8; +DECLARE + my_reserve_expiration INT8; +BEGIN + +-- FIXME: use SELECT FOR UPDATE? +SELECT + purses_allowed + ,expiration_date + ,current_balance_val + ,current_balance_frac +INTO + my_purses_allowed + ,my_reserve_expiration + ,my_balance_val + ,my_balance_frac +FROM reserves +WHERE + reserve_pub=in_reserve_pub; + +IF NOT FOUND +THEN + -- FIXME: do we need to set a 'not found'? + RAISE NOTICE 'reserve not found'; + RETURN; +END IF; + +-- Do not allow expiration time to start in the past already +IF (my_reserve_expiration < in_now) +THEN + my_expiration_date = in_now; +ELSE + my_expiration_date = my_reserve_expiration; +END IF; + +my_cost_val = 0; +my_cost_frac = 0; +my_needs_update = FALSE; +my_years = 0; + +-- Compute years based on desired expiration time +IF (my_expiration_date < in_desired_expiration) +THEN + my_years = (31535999999999 + in_desired_expiration - my_expiration_date) / 31536000000000; + my_purses_allowed = in_default_purse_limit; + my_expiration_date = my_expiration_date + 31536000000000 * my_years; +END IF; + +-- Increase years based on purses requested +IF (my_purses_allowed < in_min_purse_limit) +THEN + my_years = (31535999999999 + in_desired_expiration - in_now) / 31536000000000; + my_expiration_date = in_now + 31536000000000 * my_years; + my_years_tmp = (in_min_purse_limit + in_default_purse_limit - my_purses_allowed - 1) / in_default_purse_limit; + my_years = my_years + my_years_tmp; + my_purses_allowed = my_purses_allowed + (in_default_purse_limit * my_years_tmp); +END IF; + + +-- Compute cost based on annual fees +IF (my_years > 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 = my_reserve_expiration; + 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 (my_reserve_expiration >= 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 = my_reserve_expiration; + END IF; + RAISE NOTICE 'amount paid too low'; + RETURN; +END IF; + +-- Check reserve balance is sufficient. +IF (my_balance_val > in_reserve_payment_val) +THEN + IF (my_balance_frac >= in_reserve_payment_frac) + THEN + my_balance_val=my_balance_val - in_reserve_payment_val; + my_balance_frac=my_balance_frac - in_reserve_payment_frac; + ELSE + my_balance_val=my_balance_val - in_reserve_payment_val - 1; + my_balance_frac=my_balance_frac + 100000000 - in_reserve_payment_frac; + END IF; +ELSE + IF (my_balance_val = in_reserve_payment_val) AND (my_balance_frac >= in_reserve_payment_frac) + THEN + my_balance_val=0; + my_balance_frac=my_balance_frac - in_reserve_payment_frac; + ELSE + out_final_expiration = my_reserve_expiration; + 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_val=my_balance_val + ,current_balance_frac=my_balance_frac + ,gc_date=my_reserve_expiration + in_reserve_gc_delay + ,expiration_date=my_expiration_date + ,purses_allowed=my_purses_allowed +WHERE + reserve_pub=in_reserve_pub; + +out_final_expiration=my_expiration_date; +out_open_cost_val = my_cost_val; +out_open_cost_frac = my_cost_frac; +out_no_funds=FALSE; +RETURN; + +END $$; diff --git a/src/exchangedb/exchange_do_reserve_open_deposit.sql b/src/exchangedb/exchange_do_reserve_open_deposit.sql new file mode 100644 index 000000000..725122702 --- /dev/null +++ b/src/exchangedb/exchange_do_reserve_open_deposit.sql @@ -0,0 +1,87 @@ +-- +-- 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 +-- + + +CREATE OR REPLACE FUNCTION exchange_do_reserve_open_deposit( + IN in_coin_pub BYTEA, + IN in_known_coin_id INT8, + IN in_coin_sig BYTEA, + IN in_reserve_sig BYTEA, + IN in_reserve_pub BYTEA, + IN in_coin_total_val INT8, + IN in_coin_total_frac INT4, + OUT out_insufficient_funds BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + +INSERT INTO exchange.reserves_open_deposits + (reserve_sig + ,reserve_pub + ,coin_pub + ,coin_sig + ,contribution_val + ,contribution_frac + ) + VALUES + (in_reserve_sig + ,in_reserve_pub + ,in_coin_pub + ,in_coin_sig + ,in_coin_total_val + ,in_coin_total_frac) + ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- Idempotent request known, return success. + out_insufficient_funds=FALSE; + RETURN; +END IF; + + +-- Check and update balance of the coin. +UPDATE exchange.known_coins + SET + remaining_frac=remaining_frac-in_coin_total_frac + + CASE + WHEN remaining_frac < in_coin_total_frac + THEN 100000000 + ELSE 0 + END, + remaining_val=remaining_val-in_coin_total_val + - CASE + WHEN remaining_frac < in_coin_total_frac + THEN 1 + ELSE 0 + END + WHERE coin_pub=in_coin_pub + AND ( (remaining_val > in_coin_total_val) OR + ( (remaining_frac >= in_coin_total_frac) AND + (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 $$; + diff --git a/src/exchangedb/exchange_do_reserve_purse.sql b/src/exchangedb/exchange_do_reserve_purse.sql new file mode 100644 index 000000000..a110c85a3 --- /dev/null +++ b/src/exchangedb/exchange_do_reserve_purse.sql @@ -0,0 +1,157 @@ +-- +-- 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 +-- + +CREATE OR REPLACE FUNCTION exchange_do_reserve_purse( + IN in_purse_pub BYTEA, + IN in_merge_sig BYTEA, + IN in_merge_timestamp INT8, + IN in_reserve_sig BYTEA, + IN in_reserve_quota BOOLEAN, + IN in_purse_fee_val INT8, + IN in_purse_fee_frac INT4, + IN in_reserve_pub BYTEA, + IN in_wallet_h_payto BYTEA, + OUT out_no_funds BOOLEAN, + OUT out_no_reserve BOOLEAN, + OUT out_conflict BOOLEAN) +LANGUAGE plpgsql +AS $$ +BEGIN + +-- Store purse merge signature, checks for purse_pub uniqueness +INSERT INTO exchange.purse_merges + (partner_serial_id + ,reserve_pub + ,purse_pub + ,merge_sig + ,merge_timestamp) + VALUES + (NULL + ,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 exchange.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; + out_no_reserve=FALSE; + out_no_funds=FALSE; + RETURN; + END IF; + + -- "success" + out_conflict=FALSE; + out_no_funds=FALSE; + out_no_reserve=FALSE; + RETURN; +END IF; +out_conflict=FALSE; + +PERFORM + FROM exchange.reserves + WHERE reserve_pub=in_reserve_pub; + +out_no_reserve = NOT FOUND; + +IF (in_reserve_quota) +THEN + -- Increment active purses per reserve (and check this is allowed) + IF (out_no_reserve) + THEN + out_no_funds=TRUE; + RETURN; + END IF; + UPDATE exchange.reserves + SET purses_active=purses_active+1 + WHERE reserve_pub=in_reserve_pub + AND purses_active < purses_allowed; + IF NOT FOUND + THEN + out_no_funds=TRUE; + RETURN; + END IF; +ELSE + -- UPDATE reserves balance (and check if balance is enough to pay the fee) + IF (out_no_reserve) + THEN + IF ( (0 != in_purse_fee_val) OR + (0 != in_purse_fee_frac) ) + THEN + out_no_funds=TRUE; + RETURN; + END IF; + ELSE + UPDATE exchange.reserves + SET + current_balance_frac=current_balance_frac-in_purse_fee_frac + + CASE + WHEN current_balance_frac < in_purse_fee_frac + THEN 100000000 + ELSE 0 + END, + current_balance_val=current_balance_val-in_purse_fee_val + - CASE + WHEN current_balance_frac < in_purse_fee_frac + THEN 1 + ELSE 0 + END + WHERE reserve_pub=in_reserve_pub + AND ( (current_balance_val > 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 exchange.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, BYTEA, BOOLEAN, INT8, INT4, BYTEA, BYTEA) + IS 'Create a purse for a reserve.'; + + + + diff --git a/src/exchangedb/exchange_do_withdraw.sql b/src/exchangedb/exchange_do_withdraw.sql new file mode 100644 index 000000000..9689bae5a --- /dev/null +++ b/src/exchangedb/exchange_do_withdraw.sql @@ -0,0 +1,199 @@ +-- +-- 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 +-- + + +CREATE OR REPLACE FUNCTION exchange_do_withdraw( + IN cs_nonce BYTEA, + IN amount_val INT8, + IN amount_frac INT4, + IN h_denom_pub BYTEA, + IN rpub BYTEA, + IN reserve_sig BYTEA, + IN h_coin_envelope BYTEA, + IN denom_sig BYTEA, + IN now INT8, + IN min_reserve_gc INT8, + OUT reserve_found BOOLEAN, + OUT balance_ok BOOLEAN, + OUT nonce_ok BOOLEAN, + OUT ruuid INT8) +LANGUAGE plpgsql +AS $$ +DECLARE + reserve_gc INT8; +DECLARE + denom_serial INT8; +DECLARE + reserve_val INT8; +DECLARE + reserve_frac INT4; +BEGIN +-- Shards: reserves by reserve_pub (SELECT) +-- reserves_out (INSERT, with CONFLICT detection) by wih +-- reserves by reserve_pub (UPDATE) +-- reserves_in by reserve_pub (SELECT) +-- wire_targets by wire_target_h_payto + +SELECT denominations_serial + INTO denom_serial + FROM exchange.denominations + WHERE denom_pub_hash=h_denom_pub; + +IF NOT FOUND +THEN + -- denomination unknown, should be impossible! + reserve_found=FALSE; + balance_ok=FALSE; + ruuid=0; + ASSERT false, 'denomination unknown'; + RETURN; +END IF; + + +SELECT + current_balance_val + ,current_balance_frac + ,gc_date + ,reserve_uuid + INTO + reserve_val + ,reserve_frac + ,reserve_gc + ,ruuid + FROM exchange.reserves + WHERE reserves.reserve_pub=rpub; + +IF NOT FOUND +THEN + -- reserve unknown + reserve_found=FALSE; + balance_ok=FALSE; + nonce_ok=TRUE; + ruuid=2; + RETURN; +END IF; + +-- We optimistically insert, and then on conflict declare +-- the query successful due to idempotency. +INSERT INTO exchange.reserves_out + (h_blind_ev + ,denominations_serial + ,denom_sig + ,reserve_uuid + ,reserve_sig + ,execution_date + ,amount_with_fee_val + ,amount_with_fee_frac) +VALUES + (h_coin_envelope + ,denom_serial + ,denom_sig + ,ruuid + ,reserve_sig + ,now + ,amount_val + ,amount_frac) +ON CONFLICT DO NOTHING; + +IF NOT FOUND +THEN + -- idempotent query, all constraints must be satisfied + reserve_found=TRUE; + balance_ok=TRUE; + nonce_ok=TRUE; + RETURN; +END IF; + +-- Check reserve balance is sufficient. +IF (reserve_val > amount_val) +THEN + IF (reserve_frac >= amount_frac) + THEN + reserve_val=reserve_val - amount_val; + reserve_frac=reserve_frac - amount_frac; + ELSE + reserve_val=reserve_val - amount_val - 1; + reserve_frac=reserve_frac + 100000000 - amount_frac; + END IF; +ELSE + IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac) + THEN + reserve_val=0; + reserve_frac=reserve_frac - amount_frac; + ELSE + reserve_found=TRUE; + nonce_ok=TRUE; -- we do not really know + balance_ok=FALSE; + RETURN; + END IF; +END IF; + +-- Calculate new expiration dates. +min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); + +-- Update reserve balance. +UPDATE reserves SET + gc_date=min_reserve_gc + ,current_balance_val=reserve_val + ,current_balance_frac=reserve_frac +WHERE + reserves.reserve_pub=rpub; + +reserve_found=TRUE; +balance_ok=TRUE; + + + +-- Special actions needed for a CS withdraw? +IF NOT NULL cs_nonce +THEN + -- 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 + (cs_nonce + ,denom_serial + ,h_coin_envelope) + ON CONFLICT DO NOTHING; + + IF NOT FOUND + THEN + -- See if the existing entry is identical. + SELECT 1 + FROM exchange.cs_nonce_locks + WHERE nonce=cs_nonce + AND op_hash=h_coin_envelope; + IF NOT FOUND + THEN + reserve_found=FALSE; + balance_ok=FALSE; + nonce_ok=FALSE; + RETURN; + END IF; + END IF; +ELSE + nonce_ok=TRUE; -- no nonce, hence OK! +END IF; + +END $$; + + +COMMENT ON FUNCTION exchange_do_withdraw(BYTEA, INT8, INT4, BYTEA, BYTEA, BYTEA, BYTEA, BYTEA, INT8, INT8) + IS 'Checks whether the reserve has sufficient balance for a withdraw operation (or the request is repeated and was previously approved) and if so updates the database with the result'; + diff --git a/src/exchangedb/pg_batch_reserves_in_insert.c b/src/exchangedb/pg_batch_reserves_in_insert.c index 4a1a2792e..455f080dd 100644 --- a/src/exchangedb/pg_batch_reserves_in_insert.c +++ b/src/exchangedb/pg_batch_reserves_in_insert.c @@ -62,7 +62,8 @@ notify_on_reserve (struct PostgresClosure *pg, enum GNUNET_DB_QueryStatus TEH_PG_batch_reserves_in_insert (void *cls, - const struct TALER_EXCHANGEDB_ReserveInInfo *reserves, + const struct + TALER_EXCHANGEDB_ReserveInInfo *reserves, unsigned int reserves_length, enum GNUNET_DB_QueryStatus *results) { @@ -83,7 +84,7 @@ TEH_PG_batch_reserves_in_insert (void *cls, "out_reserve_found AS conflicted" ",transaction_duplicate" ",ruuid AS reserve_uuid" - " FROM batch_reserves_in" + " FROM exchange_do_batch_reserves_in" " ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11);"); expiry = GNUNET_TIME_absolute_to_timestamp ( GNUNET_TIME_absolute_add (reserves->execution_time.abs_time, @@ -101,7 +102,7 @@ TEH_PG_batch_reserves_in_insert (void *cls, time; we do this before adding the actual transaction to "reserves_in", as for a new reserve it can't be a duplicate 'add' operation, and as the 'add' operation needs the reserve entry as a foreign key. */ - for (unsigned int i=0;ireserve_pub); GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != qs1); @@ -144,7 +145,7 @@ TEH_PG_batch_reserves_in_insert (void *cls, ? GNUNET_DB_STATUS_SUCCESS_NO_RESULTS : GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; if ( (! conflicted) && transaction_duplicate) - TEH_PG_rollback(pg); + TEH_PG_rollback (pg); } return reserves_length; } diff --git a/src/exchangedb/pg_persist_policy_details.c b/src/exchangedb/pg_persist_policy_details.c index 28e2e4c5e..2b7787878 100644 --- a/src/exchangedb/pg_persist_policy_details.c +++ b/src/exchangedb/pg_persist_policy_details.c @@ -59,7 +59,7 @@ TEH_PG_persist_policy_details ( GNUNET_PQ_result_spec_end }; - + // FIXME: prepare missing!!?! return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "call_insert_or_update_policy_details", params, diff --git a/src/exchangedb/plugin_exchangedb_postgres.c b/src/exchangedb/plugin_exchangedb_postgres.c index cdb9b6233..d04df4677 100644 --- a/src/exchangedb/plugin_exchangedb_postgres.c +++ b/src/exchangedb/plugin_exchangedb_postgres.c @@ -249,8 +249,6 @@ prepare_statements (struct PostgresClosure *pg) " FROM wire_targets" " WHERE wire_target_h_payto=$1" " LIMIT 1;"), - - /* Used in #postgres_ensure_coin_known() */ GNUNET_PQ_make_prepare ( "get_known_coin_dh", @@ -289,12 +287,6 @@ prepare_statements (struct PostgresClosure *pg) " ON (ref.coin_pub=dep.coin_pub AND ref.deposit_serial_id=dep.deposit_serial_id)" " WHERE ref.refund_serial_id=$1" " GROUP BY (dep.amount_with_fee_val, dep.amount_with_fee_frac);"), - /* Used in #postgres_do_account_merge() */ - GNUNET_PQ_make_prepare ( - "call_account_merge", - "SELECT 1" - " FROM exchange_do_account_merge" - " ($1, $2, $3);"), /* Used in #postgres_update_kyc_requirement_by_row() */ GNUNET_PQ_make_prepare ( "update_legitimization_process", @@ -495,56 +487,6 @@ postgres_get_policy_details ( } -/* Persist the details to a policy in the policy_details table. If there - * already exists a policy, update the fields accordingly. - * - * @param details The policy details that should be persisted. If an entry for - * the given details->hash_code exists, the values will be updated. - * @param[out] policy_details_serial_id The row ID of the policy details - * @param[out] accumulated_total The total amount accumulated in that policy - * @param[out] fulfillment_state The state of policy. If the state was Insufficient prior to the call and the provided deposit raises the accumulated_total above the commitment, it will be set to Ready. - * @return query execution status - */ -static enum GNUNET_DB_QueryStatus -postgres_persist_policy_details ( - void *cls, - const struct TALER_PolicyDetails *details, - uint64_t *policy_details_serial_id, - struct TALER_Amount *accumulated_total, - enum TALER_PolicyFulfillmentState *fulfillment_state) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_auto_from_type (&details->hash_code), - TALER_PQ_query_param_json (details->policy_json), - GNUNET_PQ_query_param_timestamp (&details->deadline), - TALER_PQ_query_param_amount (&details->commitment), - TALER_PQ_query_param_amount (&details->accumulated_total), - TALER_PQ_query_param_amount (&details->policy_fee), - TALER_PQ_query_param_amount (&details->transferable_amount), - GNUNET_PQ_query_param_auto_from_type (&details->fulfillment_state), - (details->no_policy_fulfillment_id) - ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_uint64 (&details->policy_fulfillment_id), - GNUNET_PQ_query_param_end - }; - struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("policy_details_serial_id", - policy_details_serial_id), - TALER_PQ_RESULT_SPEC_AMOUNT ("accumulated_total", - accumulated_total), - GNUNET_PQ_result_spec_uint32 ("fulfillment_state", - fulfillment_state), - GNUNET_PQ_result_spec_end - }; - - return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "call_insert_or_update_policy_details", - params, - rs); -} - - /** * Perform melt operation, checking for sufficient balance * of the coin and possibly persisting the melt details. @@ -5019,7 +4961,6 @@ libtaler_plugin_exchangedb_postgres_init (void *cls) plugin = GNUNET_new (struct TALER_EXCHANGEDB_Plugin); plugin->cls = pg; plugin->get_policy_details = &postgres_get_policy_details; - plugin->persist_policy_details = &postgres_persist_policy_details; plugin->add_policy_fulfillment_proof = &postgres_add_policy_fulfillment_proof; plugin->do_melt = &postgres_do_melt; plugin->do_refund = &postgres_do_refund; diff --git a/src/exchangedb/procedures.sql b/src/exchangedb/procedures.sql deleted file mode 100644 index a869d4739..000000000 --- a/src/exchangedb/procedures.sql +++ /dev/null @@ -1,2641 +0,0 @@ --- --- 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; - -SET search_path TO exchange; - - -CREATE OR REPLACE FUNCTION exchange_do_withdraw( - IN cs_nonce BYTEA, - IN amount_val INT8, - IN amount_frac INT4, - IN h_denom_pub BYTEA, - IN rpub BYTEA, - IN reserve_sig BYTEA, - IN h_coin_envelope BYTEA, - IN denom_sig BYTEA, - IN now INT8, - IN min_reserve_gc INT8, - OUT reserve_found BOOLEAN, - OUT balance_ok BOOLEAN, - OUT nonce_ok BOOLEAN, - OUT ruuid INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - reserve_gc INT8; -DECLARE - denom_serial INT8; -DECLARE - reserve_val INT8; -DECLARE - reserve_frac INT4; -BEGIN --- Shards: reserves by reserve_pub (SELECT) --- reserves_out (INSERT, with CONFLICT detection) by wih --- reserves by reserve_pub (UPDATE) --- reserves_in by reserve_pub (SELECT) --- wire_targets by wire_target_h_payto - -SELECT denominations_serial - INTO denom_serial - FROM exchange.denominations - WHERE denom_pub_hash=h_denom_pub; - -IF NOT FOUND -THEN - -- denomination unknown, should be impossible! - reserve_found=FALSE; - balance_ok=FALSE; - ruuid=0; - ASSERT false, 'denomination unknown'; - RETURN; -END IF; - - -SELECT - current_balance_val - ,current_balance_frac - ,gc_date - ,reserve_uuid - INTO - reserve_val - ,reserve_frac - ,reserve_gc - ,ruuid - FROM exchange.reserves - WHERE reserves.reserve_pub=rpub; - -IF NOT FOUND -THEN - -- reserve unknown - reserve_found=FALSE; - balance_ok=FALSE; - nonce_ok=TRUE; - ruuid=2; - RETURN; -END IF; - --- We optimistically insert, and then on conflict declare --- the query successful due to idempotency. -INSERT INTO exchange.reserves_out - (h_blind_ev - ,denominations_serial - ,denom_sig - ,reserve_uuid - ,reserve_sig - ,execution_date - ,amount_with_fee_val - ,amount_with_fee_frac) -VALUES - (h_coin_envelope - ,denom_serial - ,denom_sig - ,ruuid - ,reserve_sig - ,now - ,amount_val - ,amount_frac) -ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- idempotent query, all constraints must be satisfied - reserve_found=TRUE; - balance_ok=TRUE; - nonce_ok=TRUE; - RETURN; -END IF; - --- Check reserve balance is sufficient. -IF (reserve_val > amount_val) -THEN - IF (reserve_frac >= amount_frac) - THEN - reserve_val=reserve_val - amount_val; - reserve_frac=reserve_frac - amount_frac; - ELSE - reserve_val=reserve_val - amount_val - 1; - reserve_frac=reserve_frac + 100000000 - amount_frac; - END IF; -ELSE - IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac) - THEN - reserve_val=0; - reserve_frac=reserve_frac - amount_frac; - ELSE - reserve_found=TRUE; - nonce_ok=TRUE; -- we do not really know - balance_ok=FALSE; - RETURN; - END IF; -END IF; - --- Calculate new expiration dates. -min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); - --- Update reserve balance. -UPDATE reserves SET - gc_date=min_reserve_gc - ,current_balance_val=reserve_val - ,current_balance_frac=reserve_frac -WHERE - reserves.reserve_pub=rpub; - -reserve_found=TRUE; -balance_ok=TRUE; - - - --- Special actions needed for a CS withdraw? -IF NOT NULL cs_nonce -THEN - -- 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 - (cs_nonce - ,denom_serial - ,h_coin_envelope) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - -- See if the existing entry is identical. - SELECT 1 - FROM exchange.cs_nonce_locks - WHERE nonce=cs_nonce - AND op_hash=h_coin_envelope; - IF NOT FOUND - THEN - reserve_found=FALSE; - balance_ok=FALSE; - nonce_ok=FALSE; - RETURN; - END IF; - END IF; -ELSE - nonce_ok=TRUE; -- no nonce, hence OK! -END IF; - -END $$; - - -COMMENT ON FUNCTION exchange_do_withdraw(BYTEA, INT8, INT4, BYTEA, BYTEA, BYTEA, BYTEA, BYTEA, INT8, INT8) - IS 'Checks whether the reserve has sufficient balance for a withdraw operation (or the request is repeated and was previously approved) and if so updates the database with the result'; - - - - -CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw( - IN amount_val INT8, - IN amount_frac INT4, - IN rpub BYTEA, - IN now INT8, - IN min_reserve_gc INT8, - OUT reserve_found BOOLEAN, - OUT balance_ok BOOLEAN, - OUT ruuid INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - reserve_gc INT8; -DECLARE - reserve_val INT8; -DECLARE - reserve_frac INT4; -BEGIN --- Shards: reserves by reserve_pub (SELECT) --- reserves_out (INSERT, with CONFLICT detection) by wih --- reserves by reserve_pub (UPDATE) --- reserves_in by reserve_pub (SELECT) --- wire_targets by wire_target_h_payto - -SELECT - current_balance_val - ,current_balance_frac - ,gc_date - ,reserve_uuid - INTO - reserve_val - ,reserve_frac - ,reserve_gc - ,ruuid - FROM exchange.reserves - WHERE reserves.reserve_pub=rpub; - -IF NOT FOUND -THEN - -- reserve unknown - reserve_found=FALSE; - balance_ok=FALSE; - ruuid=2; - RETURN; -END IF; - --- Check reserve balance is sufficient. -IF (reserve_val > amount_val) -THEN - IF (reserve_frac >= amount_frac) - THEN - reserve_val=reserve_val - amount_val; - reserve_frac=reserve_frac - amount_frac; - ELSE - reserve_val=reserve_val - amount_val - 1; - reserve_frac=reserve_frac + 100000000 - amount_frac; - END IF; -ELSE - IF (reserve_val = amount_val) AND (reserve_frac >= amount_frac) - THEN - reserve_val=0; - reserve_frac=reserve_frac - amount_frac; - ELSE - reserve_found=TRUE; - balance_ok=FALSE; - RETURN; - END IF; -END IF; - --- Calculate new expiration dates. -min_reserve_gc=GREATEST(min_reserve_gc,reserve_gc); - --- Update reserve balance. -UPDATE reserves SET - gc_date=min_reserve_gc - ,current_balance_val=reserve_val - ,current_balance_frac=reserve_frac -WHERE - reserves.reserve_pub=rpub; - -reserve_found=TRUE; -balance_ok=TRUE; - -END $$; - -COMMENT ON FUNCTION exchange_do_batch_withdraw(INT8, INT4, BYTEA, INT8, INT8) - IS 'Checks whether the reserve has sufficient balance for a withdraw operation (or the request is repeated and was previously approved) and if so updates the database with the result. Excludes storing the planchets.'; - - - - - -CREATE OR REPLACE FUNCTION exchange_do_batch_withdraw_insert( - IN cs_nonce BYTEA, - IN amount_val INT8, - IN amount_frac INT4, - IN h_denom_pub BYTEA, - IN ruuid INT8, - IN reserve_sig BYTEA, - IN h_coin_envelope BYTEA, - IN denom_sig BYTEA, - IN now INT8, - OUT out_denom_unknown BOOLEAN, - OUT out_nonce_reuse BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - denom_serial INT8; -BEGIN --- Shards: reserves by reserve_pub (SELECT) --- reserves_out (INSERT, with CONFLICT detection) by wih --- reserves by reserve_pub (UPDATE) --- reserves_in by reserve_pub (SELECT) --- wire_targets by wire_target_h_payto - -out_denom_unknown=TRUE; -out_conflict=TRUE; -out_nonce_reuse=TRUE; - -SELECT denominations_serial - INTO denom_serial - FROM exchange.denominations - WHERE denom_pub_hash=h_denom_pub; - -IF NOT FOUND -THEN - -- denomination unknown, should be impossible! - out_denom_unknown=TRUE; - ASSERT false, 'denomination unknown'; - RETURN; -END IF; -out_denom_unknown=FALSE; - -INSERT INTO exchange.reserves_out - (h_blind_ev - ,denominations_serial - ,denom_sig - ,reserve_uuid - ,reserve_sig - ,execution_date - ,amount_with_fee_val - ,amount_with_fee_frac) -VALUES - (h_coin_envelope - ,denom_serial - ,denom_sig - ,ruuid - ,reserve_sig - ,now - ,amount_val - ,amount_frac) -ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - out_conflict=TRUE; - RETURN; -END IF; -out_conflict=FALSE; - --- Special actions needed for a CS withdraw? -out_nonce_reuse=FALSE; -IF NOT NULL cs_nonce -THEN - -- 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 - (cs_nonce - ,denom_serial - ,h_coin_envelope) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - -- See if the existing entry is identical. - SELECT 1 - FROM exchange.cs_nonce_locks - WHERE nonce=cs_nonce - AND op_hash=h_coin_envelope; - IF NOT FOUND - THEN - out_nonce_reuse=TRUE; - ASSERT false, 'nonce reuse attempted by client'; - RETURN; - END IF; - END IF; -END IF; - -END $$; - -COMMENT ON FUNCTION exchange_do_batch_withdraw_insert(BYTEA, INT8, INT4, BYTEA, INT8, BYTEA, BYTEA, BYTEA, INT8) - IS 'Stores information about a planchet for a batch withdraw operation. Checks if the planchet already exists, and in that case indicates a conflict'; - - - - --- NOTE: experiment, currently dead, see postgres_Start_deferred_wire_out; --- now done inline. FIXME: Remove code here once inline version is confirmed working nicely! -CREATE OR REPLACE PROCEDURE defer_wire_out() -LANGUAGE plpgsql -AS $$ -BEGIN - -IF EXISTS ( - SELECT 1 - FROM exchange.information_Schema.constraint_column_usage - WHERE table_name='wire_out' - AND constraint_name='wire_out_ref') -THEN - SET CONSTRAINTS wire_out_ref DEFERRED; -END IF; - -END $$; - - -CREATE OR REPLACE FUNCTION exchange_do_recoup_by_reserve( - IN res_pub BYTEA -) -RETURNS TABLE -( - denom_sig BYTEA, - denominations_serial BIGINT, - coin_pub BYTEA, - coin_sig BYTEA, - coin_blind BYTEA, - amount_val BIGINT, - amount_frac INTEGER, - recoup_timestamp BIGINT -) -LANGUAGE plpgsql -AS $$ -DECLARE - res_uuid BIGINT; - blind_ev BYTEA; - c_pub BYTEA; -BEGIN - SELECT reserve_uuid - INTO res_uuid - FROM exchange.reserves - WHERE reserves.reserve_pub = res_pub; - - FOR blind_ev IN - SELECT h_blind_ev - FROM exchange.reserves_out_by_reserve - WHERE reserves_out_by_reserve.reserve_uuid = res_uuid - LOOP - SELECT robr.coin_pub - INTO c_pub - FROM exchange.recoup_by_reserve robr - WHERE robr.reserve_out_serial_id = ( - SELECT reserves_out.reserve_out_serial_id - FROM exchange.reserves_out - WHERE reserves_out.h_blind_ev = blind_ev - ); - RETURN QUERY - SELECT kc.denom_sig, - kc.denominations_serial, - rc.coin_pub, - rc.coin_sig, - rc.coin_blind, - rc.amount_val, - rc.amount_frac, - rc.recoup_timestamp - FROM ( - SELECT * - FROM exchange.known_coins - WHERE known_coins.coin_pub = c_pub - ) kc - JOIN ( - SELECT * - FROM exchange.recoup - WHERE recoup.coin_pub = c_pub - ) rc USING (coin_pub); - END LOOP; -END; -$$; - -COMMENT ON FUNCTION exchange_do_recoup_by_reserve - IS 'Recoup by reserve as a function to make sure we hit only the needed partition and not all when joining as joins on distributed tables fetch ALL rows from the shards'; - - -CREATE OR REPLACE FUNCTION exchange_do_deposit( - IN in_amount_with_fee_val INT8, - IN in_amount_with_fee_frac INT4, - IN in_h_contract_terms BYTEA, - IN in_wire_salt BYTEA, - IN in_wallet_timestamp INT8, - IN in_exchange_timestamp INT8, - IN in_refund_deadline INT8, - IN in_wire_deadline INT8, - IN in_merchant_pub BYTEA, - IN in_receiver_wire_account VARCHAR, - IN in_h_payto BYTEA, - IN in_known_coin_id INT8, - IN in_coin_pub BYTEA, - IN in_coin_sig BYTEA, - IN in_shard INT8, - IN in_policy_blocked BOOLEAN, - IN in_policy_details_serial_id INT8, - OUT out_exchange_timestamp INT8, - OUT out_balance_ok BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - wtsi INT8; -- wire target serial id -BEGIN --- Shards: INSERT policy_details (by policy_details_serial_id) --- INSERT wire_targets (by h_payto), on CONFLICT DO NOTHING; --- INSERT deposits (by coin_pub, shard), ON CONFLICT DO NOTHING; --- UPDATE known_coins (by coin_pub) - -INSERT INTO exchange.wire_targets - (wire_target_h_payto - ,payto_uri) - VALUES - (in_h_payto - ,in_receiver_wire_account) -ON CONFLICT DO NOTHING -- for CONFLICT ON (wire_target_h_payto) - RETURNING wire_target_serial_id INTO wtsi; - -IF NOT FOUND -THEN - SELECT wire_target_serial_id - INTO wtsi - FROM exchange.wire_targets - WHERE wire_target_h_payto=in_h_payto; -END IF; - - -INSERT INTO exchange.deposits - (shard - ,coin_pub - ,known_coin_id - ,amount_with_fee_val - ,amount_with_fee_frac - ,wallet_timestamp - ,exchange_timestamp - ,refund_deadline - ,wire_deadline - ,merchant_pub - ,h_contract_terms - ,coin_sig - ,wire_salt - ,wire_target_h_payto - ,policy_blocked - ,policy_details_serial_id - ) - VALUES - (in_shard - ,in_coin_pub - ,in_known_coin_id - ,in_amount_with_fee_val - ,in_amount_with_fee_frac - ,in_wallet_timestamp - ,in_exchange_timestamp - ,in_refund_deadline - ,in_wire_deadline - ,in_merchant_pub - ,in_h_contract_terms - ,in_coin_sig - ,in_wire_salt - ,in_h_payto - ,in_policy_blocked - ,in_policy_details_serial_id) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Idempotency check: see if an identical record exists. - -- Note that by checking 'coin_sig', we implicitly check - -- identity over everything that the signature covers. - -- We do select over merchant_pub and wire_target_h_payto - -- primarily here to maximally use the existing index. - SELECT - exchange_timestamp - INTO - out_exchange_timestamp - FROM exchange.deposits - WHERE shard=in_shard - AND merchant_pub=in_merchant_pub - AND wire_target_h_payto=in_h_payto - AND coin_pub=in_coin_pub - AND coin_sig=in_coin_sig; - -- AND policy_details_serial_id=in_policy_details_serial_id; -- FIXME: is this required for idempotency? - - IF NOT FOUND - THEN - -- Deposit exists, but with differences. Not allowed. - out_balance_ok=FALSE; - out_conflict=TRUE; - RETURN; - END IF; - - -- Idempotent request known, return success. - out_balance_ok=TRUE; - out_conflict=FALSE; - - RETURN; -END IF; - - -out_exchange_timestamp=in_exchange_timestamp; - --- Check and update balance of the coin. -UPDATE known_coins - SET - remaining_frac=remaining_frac-in_amount_with_fee_frac - + CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val-in_amount_with_fee_val - - CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 1 - ELSE 0 - END - WHERE coin_pub=in_coin_pub - AND ( (remaining_val > in_amount_with_fee_val) OR - ( (remaining_frac >= in_amount_with_fee_frac) AND - (remaining_val >= in_amount_with_fee_val) ) ); - -IF NOT FOUND -THEN - -- Insufficient balance. - out_balance_ok=FALSE; - out_conflict=FALSE; - RETURN; -END IF; - --- Everything fine, return success! -out_balance_ok=TRUE; -out_conflict=FALSE; - -END $$; - - - -CREATE OR REPLACE FUNCTION exchange_do_melt( - IN in_cs_rms BYTEA, - IN in_amount_with_fee_val INT8, - IN in_amount_with_fee_frac INT4, - IN in_rc BYTEA, - IN in_old_coin_pub BYTEA, - IN in_old_coin_sig BYTEA, - IN in_known_coin_id INT8, -- not used, but that's OK - IN in_noreveal_index INT4, - IN in_zombie_required BOOLEAN, - OUT out_balance_ok BOOLEAN, - OUT out_zombie_bad BOOLEAN, - OUT out_noreveal_index INT4) -LANGUAGE plpgsql -AS $$ -DECLARE - denom_max INT8; -BEGIN --- Shards: INSERT refresh_commitments (by rc) --- (rare:) SELECT refresh_commitments (by old_coin_pub) -- crosses shards! --- (rare:) SEELCT refresh_revealed_coins (by melt_serial_id) --- (rare:) PERFORM recoup_refresh (by rrc_serial) -- crosses shards! --- UPDATE known_coins (by coin_pub) - -INSERT INTO exchange.refresh_commitments - (rc - ,old_coin_pub - ,old_coin_sig - ,amount_with_fee_val - ,amount_with_fee_frac - ,noreveal_index - ) - VALUES - (in_rc - ,in_old_coin_pub - ,in_old_coin_sig - ,in_amount_with_fee_val - ,in_amount_with_fee_frac - ,in_noreveal_index) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Idempotency check: see if an identical record exists. - out_noreveal_index=-1; - SELECT - noreveal_index - INTO - out_noreveal_index - FROM exchange.refresh_commitments - WHERE rc=in_rc; - out_balance_ok=FOUND; - out_zombie_bad=FALSE; -- zombie is OK - RETURN; -END IF; - - -IF in_zombie_required -THEN - -- Check if this coin was part of a refresh - -- operation that was subsequently involved - -- in a recoup operation. We begin by all - -- refresh operations our coin was involved - -- with, then find all associated reveal - -- operations, and then see if any of these - -- reveal operations was involved in a recoup. - PERFORM - FROM exchange.recoup_refresh - WHERE rrc_serial IN - (SELECT rrc_serial - FROM exchange.refresh_revealed_coins - WHERE melt_serial_id IN - (SELECT melt_serial_id - FROM exchange.refresh_commitments - WHERE old_coin_pub=in_old_coin_pub)); - IF NOT FOUND - THEN - out_zombie_bad=TRUE; - out_balance_ok=FALSE; - RETURN; - END IF; -END IF; - -out_zombie_bad=FALSE; -- zombie is OK - - --- Check and update balance of the coin. -UPDATE known_coins - SET - remaining_frac=remaining_frac-in_amount_with_fee_frac - + CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val-in_amount_with_fee_val - - CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 1 - ELSE 0 - END - WHERE coin_pub=in_old_coin_pub - AND ( (remaining_val > in_amount_with_fee_val) OR - ( (remaining_frac >= in_amount_with_fee_frac) AND - (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 NOT NULL in_cs_rms -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 - (cs_rms - ,denom_serial - ,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=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 $$; - - - -CREATE OR REPLACE FUNCTION exchange_do_refund( - IN in_amount_with_fee_val INT8, - IN in_amount_with_fee_frac INT4, - IN in_amount_val INT8, - IN in_amount_frac INT4, - IN in_deposit_fee_val INT8, - IN in_deposit_fee_frac INT4, - IN in_h_contract_terms BYTEA, - IN in_rtransaction_id INT8, - IN in_deposit_shard INT8, - IN in_known_coin_id INT8, - IN in_coin_pub BYTEA, - IN in_merchant_pub BYTEA, - IN in_merchant_sig BYTEA, - OUT out_not_found BOOLEAN, - OUT out_refund_ok BOOLEAN, - OUT out_gone BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - dsi INT8; -- ID of deposit being refunded -DECLARE - tmp_val INT8; -- total amount refunded -DECLARE - tmp_frac INT8; -- total amount refunded -DECLARE - deposit_val INT8; -- amount that was originally deposited -DECLARE - deposit_frac INT8; -- amount that was originally deposited -BEGIN --- Shards: SELECT deposits (coin_pub, shard, h_contract_terms, merchant_pub) --- INSERT refunds (by coin_pub, rtransaction_id) ON CONFLICT DO NOTHING --- SELECT refunds (by coin_pub) --- UPDATE known_coins (by coin_pub) - -SELECT - deposit_serial_id - ,amount_with_fee_val - ,amount_with_fee_frac - ,done -INTO - dsi - ,deposit_val - ,deposit_frac - ,out_gone -FROM exchange.deposits - WHERE coin_pub=in_coin_pub - AND shard=in_deposit_shard - AND merchant_pub=in_merchant_pub - AND h_contract_terms=in_h_contract_terms; - -IF NOT FOUND -THEN - -- No matching deposit found! - out_refund_ok=FALSE; - out_conflict=FALSE; - out_not_found=TRUE; - out_gone=FALSE; - RETURN; -END IF; - -INSERT INTO exchange.refunds - (deposit_serial_id - ,coin_pub - ,merchant_sig - ,rtransaction_id - ,amount_with_fee_val - ,amount_with_fee_frac - ) - VALUES - (dsi - ,in_coin_pub - ,in_merchant_sig - ,in_rtransaction_id - ,in_amount_with_fee_val - ,in_amount_with_fee_frac) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Idempotency check: see if an identical record exists. - -- Note that by checking 'coin_sig', we implicitly check - -- identity over everything that the signature covers. - -- We do select over merchant_pub and h_contract_terms - -- primarily here to maximally use the existing index. - PERFORM - FROM exchange.refunds - WHERE coin_pub=in_coin_pub - AND deposit_serial_id=dsi - AND rtransaction_id=in_rtransaction_id - AND amount_with_fee_val=in_amount_with_fee_val - AND amount_with_fee_frac=in_amount_with_fee_frac; - - IF NOT FOUND - THEN - -- Deposit exists, but have conflicting refund. - out_refund_ok=FALSE; - out_conflict=TRUE; - out_not_found=FALSE; - RETURN; - END IF; - - -- Idempotent request known, return success. - out_refund_ok=TRUE; - out_conflict=FALSE; - out_not_found=FALSE; - out_gone=FALSE; - RETURN; -END IF; - -IF out_gone -THEN - -- money already sent to the merchant. Tough luck. - out_refund_ok=FALSE; - out_conflict=FALSE; - out_not_found=FALSE; - RETURN; -END IF; - --- Check refund balance invariant. -SELECT - SUM(amount_with_fee_val) -- overflow here is not plausible - ,SUM(CAST(amount_with_fee_frac AS INT8)) -- compute using 64 bits - INTO - tmp_val - ,tmp_frac - FROM exchange.refunds - WHERE coin_pub=in_coin_pub - AND deposit_serial_id=dsi; -IF tmp_val IS NULL -THEN - RAISE NOTICE 'failed to sum up existing refunds'; - out_refund_ok=FALSE; - out_conflict=FALSE; - out_not_found=FALSE; - RETURN; -END IF; - --- Normalize result before continuing -tmp_val = tmp_val + tmp_frac / 100000000; -tmp_frac = tmp_frac % 100000000; - --- Actually check if the deposits are sufficient for the refund. Verbosely. ;-) -IF (tmp_val < deposit_val) -THEN - out_refund_ok=TRUE; -ELSE - IF (tmp_val = deposit_val) AND (tmp_frac <= deposit_frac) - THEN - out_refund_ok=TRUE; - ELSE - out_refund_ok=FALSE; - END IF; -END IF; - -IF (tmp_val = deposit_val) AND (tmp_frac = deposit_frac) -THEN - -- Refunds have reached the full value of the original - -- deposit. Also refund the deposit fee. - in_amount_frac = in_amount_frac + in_deposit_fee_frac; - in_amount_val = in_amount_val + in_deposit_fee_val; - - -- Normalize result before continuing - in_amount_val = in_amount_val + in_amount_frac / 100000000; - in_amount_frac = in_amount_frac % 100000000; -END IF; - --- Update balance of the coin. -UPDATE known_coins - SET - remaining_frac=remaining_frac+in_amount_frac - - CASE - WHEN remaining_frac+in_amount_frac >= 100000000 - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val+in_amount_val - + CASE - WHEN 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(INT8, INT4, BYTEA, BOOLEAN, BOOLEAN) --- IS 'Executes a refund operation, checking that the corresponding deposit was sufficient to cover the refunded amount'; - - - - -CREATE OR REPLACE FUNCTION exchange_do_recoup_to_reserve( - IN in_reserve_pub BYTEA, - IN in_reserve_out_serial_id INT8, - IN in_coin_blind BYTEA, - IN in_coin_pub BYTEA, - IN in_known_coin_id INT8, - IN in_coin_sig BYTEA, - IN in_reserve_gc INT8, - IN in_reserve_expiration INT8, - IN in_recoup_timestamp INT8, - OUT out_recoup_ok BOOLEAN, - OUT out_internal_failure BOOLEAN, - OUT out_recoup_timestamp INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - tmp_val INT8; -- amount recouped -DECLARE - tmp_frac INT8; -- amount recouped -BEGIN --- Shards: SELECT known_coins (by coin_pub) --- SELECT recoup (by coin_pub) --- UPDATE known_coins (by coin_pub) --- UPDATE reserves (by reserve_pub) --- INSERT recoup (by coin_pub) - -out_internal_failure=FALSE; - - --- Check remaining balance of the coin. -SELECT - remaining_frac - ,remaining_val - INTO - tmp_frac - ,tmp_val -FROM exchange.known_coins - WHERE coin_pub=in_coin_pub; - -IF NOT FOUND -THEN - out_internal_failure=TRUE; - out_recoup_ok=FALSE; - RETURN; -END IF; - -IF tmp_val + tmp_frac = 0 -THEN - -- Check for idempotency - SELECT - recoup_timestamp - INTO - out_recoup_timestamp - FROM exchange.recoup - WHERE coin_pub=in_coin_pub; - - out_recoup_ok=FOUND; - RETURN; -END IF; - - --- Update balance of the coin. -UPDATE known_coins - SET - remaining_frac=0 - ,remaining_val=0 - WHERE coin_pub=in_coin_pub; - - --- Credit the reserve and update reserve timers. -UPDATE reserves - SET - current_balance_frac=current_balance_frac+tmp_frac - - CASE - WHEN current_balance_frac+tmp_frac >= 100000000 - THEN 100000000 - ELSE 0 - END, - current_balance_val=current_balance_val+tmp_val - + CASE - WHEN current_balance_frac+tmp_frac >= 100000000 - THEN 1 - ELSE 0 - END, - 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_val - ,amount_frac - ,recoup_timestamp - ,reserve_out_serial_id - ) -VALUES - (in_coin_pub - ,in_coin_sig - ,in_coin_blind - ,tmp_val - ,tmp_frac - ,in_recoup_timestamp - ,in_reserve_out_serial_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'; - - - - - - -CREATE OR REPLACE FUNCTION exchange_do_recoup_to_coin( - IN in_old_coin_pub BYTEA, - IN in_rrc_serial INT8, - IN in_coin_blind BYTEA, - IN in_coin_pub BYTEA, - IN in_known_coin_id INT8, - IN in_coin_sig BYTEA, - IN in_recoup_timestamp INT8, - OUT out_recoup_ok BOOLEAN, - OUT out_internal_failure BOOLEAN, - OUT out_recoup_timestamp INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - tmp_val INT8; -- amount recouped -DECLARE - tmp_frac INT8; -- amount recouped -BEGIN - --- Shards: UPDATE known_coins (by coin_pub) --- SELECT recoup_refresh (by coin_pub) --- UPDATE known_coins (by coin_pub) --- INSERT recoup_refresh (by coin_pub) - - -out_internal_failure=FALSE; - - --- Check remaining balance of the coin. -SELECT - remaining_frac - ,remaining_val - INTO - tmp_frac - ,tmp_val -FROM exchange.known_coins - WHERE coin_pub=in_coin_pub; - -IF NOT FOUND -THEN - out_internal_failure=TRUE; - out_recoup_ok=FALSE; - RETURN; -END IF; - -IF tmp_val + tmp_frac = 0 -THEN - -- Check for idempotency - SELECT - recoup_timestamp - INTO - out_recoup_timestamp - FROM exchange.recoup_refresh - WHERE coin_pub=in_coin_pub; - out_recoup_ok=FOUND; - RETURN; -END IF; - --- Update balance of the coin. -UPDATE known_coins - SET - remaining_frac=0 - ,remaining_val=0 - WHERE coin_pub=in_coin_pub; - - --- Credit the old coin. -UPDATE known_coins - SET - remaining_frac=remaining_frac+tmp_frac - - CASE - WHEN remaining_frac+tmp_frac >= 100000000 - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val+tmp_val - + CASE - WHEN 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 exchange.recoup_refresh - (coin_pub - ,known_coin_id - ,coin_sig - ,coin_blind - ,amount_val - ,amount_frac - ,recoup_timestamp - ,rrc_serial - ) -VALUES - (in_coin_pub - ,in_known_coin_id - ,in_coin_sig - ,in_coin_blind - ,tmp_val - ,tmp_frac - ,in_recoup_timestamp - ,in_rrc_serial); - --- 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'; - - - -CREATE OR REPLACE PROCEDURE exchange_do_gc( - IN in_ancient_date INT8, - IN in_now INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - reserve_uuid_min INT8; -- minimum reserve UUID still alive -DECLARE - melt_min INT8; -- minimum melt still alive -DECLARE - coin_min INT8; -- minimum known_coin still alive -DECLARE - deposit_min INT8; -- minimum deposit still alive -DECLARE - reserve_out_min INT8; -- minimum reserve_out still alive -DECLARE - denom_min INT8; -- minimum denomination still alive -BEGIN - -DELETE FROM exchange.prewire - WHERE finished=TRUE; - -DELETE FROM exchange.wire_fee - WHERE end_date < in_ancient_date; - --- TODO: use closing fee as threshold? -DELETE FROM exchange.reserves - WHERE gc_date < in_now - AND current_balance_val = 0 - AND current_balance_frac = 0; - -SELECT - reserve_out_serial_id - INTO - reserve_out_min - FROM exchange.reserves_out - ORDER BY reserve_out_serial_id ASC - LIMIT 1; - -DELETE FROM exchange.recoup - WHERE reserve_out_serial_id < reserve_out_min; --- FIXME: recoup_refresh lacks GC! - -SELECT - reserve_uuid - INTO - reserve_uuid_min - FROM exchange.reserves - ORDER BY reserve_uuid ASC - LIMIT 1; - -DELETE FROM exchange.reserves_out - WHERE reserve_uuid < reserve_uuid_min; - --- FIXME: this query will be horribly slow; --- need to find another way to formulate it... -DELETE FROM exchange.denominations - WHERE expire_legal < in_now - AND denominations_serial NOT IN - (SELECT DISTINCT denominations_serial - FROM exchange.reserves_out) - AND denominations_serial NOT IN - (SELECT DISTINCT denominations_serial - FROM exchange.known_coins - WHERE coin_pub IN - (SELECT DISTINCT coin_pub - FROM exchange.recoup)) - AND denominations_serial NOT IN - (SELECT DISTINCT denominations_serial - FROM exchange.known_coins - WHERE coin_pub IN - (SELECT DISTINCT coin_pub - FROM exchange.recoup_refresh)); - -SELECT - melt_serial_id - INTO - melt_min - FROM exchange.refresh_commitments - ORDER BY melt_serial_id ASC - LIMIT 1; - -DELETE FROM exchange.refresh_revealed_coins - WHERE melt_serial_id < melt_min; - -DELETE FROM exchange.refresh_transfer_keys - WHERE melt_serial_id < melt_min; - -SELECT - known_coin_id - INTO - coin_min - FROM exchange.known_coins - ORDER BY known_coin_id ASC - LIMIT 1; - -DELETE FROM exchange.deposits - WHERE known_coin_id < coin_min; - -SELECT - deposit_serial_id - INTO - deposit_min - FROM exchange.deposits - ORDER BY deposit_serial_id ASC - LIMIT 1; - -DELETE FROM exchange.refunds - WHERE deposit_serial_id < deposit_min; - -DELETE FROM exchange.aggregation_tracking - WHERE deposit_serial_id < deposit_min; - -SELECT - denominations_serial - INTO - denom_min - FROM exchange.denominations - ORDER BY denominations_serial ASC - LIMIT 1; - -DELETE FROM exchange.cs_nonce_locks - WHERE max_denomination_serial <= denom_min; - -END $$; - - - - -CREATE OR REPLACE FUNCTION exchange_do_purse_deposit( - IN in_partner_id INT8, - IN in_purse_pub BYTEA, - IN in_amount_with_fee_val INT8, - IN in_amount_with_fee_frac INT4, - IN in_coin_pub BYTEA, - IN in_coin_sig BYTEA, - IN in_amount_without_fee_val INT8, - IN in_amount_without_fee_frac INT4, - IN in_reserve_expiration INT8, - IN in_now INT8, - OUT out_balance_ok BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - was_merged BOOLEAN; -DECLARE - psi INT8; -- partner's serial ID (set if merged) -DECLARE - my_amount_val INT8; -- total in purse -DECLARE - my_amount_frac INT4; -- total in purse -DECLARE - was_paid BOOLEAN; -DECLARE - my_in_reserve_quota BOOLEAN; -DECLARE - my_reserve_pub BYTEA; -BEGIN - --- Store the deposit request. -INSERT INTO exchange.purse_deposits - (partner_serial_id - ,purse_pub - ,coin_pub - ,amount_with_fee_val - ,amount_with_fee_frac - ,coin_sig) - VALUES - (in_partner_id - ,in_purse_pub - ,in_coin_pub - ,in_amount_with_fee_val - ,in_amount_with_fee_frac - ,in_coin_sig) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Idempotency check: check if coin_sig is the same, - -- if so, success, otherwise conflict! - PERFORM - FROM exchange.purse_deposits - WHERE coin_pub = in_coin_pub - AND purse_pub = in_purse_pub - AND coin_sig = in_cion_sig; - IF NOT FOUND - THEN - -- Deposit exists, but with differences. Not allowed. - out_balance_ok=FALSE; - out_conflict=TRUE; - RETURN; - END IF; -END IF; - - --- Debit the coin --- Check and update balance of the coin. -UPDATE known_coins - SET - remaining_frac=remaining_frac-in_amount_with_fee_frac - + CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val-in_amount_with_fee_val - - CASE - WHEN remaining_frac < in_amount_with_fee_frac - THEN 1 - ELSE 0 - END - WHERE coin_pub=in_coin_pub - AND ( (remaining_val > in_amount_with_fee_val) OR - ( (remaining_frac >= in_amount_with_fee_frac) AND - (remaining_val >= in_amount_with_fee_val) ) ); - -IF NOT FOUND -THEN - -- Insufficient balance. - out_balance_ok=FALSE; - out_conflict=FALSE; - RETURN; -END IF; - - --- Credit the purse. -UPDATE purse_requests - SET - balance_frac=balance_frac+in_amount_without_fee_frac - - CASE - WHEN balance_frac+in_amount_without_fee_frac >= 100000000 - THEN 100000000 - ELSE 0 - END, - balance_val=balance_val+in_amount_without_fee_val - + CASE - WHEN 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 exchange.purse_merges - WHERE purse_pub=in_purse_pub; - -IF NOT FOUND -THEN - RETURN; -END IF; - -SELECT - amount_with_fee_val - ,amount_with_fee_frac - ,in_reserve_quota - INTO - my_amount_val - ,my_amount_frac - ,my_in_reserve_quota - FROM exchange.purse_requests - WHERE (purse_pub=in_purse_pub) - AND ( ( ( (amount_with_fee_val <= balance_val) - AND (amount_with_fee_frac <= balance_frac) ) - OR (amount_with_fee_val < balance_val) ) ); -IF NOT FOUND -THEN - RETURN; -END IF; - --- 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 - out_conflict=TRUE; - RETURN; -END IF; - -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; - - -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_frac - ,current_balance_val - ,expiration_date - ,gc_date) - VALUES - (my_reserve_pub - ,my_amount_frac - ,my_amount_val - ,in_reserve_expiration - ,in_reserve_expiration) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - - 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 $$; - - - - -CREATE OR REPLACE FUNCTION exchange_do_purse_merge( - IN in_purse_pub BYTEA, - IN in_merge_sig BYTEA, - IN in_merge_timestamp INT8, - IN in_reserve_sig BYTEA, - IN in_partner_url VARCHAR, - IN in_reserve_pub BYTEA, - IN in_wallet_h_payto BYTEA, - IN in_expiration_date INT8, - OUT out_no_partner BOOLEAN, - OUT out_no_balance BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - my_amount_val INT8; -DECLARE - my_amount_frac INT4; -DECLARE - my_purse_fee_val INT8; -DECLARE - my_purse_fee_frac INT4; -DECLARE - my_partner_serial_id INT8; -DECLARE - my_in_reserve_quota BOOLEAN; -BEGIN - -IF in_partner_url IS NULL -THEN - my_partner_serial_id=NULL; -ELSE - SELECT - partner_serial_id - INTO - my_partner_serial_id - FROM exchange.partners - WHERE partner_base_url=in_partner_url - AND start_date <= in_merge_timestamp - AND end_date > 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_val - ,amount_with_fee_frac - ,purse_fee_val - ,purse_fee_frac - ,in_reserve_quota - INTO my_amount_val - ,my_amount_frac - ,my_purse_fee_val - ,my_purse_fee_frac - ,my_in_reserve_quota - FROM exchange.purse_requests - WHERE purse_pub=in_purse_pub - AND balance_val >= amount_with_fee_val - AND ( (balance_frac >= amount_with_fee_frac) OR - (balance_val > amount_with_fee_val) ); -IF NOT FOUND -THEN - out_no_balance=TRUE; - out_conflict=FALSE; - RETURN; -END IF; -out_no_balance=FALSE; - --- Store purse merge signature, checks for purse_pub uniqueness -INSERT INTO exchange.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 exchange.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; -out_conflict=FALSE; - - --- Initialize reserve, if not yet exists. -INSERT INTO reserves - (reserve_pub - ,expiration_date - ,gc_date) - VALUES - (in_reserve_pub - ,in_expiration_date - ,in_expiration_date) - ON CONFLICT DO NOTHING; - --- Remember how this purse was finished. -INSERT INTO purse_decision - (purse_pub - ,action_timestamp - ,refunded) -VALUES - (in_purse_pub - ,in_merge_timestamp - ,FALSE); - -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; - --- Store account merge signature. -INSERT INTO exchange.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; - - UPDATE exchange.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 - WHERE reserve_pub=in_reserve_pub; - -END IF; - -RETURN; - -END $$; - -COMMENT ON FUNCTION exchange_do_purse_merge(BYTEA, BYTEA, INT8, BYTEA, VARCHAR, 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.'; - - -CREATE OR REPLACE FUNCTION exchange_do_reserve_purse( - IN in_purse_pub BYTEA, - IN in_merge_sig BYTEA, - IN in_merge_timestamp INT8, - IN in_reserve_sig BYTEA, - IN in_reserve_quota BOOLEAN, - IN in_purse_fee_val INT8, - IN in_purse_fee_frac INT4, - IN in_reserve_pub BYTEA, - IN in_wallet_h_payto BYTEA, - OUT out_no_funds BOOLEAN, - OUT out_no_reserve BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -BEGIN - --- Store purse merge signature, checks for purse_pub uniqueness -INSERT INTO exchange.purse_merges - (partner_serial_id - ,reserve_pub - ,purse_pub - ,merge_sig - ,merge_timestamp) - VALUES - (NULL - ,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 exchange.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; - out_no_reserve=FALSE; - out_no_funds=FALSE; - RETURN; - END IF; - - -- "success" - out_conflict=FALSE; - out_no_funds=FALSE; - out_no_reserve=FALSE; - RETURN; -END IF; -out_conflict=FALSE; - -PERFORM - FROM exchange.reserves - WHERE reserve_pub=in_reserve_pub; - -out_no_reserve = NOT FOUND; - -IF (in_reserve_quota) -THEN - -- Increment active purses per reserve (and check this is allowed) - IF (out_no_reserve) - THEN - out_no_funds=TRUE; - RETURN; - END IF; - UPDATE exchange.reserves - SET purses_active=purses_active+1 - WHERE reserve_pub=in_reserve_pub - AND purses_active < purses_allowed; - IF NOT FOUND - THEN - out_no_funds=TRUE; - RETURN; - END IF; -ELSE - -- UPDATE reserves balance (and check if balance is enough to pay the fee) - IF (out_no_reserve) - THEN - IF ( (0 != in_purse_fee_val) OR - (0 != in_purse_fee_frac) ) - THEN - out_no_funds=TRUE; - RETURN; - END IF; - ELSE - UPDATE exchange.reserves - SET - current_balance_frac=current_balance_frac-in_purse_fee_frac - + CASE - WHEN current_balance_frac < in_purse_fee_frac - THEN 100000000 - ELSE 0 - END, - current_balance_val=current_balance_val-in_purse_fee_val - - CASE - WHEN current_balance_frac < in_purse_fee_frac - THEN 1 - ELSE 0 - END - WHERE reserve_pub=in_reserve_pub - AND ( (current_balance_val > 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 exchange.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, BYTEA, BOOLEAN, INT8, INT4, BYTEA, BYTEA) - IS 'Create a purse for a reserve.'; - - - - - - -CREATE OR REPLACE FUNCTION exchange_do_account_merge( - IN in_purse_pub BYTEA, - IN in_reserve_pub BYTEA, - IN in_reserve_sig BYTEA, - OUT out_balance_ok BOOLEAN, - OUT out_conflict BOOLEAN) -LANGUAGE plpgsql -AS $$ -BEGIN - -- FIXME: function/API is dead! Do DCE? -END $$; - - - -CREATE OR REPLACE FUNCTION exchange_do_expire_purse( - IN in_start_time INT8, - IN in_end_time INT8, - IN in_now INT8, - OUT out_found BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - my_purse_pub BYTEA; -DECLARE - my_deposit record; -DECLARE - my_in_reserve_quota BOOLEAN; -BEGIN - --- FIXME: we should probably do this in a loop --- and expire all at once, instead of one per query -SELECT purse_pub - ,in_reserve_quota - INTO my_purse_pub - ,my_in_reserve_quota - FROM exchange.purse_requests - WHERE (purse_expiration >= in_start_time) AND - (purse_expiration < in_end_time) AND - purse_pub NOT IN (SELECT purse_pub - FROM purse_decision) - 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); - -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_val - ,amount_with_fee_frac - FROM exchange.purse_deposits - WHERE purse_pub = my_purse_pub -LOOP - UPDATE exchange.known_coins SET - remaining_frac=remaining_frac+my_deposit.amount_with_fee_frac - - CASE - WHEN remaining_frac+my_deposit.amount_with_fee_frac >= 100000000 - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val+my_deposit.amount_with_fee_val - + CASE - WHEN 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).'; - - - - -CREATE OR REPLACE FUNCTION exchange_do_history_request( - IN in_reserve_pub BYTEA, - IN in_reserve_sig BYTEA, - IN in_request_timestamp INT8, - IN in_history_fee_val INT8, - IN in_history_fee_frac INT4, - OUT out_balance_ok BOOLEAN, - OUT out_idempotent BOOLEAN) -LANGUAGE plpgsql -AS $$ -BEGIN - - -- Insert and check for idempotency. - INSERT INTO exchange.history_requests - (reserve_pub - ,request_timestamp - ,reserve_sig - ,history_fee_val - ,history_fee_frac) - VALUES - (in_reserve_pub - ,in_request_timestamp - ,in_reserve_sig - ,in_history_fee_val - ,in_history_fee_frac) - ON CONFLICT DO NOTHING; - - IF NOT FOUND - THEN - out_balance_ok=TRUE; - out_idempotent=TRUE; - RETURN; - END IF; - - out_idempotent=FALSE; - - -- Update reserve balance. - UPDATE exchange.reserves - SET - current_balance_frac=current_balance_frac-in_history_fee_frac - + CASE - WHEN current_balance_frac < in_history_fee_frac - THEN 100000000 - ELSE 0 - END, - current_balance_val=current_balance_val-in_history_fee_val - - CASE - WHEN current_balance_frac < in_history_fee_frac - THEN 1 - ELSE 0 - END - WHERE - reserve_pub=in_reserve_pub - AND ( (current_balance_val > in_history_fee_val) OR - ( (current_balance_frac >= in_history_fee_frac) AND - (current_balance_val >= in_history_fee_val) ) ); - - IF NOT FOUND - THEN - -- Either reserve does not exist, or balance insufficient. - -- Both we treat the same here as balance insufficient. - out_balance_ok=FALSE; - RETURN; - END IF; - - out_balance_ok=TRUE; -END $$; - - -CREATE OR REPLACE FUNCTION exchange_do_reserve_open_deposit( - IN in_coin_pub BYTEA, - IN in_known_coin_id INT8, - IN in_coin_sig BYTEA, - IN in_reserve_sig BYTEA, - IN in_reserve_pub BYTEA, - IN in_coin_total_val INT8, - IN in_coin_total_frac INT4, - OUT out_insufficient_funds BOOLEAN) -LANGUAGE plpgsql -AS $$ -BEGIN - -INSERT INTO exchange.reserves_open_deposits - (reserve_sig - ,reserve_pub - ,coin_pub - ,coin_sig - ,contribution_val - ,contribution_frac - ) - VALUES - (in_reserve_sig - ,in_reserve_pub - ,in_coin_pub - ,in_coin_sig - ,in_coin_total_val - ,in_coin_total_frac) - ON CONFLICT DO NOTHING; - -IF NOT FOUND -THEN - -- Idempotent request known, return success. - out_insufficient_funds=FALSE; - RETURN; -END IF; - - --- Check and update balance of the coin. -UPDATE exchange.known_coins - SET - remaining_frac=remaining_frac-in_coin_total_frac - + CASE - WHEN remaining_frac < in_coin_total_frac - THEN 100000000 - ELSE 0 - END, - remaining_val=remaining_val-in_coin_total_val - - CASE - WHEN remaining_frac < in_coin_total_frac - THEN 1 - ELSE 0 - END - WHERE coin_pub=in_coin_pub - AND ( (remaining_val > in_coin_total_val) OR - ( (remaining_frac >= in_coin_total_frac) AND - (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 $$; - - -CREATE OR REPLACE FUNCTION exchange_do_reserve_open( - IN in_reserve_pub BYTEA, - IN in_total_paid_val INT8, - IN in_total_paid_frac INT4, - IN in_reserve_payment_val INT8, - IN in_reserve_payment_frac INT4, - IN in_min_purse_limit INT4, - IN in_default_purse_limit INT4, - IN in_reserve_sig BYTEA, - IN in_desired_expiration INT8, - IN in_reserve_gc_delay INT8, - IN in_now INT8, - IN in_open_fee_val INT8, - IN in_open_fee_frac INT4, - OUT out_open_cost_val INT8, - OUT out_open_cost_frac INT4, - OUT out_final_expiration INT8, - OUT out_no_funds BOOLEAN) -LANGUAGE plpgsql -AS $$ -DECLARE - my_balance_val INT8; -DECLARE - my_balance_frac INT4; -DECLARE - my_cost_val INT8; -DECLARE - my_cost_tmp INT8; -DECLARE - my_cost_frac INT4; -DECLARE - my_years_tmp INT4; -DECLARE - my_years INT4; -DECLARE - my_needs_update BOOL; -DECLARE - my_purses_allowed INT8; -DECLARE - my_expiration_date INT8; -DECLARE - my_reserve_expiration INT8; -BEGIN - --- FIXME: use SELECT FOR UPDATE? -SELECT - purses_allowed - ,expiration_date - ,current_balance_val - ,current_balance_frac -INTO - my_purses_allowed - ,my_reserve_expiration - ,my_balance_val - ,my_balance_frac -FROM reserves -WHERE - reserve_pub=in_reserve_pub; - -IF NOT FOUND -THEN - -- FIXME: do we need to set a 'not found'? - RAISE NOTICE 'reserve not found'; - RETURN; -END IF; - --- Do not allow expiration time to start in the past already -IF (my_reserve_expiration < in_now) -THEN - my_expiration_date = in_now; -ELSE - my_expiration_date = my_reserve_expiration; -END IF; - -my_cost_val = 0; -my_cost_frac = 0; -my_needs_update = FALSE; -my_years = 0; - --- Compute years based on desired expiration time -IF (my_expiration_date < in_desired_expiration) -THEN - my_years = (31535999999999 + in_desired_expiration - my_expiration_date) / 31536000000000; - my_purses_allowed = in_default_purse_limit; - my_expiration_date = my_expiration_date + 31536000000000 * my_years; -END IF; - --- Increase years based on purses requested -IF (my_purses_allowed < in_min_purse_limit) -THEN - my_years = (31535999999999 + in_desired_expiration - in_now) / 31536000000000; - my_expiration_date = in_now + 31536000000000 * my_years; - my_years_tmp = (in_min_purse_limit + in_default_purse_limit - my_purses_allowed - 1) / in_default_purse_limit; - my_years = my_years + my_years_tmp; - my_purses_allowed = my_purses_allowed + (in_default_purse_limit * my_years_tmp); -END IF; - - --- Compute cost based on annual fees -IF (my_years > 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 = my_reserve_expiration; - 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 (my_reserve_expiration >= 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 = my_reserve_expiration; - END IF; - RAISE NOTICE 'amount paid too low'; - RETURN; -END IF; - --- Check reserve balance is sufficient. -IF (my_balance_val > in_reserve_payment_val) -THEN - IF (my_balance_frac >= in_reserve_payment_frac) - THEN - my_balance_val=my_balance_val - in_reserve_payment_val; - my_balance_frac=my_balance_frac - in_reserve_payment_frac; - ELSE - my_balance_val=my_balance_val - in_reserve_payment_val - 1; - my_balance_frac=my_balance_frac + 100000000 - in_reserve_payment_frac; - END IF; -ELSE - IF (my_balance_val = in_reserve_payment_val) AND (my_balance_frac >= in_reserve_payment_frac) - THEN - my_balance_val=0; - my_balance_frac=my_balance_frac - in_reserve_payment_frac; - ELSE - out_final_expiration = my_reserve_expiration; - 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_val=my_balance_val - ,current_balance_frac=my_balance_frac - ,gc_date=my_reserve_expiration + in_reserve_gc_delay - ,expiration_date=my_expiration_date - ,purses_allowed=my_purses_allowed -WHERE - reserve_pub=in_reserve_pub; - -out_final_expiration=my_expiration_date; -out_open_cost_val = my_cost_val; -out_open_cost_frac = my_cost_frac; -out_no_funds=FALSE; -RETURN; - -END $$; - -CREATE OR REPLACE FUNCTION insert_or_update_policy_details( - IN in_policy_hash_code BYTEA, - IN in_policy_json VARCHAR, - IN in_deadline INT8, - IN in_commitment_val INT8, - IN in_commitment_frac INT4, - IN in_accumulated_total_val INT8, - IN in_accumulated_total_frac INT4, - IN in_fee_val INT8, - IN in_fee_frac INT4, - IN in_transferable_val INT8, - IN in_transferable_frac INT4, - IN in_fulfillment_state SMALLINT, - OUT out_policy_details_serial_id INT8, - OUT out_accumulated_total_val INT8, - OUT out_accumulated_total_frac INT4, - OUT out_fulfillment_state SMALLINT) -LANGUAGE plpgsql -AS $$ -DECLARE - cur_commitment_val INT8; - cur_commitment_frac INT4; - cur_accumulated_total_val INT8; - cur_accumulated_total_frac INT4; -BEGIN - -- First, try to create a new entry. - INSERT INTO policy_details - (policy_hash_code, - policy_json, - deadline, - commitment_val, - commitment_frac, - accumulated_total_val, - accumulated_total_frac, - fee_val, - fee_frac, - transferable_val, - transferable_frac, - fulfillment_state) - VALUES (in_policy_hash_code, - in_policy_json, - in_deadline, - in_commitment_val, - in_commitment_frac, - in_accumulated_total_val, - in_accumulated_total_frac, - in_fee_val, - in_fee_frac, - in_transferable_val, - in_transferable_frac, - in_fulfillment_state) - ON CONFLICT (policy_hash_code) DO NOTHING - RETURNING policy_details_serial_id INTO out_policy_details_serial_id; - - -- If the insert was successful, return - -- We assume that the fullfilment_state was correct in first place. - IF FOUND THEN - out_accumulated_total_val = in_accumulated_total_val; - out_accumulated_total_frac = in_accumulated_total_frac; - out_fulfillment_state = in_fulfillment_state; - RETURN; - END IF; - - -- We had a conflict, grab the parts we need to update. - SELECT policy_details_serial_id, - commitment_val, - commitment_frac, - accumulated_total_val, - accumulated_total_frac - INTO out_policy_details_serial_id, - cur_commitment_val, - cur_commitment_frac, - cur_accumulated_total_val, - cur_accumulated_total_frac - FROM policy_details - WHERE policy_hash_code = in_policy_hash_code; - - -- calculate the new values (overflows throws exception) - out_accumulated_total_val = cur_accumulated_total_val + in_accumulated_total_val; - out_accumulated_total_frac = cur_accumulated_total_frac + in_accumulated_total_frac; - -- normalize - out_accumulated_total_val = out_accumulated_total_val + out_accumulated_total_frac / 100000000; - out_accumulated_total_frac = out_accumulated_total_frac % 100000000; - - IF (out_accumulated_total_val > (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: What to do in case of Failure or other state? - IF (out_fullfillment_state = 1) -- 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 = 2; -- READY - END IF; - END IF; - - -- Now, update the record - UPDATE exchange.policy_details - SET - accumulated_val = out_accumulated_total_val, - accumulated_frac = out_accumulated_total_frac, - fulfillment_state = out_fulfillment_state - WHERE - policy_details_serial_id = out_policy_details_serial_id; -END $$; - -CREATE OR REPLACE FUNCTION batch_reserves_in( - IN in_reserve_pub BYTEA, - IN in_expiration_date INT8, - IN in_gc_date INT8, - IN in_wire_ref INT8, - IN in_credit_val INT8, - IN in_credit_frac INT4, - IN in_exchange_account_name VARCHAR, - IN in_exectution_date INT8, - IN in_wire_source_h_payto BYTEA, ---h_payto - IN in_payto_uri VARCHAR, - IN in_reserve_expiration INT8, - OUT out_reserve_found BOOLEAN, - OUT transaction_duplicate BOOLEAN, - OUT ruuid INT8) -LANGUAGE plpgsql -AS $$ -DECLARE - my_amount_val INT8; -DECLARE - my_amount_frac INT4; -BEGIN - - INSERT INTO reserves - (reserve_pub - ,current_balance_val - ,current_balance_frac - ,expiration_date - ,gc_date) - VALUES - (in_reserve_pub - ,in_credit_val - ,in_credit_frac - ,in_expiration_date - ,in_gc_date) - ON CONFLICT DO NOTHING - RETURNING reserve_uuid INTO ruuid; - - IF FOUND - THEN - -- We made a change, so the reserve did not previously exist. - out_reserve_found = FALSE; - ELSE - -- We made no change, which means the reserve existed. - out_reserve_found = TRUE; - END IF; - - --SIMPLE INSERT ON CONFLICT DO NOTHING - INSERT INTO wire_targets - (wire_target_h_payto - ,payto_uri) - VALUES - (in_wire_source_h_payto - ,in_payto_uri) - ON CONFLICT DO NOTHING; - - INSERT INTO reserves_in - (reserve_pub - ,wire_reference - ,credit_val - ,credit_frac - ,exchange_account_section - ,wire_source_h_payto - ,execution_date) - VALUES - (in_reserve_pub - ,in_wire_ref - ,in_credit_val - ,in_credit_frac - ,in_exchange_account_name - ,in_wire_source_h_payto - ,in_expiration_date); - - --IF THE INSERTION WAS A SUCCESS IT MEANS NO DUPLICATED TRANSACTION - IF FOUND - THEN - transaction_duplicate = FALSE; - IF out_reserve_found - THEN - UPDATE reserves - SET - current_balance_frac = current_balance_frac+in_credit_frac - - CASE - WHEN current_balance_frac + in_credit_frac >= 100000000 - THEN 100000000 - ELSE 1 - END - ,current_balance_val = current_balance_val+in_credit_val - + CASE - WHEN 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 reserves.reserve_pub=in_reserve_pub; - out_reserve_found = TRUE; - RETURN; - ELSE - out_reserve_found=FALSE; - RETURN; - END IF; - out_reserve_found = TRUE; - ELSE - transaction_duplicate = TRUE; - IF out_reserve_found - THEN - out_reserve_found = TRUE; - RETURN; - ELSE - out_reserve_found = FALSE; - RETURN; - END IF; - END IF; -END $$; - -COMMIT; diff --git a/src/exchangedb/procedures.sql.in b/src/exchangedb/procedures.sql.in new file mode 100644 index 000000000..b412b66d7 --- /dev/null +++ b/src/exchangedb/procedures.sql.in @@ -0,0 +1,41 @@ +-- +-- 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; + +SET search_path TO exchange; + +#include "exchange_do_withdraw.sql" +#include "exchange_do_batch_withdraw.sql" +#include "exchange_do_batch_withdraw_insert.sql" +#include "exchange_do_recoup_by_reserve.sql" +#include "exchange_do_deposit.sql" +#include "exchange_do_melt.sql" +#include "exchange_do_refund.sql" +#include "exchange_do_recoup_to_reserve.sql" +#include "exchange_do_recoup_to_coin.sql" +#include "exchange_do_gc.sql" +#include "exchange_do_purse_deposit.sql" +#include "exchange_do_reserve_purse.sql" +#include "exchange_do_expire_purse.sql" +#include "exchange_do_history_request.sql" +#include "exchange_do_reserve_open_deposit.sql" +#include "exchange_do_reserve_open.sql" +#include "exchange_do_insert_or_update_policy_details.sql" +#include "exchange_do_batch_reserves_in.sql" + + +COMMIT;