exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

do_reserve_open.sql (9017B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2014--2022 Taler Systems SA
      4 --
      5 -- TALER is free software; you can redistribute it and/or modify it under the
      6 -- terms of the GNU General Public License as published by the Free Software
      7 -- Foundation; either version 3, or (at your option) any later version.
      8 --
      9 -- TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10 -- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11 -- A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 --
     13 -- You should have received a copy of the GNU General Public License along with
     14 -- TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 --
     16 
     17 CREATE OR REPLACE FUNCTION exchange_do_reserve_open(
     18   IN in_reserve_pub BYTEA,
     19   IN in_total_paid taler_amount,
     20   IN in_reserve_payment taler_amount,
     21   IN in_min_purse_limit INT4,
     22   IN in_default_purse_limit INT4,
     23   IN in_reserve_sig BYTEA,
     24   IN in_desired_expiration INT8,
     25   IN in_reserve_gc_delay INT8,
     26   IN in_now INT8,
     27   IN in_open_fee taler_amount,
     28   OUT out_open_cost taler_amount,
     29   OUT out_final_expiration INT8,
     30   OUT out_no_reserve BOOLEAN,
     31   OUT out_no_funds BOOLEAN,
     32   OUT out_reserve_balance taler_amount)
     33 LANGUAGE plpgsql
     34 AS $$
     35 DECLARE
     36   my_balance taler_amount;
     37   my_cost taler_amount;
     38   my_cost_tmp INT8;
     39   my_years_tmp INT8;
     40   my_years INT8;
     41   my_purses_allowed INT8;
     42   my_needs_update BOOL;
     43   my_expiration_date INT8;
     44   my_desired_expiration INT8;
     45   reserve RECORD;
     46 BEGIN
     47 
     48 SELECT current_balance
     49       ,expiration_date
     50       ,purses_allowed
     51   INTO reserve
     52   FROM reserves
     53  WHERE reserve_pub=in_reserve_pub;
     54 
     55 IF NOT FOUND
     56 THEN
     57   RAISE NOTICE 'reserve not found';
     58   out_no_reserve = TRUE;
     59   out_no_funds = TRUE;
     60   out_reserve_balance.val = 0;
     61   out_reserve_balance.frac = 0;
     62   out_open_cost.val = 0;
     63   out_open_cost.frac = 0;
     64   out_final_expiration = 0;
     65   RETURN;
     66 END IF;
     67 
     68 out_no_reserve = FALSE;
     69 out_reserve_balance = reserve.current_balance;
     70 
     71 -- Do not allow expiration time to start in the past already
     72 IF (reserve.expiration_date < in_now)
     73 THEN
     74   my_expiration_date = in_now;
     75 ELSE
     76   my_expiration_date = reserve.expiration_date;
     77 END IF;
     78 
     79 my_cost.val = 0;
     80 my_cost.frac = 0;
     81 my_needs_update = FALSE;
     82 my_years = 0;
     83 
     84 -- Bound the requested expiration.  GNUNET_JSON_spec_timestamp() accepts the
     85 -- literal "never" and hands us INT64_MAX, and any t_s above roughly 9.2e12
     86 -- seconds is clamped to the same value; the additions below would then
     87 -- overflow INT8 (SQLSTATE 22003), which the caller can only turn into an
     88 -- HTTP 500.  Pricing such a request as if it had asked for the cap gives the
     89 -- client the ordinary "here is what it would cost" answer -- which it cannot
     90 -- pay, so it gets a 402 -- instead of an internal error.  The cap is the end
     91 -- of year 9999, the largest timestamp PostgreSQL itself can represent.
     92 my_desired_expiration = LEAST (in_desired_expiration, 253402300799000000);
     93 
     94 -- Compute years based on desired expiration time
     95 IF (my_expiration_date < my_desired_expiration)
     96 THEN
     97   my_years = (31535999999999 + my_desired_expiration - my_expiration_date) / 31536000000000;
     98   reserve.purses_allowed = in_default_purse_limit;
     99   my_expiration_date = my_expiration_date + 31536000000000 * my_years;
    100 END IF;
    101 
    102 -- Increase years based on purses requested
    103 IF (reserve.purses_allowed < in_min_purse_limit)
    104 THEN
    105   my_years = (31535999999999 + my_desired_expiration - in_now) / 31536000000000;
    106   my_expiration_date = in_now + 31536000000000 * my_years;
    107   -- in_min_purse_limit is an INT4 that comes straight from the client's
    108   -- "purse_limit", so this sum has to be evaluated in INT8: in INT4 it
    109   -- overflows before the division.
    110   my_years_tmp = (in_min_purse_limit::INT8 + in_default_purse_limit - reserve.purses_allowed - 1) / in_default_purse_limit;
    111   my_years = my_years + my_years_tmp;
    112   my_purses_allowed = reserve.purses_allowed::INT8 + (in_default_purse_limit::INT8 * my_years_tmp);
    113 ELSE
    114   my_purses_allowed = reserve.purses_allowed;
    115 END IF;
    116 
    117 -- Refuse to price what we cannot represent, rather than overflowing further
    118 -- down: reporting the maximum cost makes the request fail with "payment
    119 -- required", where an arithmetic overflow can only become an HTTP 500.
    120 IF ( (my_years > 8000) OR
    121      (my_purses_allowed > 2147483647) )
    122 THEN
    123   -- Just below TALER_AMOUNT_MAX_VALUE, the largest amount the C layer parses.
    124   out_open_cost.val=4503599627370495;
    125   out_open_cost.frac=99999999;
    126   out_no_funds=FALSE;
    127   -- We must return a failure, which the C layer detects by the returned
    128   -- expiration being below the desired expiration.  Returning
    129   -- my_expiration_date here would be at or above the desired expiration
    130   -- and thus be reported as a successful "reserve opened" (200 OK) even
    131   -- though the UPDATE below never ran.
    132   IF (reserve.expiration_date >= in_desired_expiration)
    133   THEN
    134     out_final_expiration = 0;
    135   ELSE
    136     out_final_expiration = reserve.expiration_date;
    137   END IF;
    138   RAISE NOTICE 'requested reserve lifetime or purse limit out of range';
    139   RETURN;
    140 END IF;
    141 
    142 -- Compute cost based on annual fees
    143 IF (my_years > 0)
    144 THEN
    145   -- my_years and taler_amount.frac are multiplied in INT8: with my_years
    146   -- declared INT4 this used to be an INT4 multiplication that overflowed
    147   -- before the division, making the guard below unreachable.
    148   my_cost.val = my_years * in_open_fee.val;
    149   my_cost_tmp = my_years * in_open_fee.frac / 100000000;
    150   IF (CAST (my_cost.val + my_cost_tmp AS INT8) < my_cost.val)
    151   THEN
    152     -- Must stay below TALER_AMOUNT_MAX_VALUE (and 100000000): the previous
    153     -- sentinel (INT64_MAX, 2^31-1) is not a legal taler amount, so the C
    154     -- layer refused to parse it and turned this branch into an HTTP 500 of
    155     -- its own.
    156     out_open_cost.val=4503599627370495;
    157     out_open_cost.frac=99999999;
    158     out_no_funds=FALSE;
    159     -- As above: the failure must be signalled by an expiration below the
    160     -- desired expiration, or the C layer reports 200 OK for a reserve
    161     -- that was never opened.
    162     IF (reserve.expiration_date >= in_desired_expiration)
    163     THEN
    164       out_final_expiration = 0;
    165     ELSE
    166       out_final_expiration = reserve.expiration_date;
    167     END IF;
    168     RAISE NOTICE 'arithmetic issue computing amount';
    169   RETURN;
    170   END IF;
    171   my_cost.val = CAST (my_cost.val + my_cost_tmp AS INT8);
    172   my_cost.frac = my_years * in_open_fee.frac % 100000000;
    173   my_needs_update = TRUE;
    174 END IF;
    175 
    176 -- check if we actually have something to do
    177 IF NOT my_needs_update
    178 THEN
    179   out_final_expiration = reserve.expiration_date;
    180   out_open_cost.val = 0;
    181   out_open_cost.frac = 0;
    182   out_no_funds=FALSE;
    183   RAISE NOTICE 'no change required';
    184   RETURN;
    185 END IF;
    186 
    187 -- Check payment (coins and reserve) would be sufficient.
    188 IF ( (in_total_paid.val < my_cost.val) OR
    189      ( (in_total_paid.val = my_cost.val) AND
    190        (in_total_paid.frac < my_cost.frac) ) )
    191 THEN
    192   out_open_cost.val = my_cost.val;
    193   out_open_cost.frac = my_cost.frac;
    194   out_no_funds=FALSE;
    195   -- We must return a failure, which is indicated by
    196   -- the expiration being below the desired expiration.
    197   IF (reserve.expiration_date >= in_desired_expiration)
    198   THEN
    199     -- This case is relevant especially if the purse
    200     -- count was to be increased and the payment was
    201     -- insufficient to cover this for the full period.
    202     RAISE NOTICE 'forcing low expiration time';
    203     out_final_expiration = 0;
    204   ELSE
    205     out_final_expiration = reserve.expiration_date;
    206   END IF;
    207   RAISE NOTICE 'amount paid too low';
    208   RETURN;
    209 END IF;
    210 
    211 -- Check reserve balance is sufficient.
    212 IF (out_reserve_balance.val > in_reserve_payment.val)
    213 THEN
    214   IF (out_reserve_balance.frac >= in_reserve_payment.frac)
    215   THEN
    216     my_balance.val=out_reserve_balance.val - in_reserve_payment.val;
    217     my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac;
    218   ELSE
    219     my_balance.val=out_reserve_balance.val - in_reserve_payment.val - 1;
    220     my_balance.frac=out_reserve_balance.frac + 100000000 - in_reserve_payment.frac;
    221   END IF;
    222 ELSE
    223   IF (out_reserve_balance.val = in_reserve_payment.val) AND (out_reserve_balance.frac >= in_reserve_payment.frac)
    224   THEN
    225     my_balance.val=0;
    226     my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac;
    227   ELSE
    228     out_final_expiration = reserve.expiration_date;
    229     out_open_cost.val = my_cost.val;
    230     out_open_cost.frac = my_cost.frac;
    231     out_no_funds=TRUE;
    232     RAISE NOTICE 'reserve balance too low';
    233   RETURN;
    234   END IF;
    235 END IF;
    236 
    237 UPDATE reserves SET
    238   current_balance=my_balance
    239  ,gc_date=reserve.expiration_date + in_reserve_gc_delay
    240  ,expiration_date=my_expiration_date
    241  ,purses_allowed=my_purses_allowed
    242 WHERE
    243  reserve_pub=in_reserve_pub;
    244 
    245 INSERT INTO reserves_open_requests
    246   (reserve_pub
    247   ,request_timestamp
    248   ,expiration_date
    249   ,reserve_sig
    250   ,reserve_payment
    251   ,requested_purse_limit
    252   ) VALUES (
    253    in_reserve_pub
    254   ,in_now
    255   ,in_desired_expiration
    256   ,in_reserve_sig
    257   ,in_reserve_payment
    258   ,in_min_purse_limit)
    259   ON CONFLICT DO NOTHING;
    260 
    261 out_final_expiration=my_expiration_date;
    262 out_open_cost = my_cost;
    263 out_no_funds=FALSE;
    264 RETURN;
    265 
    266 END $$;