aboutsummaryrefslogtreecommitdiff
path: root/src/exchangedb/procedures.sql
diff options
context:
space:
mode:
Diffstat (limited to 'src/exchangedb/procedures.sql')
-rw-r--r--src/exchangedb/procedures.sql2281
1 files changed, 2281 insertions, 0 deletions
diff --git a/src/exchangedb/procedures.sql b/src/exchangedb/procedures.sql
new file mode 100644
index 00000000..e2e7c75c
--- /dev/null
+++ b/src/exchangedb/procedures.sql
@@ -0,0 +1,2281 @@
+--
+-- 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 <http://www.gnu.org/licenses/>
+--
+
+-- Everything in one big transaction
+BEGIN;
+
+---------------------------------------------------------------------------
+-- Stored procedures
+---------------------------------------------------------------------------
+
+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 kycok BOOLEAN,
+ OUT account_uuid INT8,
+ 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 denominations
+ WHERE denom_pub_hash=h_denom_pub;
+
+IF NOT FOUND
+THEN
+ -- denomination unknown, should be impossible!
+ reserve_found=FALSE;
+ balance_ok=FALSE;
+ kycok=FALSE;
+ account_uuid=0;
+ 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 reserves
+ WHERE reserves.reserve_pub=rpub;
+
+IF NOT FOUND
+THEN
+ -- reserve unknown
+ reserve_found=FALSE;
+ balance_ok=FALSE;
+ nonce_ok=TRUE;
+ kycok=FALSE;
+ account_uuid=0;
+ ruuid=2;
+ RETURN;
+END IF;
+
+-- We optimistically insert, and then on conflict declare
+-- the query successful due to idempotency.
+INSERT INTO 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;
+ kycok=TRUE;
+ account_uuid=0;
+ 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;
+ kycok=FALSE; -- we do not really know or care
+ account_uuid=0;
+ 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 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 cs_nonce_locks
+ WHERE nonce=cs_nonce
+ AND op_hash=h_coin_envelope;
+ IF NOT FOUND
+ THEN
+ reserve_found=FALSE;
+ balance_ok=FALSE;
+ kycok=FALSE;
+ account_uuid=0;
+ nonce_ok=FALSE;
+ RETURN;
+ END IF;
+ END IF;
+ELSE
+ nonce_ok=TRUE; -- no nonce, hence OK!
+END IF;
+
+
+
+-- Obtain KYC status based on the last wire transfer into
+-- this reserve. FIXME: likely not adequate for reserves that got P2P transfers!
+-- SELECT
+-- kyc_ok
+-- ,wire_target_serial_id
+-- INTO
+-- kycok
+-- ,account_uuid
+-- FROM reserves_in
+-- JOIN wire_targets ON (wire_source_h_payto = wire_target_h_payto)
+-- WHERE reserve_pub=rpub
+-- LIMIT 1; -- limit 1 should not be required (without p2p transfers)
+
+WITH reserves_in AS materialized (
+ SELECT wire_source_h_payto
+ FROM reserves_in WHERE
+ reserve_pub=rpub
+)
+SELECT
+ kyc_ok
+ ,wire_target_serial_id
+INTO
+ kycok
+ ,account_uuid
+FROM wire_targets
+ WHERE wire_target_h_payto = (
+ SELECT wire_source_h_payto
+ FROM reserves_in
+ );
+
+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 kycok BOOLEAN,
+ OUT account_uuid INT8,
+ 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 reserves
+ WHERE reserves.reserve_pub=rpub;
+
+IF NOT FOUND
+THEN
+ -- reserve unknown
+ reserve_found=FALSE;
+ balance_ok=FALSE;
+ kycok=FALSE;
+ account_uuid=0;
+ 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;
+ kycok=FALSE; -- we do not really know or care
+ account_uuid=0;
+ 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;
+
+
+-- Obtain KYC status based on the last wire transfer into
+-- this reserve. FIXME: likely not adequate for reserves that got P2P transfers!
+-- SELECT
+-- kyc_ok
+-- ,wire_target_serial_id
+-- INTO
+-- kycok
+-- ,account_uuid
+-- FROM reserves_in
+-- JOIN wire_targets ON (wire_source_h_payto = wire_target_h_payto)
+-- WHERE reserve_pub=rpub
+-- LIMIT 1; -- limit 1 should not be required (without p2p transfers)
+
+WITH reserves_in AS materialized (
+ SELECT wire_source_h_payto
+ FROM reserves_in WHERE
+ reserve_pub=rpub
+)
+SELECT
+ kyc_ok
+ ,wire_target_serial_id
+INTO
+ kycok
+ ,account_uuid
+FROM wire_targets
+ WHERE wire_target_h_payto = (
+ SELECT wire_source_h_payto
+ FROM reserves_in
+ );
+
+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 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 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 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 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';
+
+
+
+
+CREATE OR REPLACE FUNCTION exchange_do_withdraw_limit_check(
+ IN ruuid INT8,
+ IN start_time INT8,
+ IN upper_limit_val INT8,
+ IN upper_limit_frac INT4,
+ OUT below_limit BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ total_val INT8;
+DECLARE
+ total_frac INT8; -- INT4 could overflow during accumulation!
+BEGIN
+-- NOTE: Read-only, but crosses shards.
+-- Shards: reserves by reserve_pub
+-- reserves_out by reserve_uuid -- crosses shards!!
+
+
+SELECT
+ SUM(amount_with_fee_val) -- overflow here is not plausible
+ ,SUM(CAST(amount_with_fee_frac AS INT8)) -- compute using 64 bits
+ INTO
+ total_val
+ ,total_frac
+ FROM reserves_out
+ WHERE reserve_uuid=ruuid
+ AND execution_date > start_time;
+
+-- normalize result
+total_val = total_val + total_frac / 100000000;
+total_frac = total_frac % 100000000;
+
+-- compare to threshold
+below_limit = (total_val < upper_limit_val) OR
+ ( (total_val = upper_limit_val) AND
+ (total_frac <= upper_limit_frac) );
+END $$;
+
+COMMENT ON FUNCTION exchange_do_withdraw_limit_check(INT8, INT8, INT8, INT4)
+ IS 'Check whether the withdrawals from the given reserve since the given time are below the given threshold';
+
+
+-- 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 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 reserves
+ WHERE reserves.reserve_pub = res_pub;
+
+ FOR blind_ev IN
+ SELECT h_blind_ev
+ FROM reserves_out_by_reserve
+ WHERE reserves_out_by_reserve.reserve_uuid = res_uuid
+ LOOP
+ SELECT robr.coin_pub
+ INTO c_pub
+ FROM recoup_by_reserve robr
+ WHERE robr.reserve_out_serial_id = (
+ SELECT reserves_out.reserve_out_serial_id
+ FROM 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 known_coins
+ WHERE known_coins.coin_pub = c_pub
+ ) kc
+ JOIN (
+ SELECT *
+ FROM 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_extension_blocked BOOLEAN,
+ IN in_extension_details VARCHAR,
+ OUT out_exchange_timestamp INT8,
+ OUT out_balance_ok BOOLEAN,
+ OUT out_conflict BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ wtsi INT8; -- wire target serial id
+DECLARE
+ xdi INT8; -- eXstension details serial id
+BEGIN
+-- Shards: INSERT extension_details (by extension_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)
+
+IF NOT NULL in_extension_details
+THEN
+ INSERT INTO extension_details
+ (extension_options)
+ VALUES
+ (in_extension_details)
+ RETURNING extension_details_serial_id INTO xdi;
+ELSE
+ xdi=NULL;
+END IF;
+
+
+INSERT INTO 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 wire_targets
+ WHERE wire_target_h_payto=in_h_payto;
+END IF;
+
+
+INSERT INTO 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
+ ,extension_blocked
+ ,extension_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_extension_blocked
+ ,xdi)
+ 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 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;
+
+ 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 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 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 recoup_refresh
+ WHERE rrc_serial IN
+ (SELECT rrc_serial
+ FROM refresh_revealed_coins
+ WHERE melt_serial_id IN
+ (SELECT melt_serial_id
+ FROM 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 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 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 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 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 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 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 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 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 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 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 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 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 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 prewire
+ WHERE finished=TRUE;
+
+DELETE FROM wire_fee
+ WHERE end_date < in_ancient_date;
+
+-- TODO: use closing fee as threshold?
+DELETE FROM 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 reserves_out
+ ORDER BY reserve_out_serial_id ASC
+ LIMIT 1;
+
+DELETE FROM recoup
+ WHERE reserve_out_serial_id < reserve_out_min;
+-- FIXME: recoup_refresh lacks GC!
+
+SELECT
+ reserve_uuid
+ INTO
+ reserve_uuid_min
+ FROM reserves
+ ORDER BY reserve_uuid ASC
+ LIMIT 1;
+
+DELETE FROM 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 denominations
+ WHERE expire_legal < in_now
+ AND denominations_serial NOT IN
+ (SELECT DISTINCT denominations_serial
+ FROM reserves_out)
+ AND denominations_serial NOT IN
+ (SELECT DISTINCT denominations_serial
+ FROM known_coins
+ WHERE coin_pub IN
+ (SELECT DISTINCT coin_pub
+ FROM recoup))
+ AND denominations_serial NOT IN
+ (SELECT DISTINCT denominations_serial
+ FROM known_coins
+ WHERE coin_pub IN
+ (SELECT DISTINCT coin_pub
+ FROM recoup_refresh));
+
+SELECT
+ melt_serial_id
+ INTO
+ melt_min
+ FROM refresh_commitments
+ ORDER BY melt_serial_id ASC
+ LIMIT 1;
+
+DELETE FROM refresh_revealed_coins
+ WHERE melt_serial_id < melt_min;
+
+DELETE FROM refresh_transfer_keys
+ WHERE melt_serial_id < melt_min;
+
+SELECT
+ known_coin_id
+ INTO
+ coin_min
+ FROM known_coins
+ ORDER BY known_coin_id ASC
+ LIMIT 1;
+
+DELETE FROM deposits
+ WHERE known_coin_id < coin_min;
+
+SELECT
+ deposit_serial_id
+ INTO
+ deposit_min
+ FROM deposits
+ ORDER BY deposit_serial_id ASC
+ LIMIT 1;
+
+DELETE FROM refunds
+ WHERE deposit_serial_id < deposit_min;
+
+DELETE FROM aggregation_tracking
+ WHERE deposit_serial_id < deposit_min;
+
+SELECT
+ denominations_serial
+ INTO
+ denom_min
+ FROM denominations
+ ORDER BY denominations_serial ASC
+ LIMIT 1;
+
+DELETE FROM 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,
+ 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_reserve_pub BYTEA;
+BEGIN
+
+-- Store the deposit request.
+INSERT INTO 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 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 partner_serial_id
+ ,reserve_pub
+ INTO psi
+ ,my_reserve_pub
+ FROM purse_merges
+ WHERE purse_pub=in_purse_pub;
+
+IF NOT FOUND
+THEN
+ RETURN;
+END IF;
+
+SELECT
+ amount_with_fee_val
+ ,amount_with_fee_frac
+ INTO
+ my_amount_val
+ ,my_amount_frac
+ FROM 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;
+
+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.
+ 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
+ WHERE reserve_pub=my_reserve_pub;
+
+ -- ... and mark purse as finished.
+ -- FIXME: combine with UPDATE above?
+ UPDATE purse_requests
+ SET finished=true
+ WHERE purse_pub=in_purse_pub;
+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_require_kyc BOOLEAN,
+ OUT out_no_partner BOOLEAN,
+ OUT out_no_balance BOOLEAN,
+ OUT out_no_kyc BOOLEAN,
+ OUT out_no_reserve 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_finished BOOLEAN;
+BEGIN
+
+IF in_partner_url IS NULL
+THEN
+ my_partner_serial_id=0;
+ELSE
+ SELECT
+ partner_serial_id
+ INTO
+ my_partner_serial_id
+ FROM 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;
+ out_no_kyc=FALSE;
+ out_no_reserve=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
+ ,finished
+ INTO my_amount_val
+ ,my_amount_frac
+ ,my_purse_fee_val
+ ,my_purse_fee_frac
+ ,my_finished
+ FROM 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;
+ out_no_kyc=FALSE;
+ out_no_reserve=FALSE;
+ RETURN;
+END IF;
+out_no_balance=FALSE;
+
+-- Store purse merge signature, checks for purse_pub uniqueness
+INSERT INTO purse_merges
+ (partner_serial_id
+ ,reserve_pub
+ ,purse_pub
+ ,merge_sig
+ ,merge_timestamp)
+ VALUES
+ (my_partner_serial_id
+ ,in_reserve_pub
+ ,in_purse_pub
+ ,in_merge_sig
+ ,in_merge_timestamp)
+ ON CONFLICT DO NOTHING;
+
+IF NOT FOUND
+THEN
+ -- Idempotency check: see if an identical record exists.
+ -- Note that by checking 'merge_sig', we implicitly check
+ -- identity over everything that the signature covers.
+ PERFORM
+ FROM purse_merges
+ WHERE purse_pub=in_purse_pub
+ AND merge_sig=in_merge_sig;
+ IF NOT FOUND
+ THEN
+ -- Purse was merged, but to some other reserve. Not allowed.
+ out_conflict=TRUE;
+ out_no_kyc=FALSE;
+ out_no_reserve=FALSE;
+ RETURN;
+ END IF;
+
+ -- "success"
+ out_conflict=FALSE;
+ out_no_kyc=FALSE;
+ out_no_reserve=FALSE;
+ RETURN;
+END IF;
+out_conflict=FALSE;
+
+ASSERT NOT my_finished, 'internal invariant failed';
+
+IF ( (in_partner_url IS NULL) AND
+ (in_require_kyc) )
+THEN
+ -- Need to do KYC check.
+ SELECT NOT kyc_passed
+ INTO out_no_kyc
+ FROM reserves
+ WHERE reserve_pub=in_reserve_pub;
+
+ IF NOT FOUND
+ THEN
+ out_no_kyc=TRUE;
+ out_no_reserve=TRUE;
+ RETURN;
+ END IF;
+ out_no_reserve=FALSE;
+
+ IF (out_no_kyc)
+ THEN
+ RETURN;
+ END IF;
+ELSE
+ -- KYC is not our responsibility
+ out_no_reserve=FALSE;
+ out_no_kyc=FALSE;
+END IF;
+
+
+
+-- Store account merge signature.
+INSERT INTO account_merges
+ (reserve_pub
+ ,reserve_sig
+ ,purse_pub)
+ VALUES
+ (in_reserve_pub
+ ,in_reserve_sig
+ ,in_purse_pub);
+
+-- 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 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;
+
+ -- ... and mark purse as finished.
+ UPDATE purse_requests
+ SET finished=true
+ WHERE purse_pub=in_purse_pub;
+END IF;
+
+
+RETURN;
+
+END $$;
+
+COMMENT ON FUNCTION exchange_do_purse_merge(BYTEA, BYTEA, INT8, BYTEA, VARCHAR, BYTEA, BOOLEAN)
+ 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_require_kyc BOOLEAN,
+ OUT out_no_funds BOOLEAN,
+ OUT out_no_kyc 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 purse_merges
+ (partner_serial_id
+ ,reserve_pub
+ ,purse_pub
+ ,merge_sig
+ ,merge_timestamp)
+ VALUES
+ (0
+ ,in_reserve_pub
+ ,in_purse_pub
+ ,in_merge_sig
+ ,in_merge_timestamp)
+ ON CONFLICT DO NOTHING;
+
+IF NOT FOUND
+THEN
+ -- Idempotency check: see if an identical record exists.
+ -- Note that by checking 'merge_sig', we implicitly check
+ -- identity over everything that the signature covers.
+ PERFORM
+ FROM purse_merges
+ WHERE purse_pub=in_purse_pub
+ AND merge_sig=in_merge_sig;
+ IF NOT FOUND
+ THEN
+ -- Purse was merged, but to some other reserve. Not allowed.
+ out_conflict=TRUE;
+ out_no_kyc=FALSE;
+ out_no_reserve=FALSE;
+ out_no_funds=FALSE;
+ RETURN;
+ END IF;
+
+ -- "success"
+ out_conflict=FALSE;
+ out_no_funds=FALSE;
+ out_no_kyc=FALSE;
+ out_no_reserve=FALSE;
+ RETURN;
+END IF;
+out_conflict=FALSE;
+
+SELECT NOT kyc_passed
+ INTO out_no_kyc
+ FROM reserves
+ WHERE reserve_pub=in_reserve_pub;
+
+IF NOT FOUND
+THEN
+ out_no_kyc=TRUE;
+ out_no_reserve=TRUE;
+ out_no_funds=TRUE;
+ RETURN;
+END IF;
+out_no_reserve=FALSE;
+
+IF (out_no_kyc AND in_require_kyc)
+THEN
+ out_no_funds=FALSE;
+ RETURN;
+END IF;
+
+IF (in_reserve_quota)
+THEN
+ -- Increment active purses per reserve (and check this is allowed)
+ UPDATE reserves
+ SET purses_active=purses_active+1
+ ,kyc_required=TRUE
+ 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)
+ UPDATE 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
+ ,kyc_required=TRUE
+ 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;
+
+out_no_funds=FALSE;
+
+
+-- Store account merge signature.
+INSERT INTO account_merges
+ (reserve_pub
+ ,reserve_sig
+ ,purse_pub)
+ VALUES
+ (in_reserve_pub
+ ,in_reserve_sig
+ ,in_purse_pub);
+
+END $$;
+
+COMMENT ON FUNCTION exchange_do_reserve_purse(BYTEA, BYTEA, INT8, BYTEA, BOOLEAN, INT8, INT4, BYTEA, BOOLEAN)
+ 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,
+ OUT out_found BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+DECLARE
+ my_purse_pub BYTEA;
+DECLARE
+ my_deposit record;
+BEGIN
+
+SELECT purse_pub
+ INTO my_purse_pub
+ FROM purse_requests
+ WHERE (purse_expiration >= in_start_time) AND
+ (purse_expiration < in_end_time) AND
+ (NOT finished) AND
+ (NOT refunded)
+ ORDER BY purse_expiration ASC
+ LIMIT 1;
+out_found = FOUND;
+IF NOT FOUND
+THEN
+ RETURN;
+END IF;
+
+UPDATE purse_requests
+ SET refunded=TRUE,
+ finished=TRUE
+ WHERE purse_pub=my_purse_pub;
+
+INSERT INTO purse_refunds
+ (purse_pub)
+ VALUES
+ (my_purse_pub);
+
+-- 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 purse_deposits
+ WHERE purse_pub = my_purse_pub
+LOOP
+ UPDATE 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)
+ 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 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 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_close_request(
+ IN in_reserve_pub BYTEA,
+ IN in_close_timestamp INT8,
+ IN in_reserve_sig BYTEA,
+ OUT out_final_balance_val INT8,
+ OUT out_final_balance_frac INT4,
+ OUT out_balance_ok BOOLEAN,
+ OUT out_conflict BOOLEAN)
+LANGUAGE plpgsql
+AS $$
+BEGIN
+
+ SELECT
+ current_balance_val
+ ,current_balance_frac
+ INTO
+ out_final_balance_val
+ ,out_final_balance_frac
+ FROM reserves
+ WHERE reserve_pub=in_reserve_pub;
+
+ IF NOT FOUND
+ THEN
+ out_final_balance_val=0;
+ out_final_balance_frac=0;
+ out_balance_ok = FALSE;
+ out_conflict = FALSE;
+ END IF;
+
+ INSERT INTO close_requests
+ (reserve_pub
+ ,close_timestamp
+ ,reserve_sig
+ ,close_val
+ ,close_frac)
+ VALUES
+ (in_reserve_pub
+ ,in_close_timestamp
+ ,in_reserve_sig
+ ,out_final_balance_val
+ ,out_final_balance_frac)
+ ON CONFLICT DO NOTHING;
+ out_conflict = NOT FOUND;
+
+ UPDATE reserves SET
+ current_balance_val=0
+ ,current_balance_frac=0
+ WHERE reserve_pub=in_reserve_pub;
+ out_balance_ok = TRUE;
+
+END $$;
+
+
+COMMIT;