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 $$;