depolymerization

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

depolymerizer-bitcoin-procedures.sql (12835B)


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