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 (7967B)


      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_final_expiration=my_expiration_date;
    127   out_no_funds=FALSE;
    128   RAISE NOTICE 'requested reserve lifetime or purse limit out of range';
    129   RETURN;
    130 END IF;
    131 
    132 -- Compute cost based on annual fees
    133 IF (my_years > 0)
    134 THEN
    135   -- my_years and taler_amount.frac are multiplied in INT8: with my_years
    136   -- declared INT4 this used to be an INT4 multiplication that overflowed
    137   -- before the division, making the guard below unreachable.
    138   my_cost.val = my_years * in_open_fee.val;
    139   my_cost_tmp = my_years * in_open_fee.frac / 100000000;
    140   IF (CAST (my_cost.val + my_cost_tmp AS INT8) < my_cost.val)
    141   THEN
    142     -- Must stay below TALER_AMOUNT_MAX_VALUE (and 100000000): the previous
    143     -- sentinel (INT64_MAX, 2^31-1) is not a legal taler amount, so the C
    144     -- layer refused to parse it and turned this branch into an HTTP 500 of
    145     -- its own.
    146     out_open_cost.val=4503599627370495;
    147     out_open_cost.frac=99999999;
    148     out_final_expiration=my_expiration_date;
    149     out_no_funds=FALSE;
    150     RAISE NOTICE 'arithmetic issue computing amount';
    151   RETURN;
    152   END IF;
    153   my_cost.val = CAST (my_cost.val + my_cost_tmp AS INT8);
    154   my_cost.frac = my_years * in_open_fee.frac % 100000000;
    155   my_needs_update = TRUE;
    156 END IF;
    157 
    158 -- check if we actually have something to do
    159 IF NOT my_needs_update
    160 THEN
    161   out_final_expiration = reserve.expiration_date;
    162   out_open_cost.val = 0;
    163   out_open_cost.frac = 0;
    164   out_no_funds=FALSE;
    165   RAISE NOTICE 'no change required';
    166   RETURN;
    167 END IF;
    168 
    169 -- Check payment (coins and reserve) would be sufficient.
    170 IF ( (in_total_paid.val < my_cost.val) OR
    171      ( (in_total_paid.val = my_cost.val) AND
    172        (in_total_paid.frac < my_cost.frac) ) )
    173 THEN
    174   out_open_cost.val = my_cost.val;
    175   out_open_cost.frac = my_cost.frac;
    176   out_no_funds=FALSE;
    177   -- We must return a failure, which is indicated by
    178   -- the expiration being below the desired expiration.
    179   IF (reserve.expiration_date >= in_desired_expiration)
    180   THEN
    181     -- This case is relevant especially if the purse
    182     -- count was to be increased and the payment was
    183     -- insufficient to cover this for the full period.
    184     RAISE NOTICE 'forcing low expiration time';
    185     out_final_expiration = 0;
    186   ELSE
    187     out_final_expiration = reserve.expiration_date;
    188   END IF;
    189   RAISE NOTICE 'amount paid too low';
    190   RETURN;
    191 END IF;
    192 
    193 -- Check reserve balance is sufficient.
    194 IF (out_reserve_balance.val > in_reserve_payment.val)
    195 THEN
    196   IF (out_reserve_balance.frac >= in_reserve_payment.frac)
    197   THEN
    198     my_balance.val=out_reserve_balance.val - in_reserve_payment.val;
    199     my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac;
    200   ELSE
    201     my_balance.val=out_reserve_balance.val - in_reserve_payment.val - 1;
    202     my_balance.frac=out_reserve_balance.frac + 100000000 - in_reserve_payment.frac;
    203   END IF;
    204 ELSE
    205   IF (out_reserve_balance.val = in_reserve_payment.val) AND (out_reserve_balance.frac >= in_reserve_payment.frac)
    206   THEN
    207     my_balance.val=0;
    208     my_balance.frac=out_reserve_balance.frac - in_reserve_payment.frac;
    209   ELSE
    210     out_final_expiration = reserve.expiration_date;
    211     out_open_cost.val = my_cost.val;
    212     out_open_cost.frac = my_cost.frac;
    213     out_no_funds=TRUE;
    214     RAISE NOTICE 'reserve balance too low';
    215   RETURN;
    216   END IF;
    217 END IF;
    218 
    219 UPDATE reserves SET
    220   current_balance=my_balance
    221  ,gc_date=reserve.expiration_date + in_reserve_gc_delay
    222  ,expiration_date=my_expiration_date
    223  ,purses_allowed=my_purses_allowed
    224 WHERE
    225  reserve_pub=in_reserve_pub;
    226 
    227 out_final_expiration=my_expiration_date;
    228 out_open_cost = my_cost;
    229 out_no_funds=FALSE;
    230 RETURN;
    231 
    232 END $$;