depolymerization

wire gateway for Bitcoin/Ethereum
Log | Files | Refs | Submodules | README | LICENSE

depolymerizer-bitcoin-procedures.sql (11712B)


      1 --
      2 -- This file is part of TALER
      3 -- Copyright (C) 2025, 2026 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 SET search_path TO depolymerizer_bitcoin;
     17 
     18 -- Remove all existing functions
     19 DO
     20 $do$
     21 DECLARE
     22   _sql text;
     23 BEGIN
     24   SELECT INTO _sql
     25         string_agg(format('DROP %s %s CASCADE;'
     26                         , CASE prokind
     27                             WHEN 'f' THEN 'FUNCTION'
     28                             WHEN 'p' THEN 'PROCEDURE'
     29                           END
     30                         , oid::regprocedure)
     31                   , E'\n')
     32   FROM   pg_proc
     33   WHERE  pronamespace = 'depolymerizer_bitcoin'::regnamespace;
     34 
     35   IF _sql IS NOT NULL THEN
     36     EXECUTE _sql;
     37   END IF;
     38 END
     39 $do$;
     40 
     41 CREATE FUNCTION taler_transfer(
     42   IN in_amount taler_amount,
     43   IN in_exchange_base_url TEXT,
     44   IN in_credit_acc TEXT,
     45   IN in_credit_name TEXT,
     46   IN in_request_uid BYTEA,
     47   IN in_wtid BYTEA,
     48   IN in_metadata TEXT,
     49   IN in_now INT8,
     50   -- Error status
     51   OUT out_request_uid_reuse BOOLEAN,
     52   OUT out_wtid_reuse BOOLEAN,
     53   -- Success return
     54   OUT out_transfer_row_id INT8,
     55   OUT out_created_at INT8
     56 )
     57 LANGUAGE plpgsql AS $$
     58 BEGIN
     59 -- Check for idempotence and conflict
     60 SELECT (amount != in_amount
     61           OR credit_acc != in_credit_acc
     62           OR credit_name != in_credit_name
     63           OR exchange_url != in_exchange_base_url
     64           OR wtid != in_wtid)
     65         ,transfer_id, created_at
     66   INTO out_request_uid_reuse, out_transfer_row_id, out_created_at
     67   FROM transfer
     68   WHERE request_uid = in_request_uid;
     69 IF FOUND THEN
     70   RETURN;
     71 END IF;
     72 
     73 -- Register a transfer operation
     74 INSERT INTO transfer (
     75   amount,
     76   exchange_url,
     77   credit_acc,
     78   credit_name,
     79   request_uid,
     80   wtid,
     81   metadata,
     82   created_at,
     83   status
     84 ) VALUES (
     85   in_amount,
     86   in_exchange_base_url,
     87   in_credit_acc,
     88   in_credit_name,
     89   in_request_uid,
     90   in_wtid,
     91   in_metadata,
     92   in_now,
     93   'requested'
     94 ) ON CONFLICT (wtid) DO NOTHING
     95   RETURNING transfer_id, created_at INTO out_transfer_row_id, out_created_at;
     96 out_wtid_reuse=NOT FOUND;
     97 IF out_wtid_reuse THEN
     98   RETURN;
     99 END IF;
    100 -- Notify new transaction
    101 PERFORM pg_notify('transfer', out_transfer_row_id || '');
    102 END $$;
    103 COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it';
    104 
    105 CREATE FUNCTION register_tx_in(
    106   IN in_txid BYTEA,
    107   IN in_amount taler_amount,
    108   IN in_debit_acc TEXT,
    109   IN in_received_at INT8,
    110   IN in_type incoming_type,
    111   IN in_metadata BYTEA,
    112   -- Error status
    113   OUT out_reserve_pub_reuse BOOLEAN,
    114   OUT out_mapping_reuse BOOLEAN,
    115   OUT out_unknown_mapping BOOLEAN,
    116   -- Success return
    117   OUT out_tx_row_id INT8,
    118   OUT out_valued_at INT8,
    119   OUT out_new BOOLEAN,
    120   OUT out_pending BOOLEAN
    121 )
    122 LANGUAGE plpgsql AS $$
    123 DECLARE
    124 local_authorization_pub BYTEA;
    125 local_authorization_sig BYTEA;
    126 BEGIN
    127 out_pending=false;
    128 
    129 -- Check for idempotence, txid is a hash of the transaction data, if the txid match all info match
    130 SELECT tx_in_id, received_at INTO out_tx_row_id, out_valued_at FROM tx_in WHERE txid = in_txid;
    131 out_new=NOT FOUND;
    132 IF NOT out_new THEN
    133     RETURN;
    134 END IF;
    135 
    136 -- Resolve mapping logic
    137 IF in_type = 'map' THEN
    138   SELECT type, account_pub, authorization_pub, authorization_sig,
    139       tx_in_id IS NOT NULL AND NOT recurrent,
    140       tx_in_id IS NOT NULL AND recurrent
    141     INTO in_type, in_metadata, local_authorization_pub, local_authorization_sig, out_mapping_reuse, out_pending
    142     FROM prepared_in
    143     WHERE authorization_pub = in_metadata;
    144   out_unknown_mapping = NOT FOUND;
    145   IF out_unknown_mapping OR out_mapping_reuse THEN
    146     RETURN;
    147   END IF;
    148 END IF;
    149 
    150 -- Check conflict
    151 out_reserve_pub_reuse=NOT out_pending AND in_type = 'reserve' AND EXISTS(SELECT FROM taler_in WHERE metadata = in_metadata AND type = 'reserve');
    152 IF out_reserve_pub_reuse THEN
    153   RETURN;
    154 END IF;
    155 
    156 -- Insert new incoming transaction
    157 INSERT INTO tx_in (
    158   txid,
    159   amount,
    160   debit_acc,
    161   received_at
    162 ) VALUES (
    163   in_txid,
    164   in_amount,
    165   in_debit_acc,
    166   in_received_at
    167 ) RETURNING tx_in_id, received_at INTO out_tx_row_id, out_valued_at;
    168 -- Notify new incoming transaction registration
    169 PERFORM pg_notify('tx_in', out_tx_row_id || '');
    170 
    171 IF out_pending THEN
    172   -- Delay talerable registration until mapping again
    173   INSERT INTO pending_recurrent_in (tx_in_id, authorization_pub)
    174     VALUES (out_tx_row_id, local_authorization_pub);
    175 ELSIF in_type IS NOT NULL THEN
    176   UPDATE prepared_in
    177   SET tx_in_id = out_tx_row_id
    178   WHERE (tx_in_id IS NULL AND account_pub = in_metadata) OR authorization_pub = local_authorization_pub;
    179   -- Insert new incoming talerable tranreceived_atsaction
    180   INSERT INTO taler_in (
    181     tx_in_id,
    182     type,
    183     metadata,
    184     authorization_pub,
    185     authorization_sig
    186   ) VALUES (
    187     out_tx_row_id,
    188     in_type,
    189     in_metadata,
    190     local_authorization_pub,
    191     local_authorization_sig
    192   );
    193   -- Notify new incoming talerable transaction registration
    194   PERFORM pg_notify('taler_in', out_tx_row_id || '');
    195 END IF;
    196 END $$;
    197 COMMENT ON FUNCTION register_tx_in IS 'Register an incoming transaction idempotently';
    198 
    199 
    200 CREATE FUNCTION register_bounce_tx_in(
    201   IN in_txid BYTEA,
    202   IN in_amount taler_amount,
    203   IN in_debit_acc TEXT,
    204   IN in_received_at INT8,
    205   IN in_reason TEXT,
    206   IN in_now INT8,
    207   -- Success return
    208   OUT out_tx_row_id INT8,
    209   OUT out_tx_new BOOLEAN,
    210   OUT out_bounce_row_id INT8,
    211   OUT out_bounce_new BOOLEAN
    212 )
    213 LANGUAGE plpgsql AS $$
    214 BEGIN
    215 -- Register incoming transaction idempotently
    216 SELECT register_tx_in.out_tx_row_id, register_tx_in.out_new
    217 INTO out_tx_row_id, out_tx_new
    218 FROM register_tx_in(in_txid, in_amount, in_debit_acc, in_received_at, NULL, NULL);
    219 
    220 -- Register bounce
    221 INSERT INTO bounced(
    222   tx_in_id,
    223   reason,
    224   status
    225 ) VALUES (
    226   out_tx_row_id,
    227   in_reason,
    228   'requested'
    229 ) ON CONFLICT (tx_in_id) DO NOTHING;
    230 END $$;
    231 COMMENT ON FUNCTION register_bounce_tx_in IS 'Register an incoming transaction and bounce it idempotently';
    232 
    233 CREATE FUNCTION sync_out(
    234   IN in_txid BYTEA,
    235   IN in_replace_txid BYTEA,
    236   IN in_amount taler_amount,
    237   IN in_credit_acc TEXT,
    238   IN in_wtid BYTEA,
    239   IN in_exchange_base_url TEXT,
    240   IN in_metadata TEXT,
    241   IN in_bounced_txid BYTEA,
    242   IN in_created_at INT8,
    243   IN in_now INT8,
    244   -- Success return
    245   OUT out_tx_row_id INT8,
    246   OUT out_new BOOLEAN,
    247   OUT out_replaced BOOLEAN,
    248   OUT out_recovered BOOLEAN
    249 )
    250 LANGUAGE plpgsql AS $$
    251 DECLARE
    252   local_id INT8;
    253 BEGIN
    254 UPDATE tx_out SET txid=in_txid WHERE txid=in_replace_txid;
    255 out_replaced=FOUND;
    256 out_new=NOT EXISTS(SELECT FROM tx_out WHERE txid = in_txid);
    257 IF NOT out_new THEN
    258     RETURN;
    259 END IF;
    260 
    261 -- Insert new outgoing transaction
    262 INSERT INTO tx_out (
    263   amount,
    264   credit_acc,
    265   txid,
    266   created_at
    267 ) VALUES (
    268   in_amount,
    269   in_credit_acc,
    270   in_txid,
    271   in_created_at
    272 ) RETURNING tx_out_id INTO out_tx_row_id;
    273 -- Notify new outgoing transaction registration
    274 PERFORM pg_notify('tx_out', out_tx_row_id || '');
    275 
    276 IF in_wtid IS NOT NULL THEN
    277   -- Insert new outgoing talerable transaction
    278   INSERT INTO taler_out (
    279     tx_out_id,
    280     wtid,
    281     exchange_base_url,
    282     metadata
    283   ) VALUES (
    284     out_tx_row_id,
    285     in_wtid,
    286     in_exchange_base_url,
    287     in_metadata
    288   ) ON CONFLICT (wtid) DO NOTHING;
    289   IF FOUND THEN
    290     -- Notify new outgoing talerable transaction registration
    291     PERFORM pg_notify('taler_out', out_tx_row_id || '');
    292   END IF;
    293   -- Update transfer state
    294   UPDATE transfer SET status='confirmed',txid=in_txid WHERE wtid=in_wtid;
    295   out_recovered=FOUND;
    296 ELSIF in_bounced_txid IS NOT NULL THEN
    297   -- Update bounce state
    298   IF NOT EXISTS(SELECT FROM bounced JOIN tx_in USING (tx_in_id) WHERE tx_in.txid=in_bounced_txid) THEN
    299     INSERT INTO bounced (
    300       tx_in_id,
    301       txid,
    302       status
    303     ) VALUES (
    304       (SELECT tx_in_id FROM tx_in WHERE txid=in_bounced_txid),
    305       in_txid,
    306       'confirmed'
    307     );
    308     out_recovered=TRUE;
    309   ELSE
    310     UPDATE bounced SET status='confirmed',txid=in_txid
    311     FROM tx_in
    312     WHERE bounced.tx_in_id=tx_in.tx_in_id AND tx_in.txid=in_bounced_txid;
    313     out_recovered=FALSE;
    314   END IF;
    315 END IF;
    316 END $$;
    317 COMMENT ON FUNCTION sync_out IS 'Sync a debit blockchain state with local state';
    318 
    319 
    320 CREATE FUNCTION register_prepared_transfers (
    321   IN in_type incoming_type,
    322   IN in_account_pub BYTEA,
    323   IN in_authorization_pub BYTEA,
    324   IN in_authorization_sig BYTEA,
    325   IN in_recurrent BOOLEAN,
    326   IN in_timestamp INT8,
    327   -- Error status
    328   OUT out_reserve_pub_reuse BOOLEAN
    329 )
    330 LANGUAGE plpgsql AS $$
    331 DECLARE
    332   talerable_tx INT8;
    333   idempotent BOOLEAN;
    334 BEGIN
    335 
    336 -- Check idempotency
    337 SELECT type = in_type
    338     AND account_pub = in_account_pub
    339     AND recurrent = in_recurrent
    340 INTO idempotent
    341 FROM prepared_in
    342 WHERE authorization_pub = in_authorization_pub;
    343 
    344 -- Check idempotency and delay garbage collection
    345 IF FOUND AND idempotent THEN
    346   UPDATE prepared_in
    347   SET registered_at=in_timestamp
    348   WHERE authorization_pub=in_authorization_pub;
    349   RETURN;
    350 END IF;
    351 
    352 -- Check reserve pub reuse
    353 out_reserve_pub_reuse=in_type = 'reserve' AND (
    354   EXISTS(SELECT FROM taler_in WHERE metadata = in_account_pub AND type = 'reserve')
    355   OR EXISTS(SELECT FROM prepared_in WHERE account_pub = in_account_pub AND type = 'reserve' AND authorization_pub != in_authorization_pub)
    356 );
    357 IF out_reserve_pub_reuse THEN
    358   RETURN;
    359 END IF;
    360 
    361 IF in_recurrent THEN
    362   -- Finalize one pending right now
    363   WITH moved_tx AS (
    364     DELETE FROM pending_recurrent_in
    365     WHERE tx_in_id = (
    366       SELECT txid
    367       FROM tx_in
    368       JOIN tx_in USING (tx_in_id)
    369       WHERE authorization_pub = in_authorization_pub
    370       ORDER BY created_at ASC
    371       LIMIT 1
    372     )
    373     RETURNING tx_in_id
    374   )
    375   INSERT INTO taler_in (tx_in_id, type, metadata, authorization_pub, authorization_sig)
    376   SELECT moved_tx.tx_in_id, in_type, in_account_pub, in_authorization_pub, in_authorization_sig
    377   FROM moved_tx
    378   RETURNING tx_in_id INTO talerable_tx;
    379   IF talerable_tx IS NOT NULL THEN
    380     PERFORM pg_notify('taler_in', talerable_tx::text);
    381   END IF;
    382 ELSE
    383   -- Bounce all pending
    384   WITH bounced AS (
    385     DELETE FROM pending_recurrent_in
    386     WHERE authorization_pub = in_authorization_pub
    387     RETURNING tx_in_id
    388   )
    389   INSERT INTO bounced (tx_in_id, reason, status)
    390   SELECT tx_in_id, 'cancelled mapping', 'requested' FROM bounced;
    391 END IF;
    392 
    393 -- Upsert registration
    394 INSERT INTO prepared_in (
    395   type,
    396   account_pub,
    397   authorization_pub,
    398   authorization_sig,
    399   recurrent,
    400   registered_at,
    401   tx_in_id
    402 ) VALUES (
    403   in_type,
    404   in_account_pub,
    405   in_authorization_pub,
    406   in_authorization_sig,
    407   in_recurrent,
    408   in_timestamp,
    409   talerable_tx
    410 ) ON CONFLICT (authorization_pub)
    411 DO UPDATE SET
    412   type = EXCLUDED.type,
    413   account_pub = EXCLUDED.account_pub,
    414   recurrent = EXCLUDED.recurrent,
    415   registered_at = EXCLUDED.registered_at,
    416   tx_in_id = EXCLUDED.tx_in_id,
    417   authorization_sig = EXCLUDED.authorization_sig;
    418 END $$;
    419 
    420 CREATE FUNCTION delete_prepared_transfers (
    421   IN in_authorization_pub BYTEA,
    422   IN in_timestamp INT8,
    423   OUT out_found BOOLEAN
    424 )
    425 LANGUAGE plpgsql AS $$
    426 BEGIN
    427 
    428 -- Bounce all pending
    429 WITH bounced AS (
    430   DELETE FROM pending_recurrent_in
    431   WHERE authorization_pub = in_authorization_pub
    432   RETURNING tx_in_id
    433 )
    434 INSERT INTO bounced (tx_in_id, reason, status)
    435 SELECT tx_in_id, 'cancelled mapping', 'requested' FROM bounced;
    436 
    437 -- Delete registration
    438 DELETE FROM prepared_in
    439 WHERE authorization_pub = in_authorization_pub;
    440 out_found = FOUND;
    441 
    442 END $$;