libeufin-bank-procedures.sql (71772B)
1 -- 2 -- This file is part of TALER 3 -- Copyright (C) 2023, 2024, 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 BEGIN; 17 SET search_path TO libeufin_bank; 18 19 -- Remove all existing functions 20 DO 21 $do$ 22 DECLARE 23 _sql text; 24 BEGIN 25 SELECT INTO _sql 26 string_agg(format('DROP %s %s CASCADE;' 27 , CASE prokind 28 WHEN 'f' THEN 'FUNCTION' 29 WHEN 'p' THEN 'PROCEDURE' 30 END 31 , oid::regprocedure) 32 , E'\n') 33 FROM pg_proc 34 WHERE pronamespace = 'libeufin_bank'::regnamespace; 35 36 IF _sql IS NOT NULL THEN 37 EXECUTE _sql; 38 END IF; 39 END 40 $do$; 41 42 CREATE FUNCTION url_encode(input TEXT) 43 RETURNS TEXT 44 LANGUAGE plpgsql IMMUTABLE AS $$ 45 DECLARE 46 result TEXT := ''; 47 char TEXT; 48 BEGIN 49 FOR i IN 1..length(input) LOOP 50 char := substring(input FROM i FOR 1); 51 IF char ~ '[A-Za-z0-9\-._~]' THEN 52 result := result || char; 53 ELSE 54 result := result || '%' || lpad(upper(to_hex(ascii(char))), 2, '0'); 55 END IF; 56 END LOOP; 57 RETURN result; 58 END; 59 $$; 60 61 CREATE OR REPLACE FUNCTION sort_uniq(anyarray) 62 RETURNS anyarray LANGUAGE SQL IMMUTABLE AS $$ 63 SELECT COALESCE(array_agg(DISTINCT x ORDER BY x), $1[0:0]) 64 FROM unnest($1) AS t(x); 65 $$; 66 67 CREATE FUNCTION amount_normalize( 68 IN amount taler_amount 69 ,OUT normalized taler_amount 70 ) 71 LANGUAGE plpgsql IMMUTABLE AS $$ 72 BEGIN 73 normalized.val = amount.val + amount.frac / 100000000; 74 IF (normalized.val > 1::INT8<<52) THEN 75 RAISE EXCEPTION 'amount value overflowed'; 76 END IF; 77 normalized.frac = amount.frac % 100000000; 78 79 END $$; 80 COMMENT ON FUNCTION amount_normalize 81 IS 'Returns the normalized amount by adding to the .val the value of (.frac / 100000000) and removing the modulus 100000000 from .frac.' 82 'It raises an exception when the resulting .val is larger than 2^52'; 83 84 CREATE FUNCTION amount_add( 85 IN l taler_amount 86 ,IN r taler_amount 87 ,OUT sum taler_amount 88 ) 89 LANGUAGE plpgsql IMMUTABLE AS $$ 90 BEGIN 91 sum = (l.val + r.val, l.frac + r.frac); 92 SELECT normalized.val, normalized.frac INTO sum.val, sum.frac FROM amount_normalize(sum) as normalized; 93 END $$; 94 COMMENT ON FUNCTION amount_add 95 IS 'Returns the normalized sum of two amounts. It raises an exception when the resulting .val is larger than 2^52'; 96 97 CREATE FUNCTION amount_left_minus_right( 98 IN l taler_amount 99 ,IN r taler_amount 100 ,OUT diff taler_amount 101 ,OUT ok BOOLEAN 102 ) 103 LANGUAGE plpgsql IMMUTABLE AS $$ 104 BEGIN 105 diff = l; 106 IF diff.frac < r.frac THEN 107 IF diff.val <= 0 THEN 108 diff = (-1, -1); 109 ok = FALSE; 110 RETURN; 111 END IF; 112 diff.frac = diff.frac + 100000000; 113 diff.val = diff.val - 1; 114 END IF; 115 IF diff.val < r.val THEN 116 diff = (-1, -1); 117 ok = FALSE; 118 RETURN; 119 END IF; 120 diff.val = diff.val - r.val; 121 diff.frac = diff.frac - r.frac; 122 ok = TRUE; 123 END $$; 124 COMMENT ON FUNCTION amount_left_minus_right 125 IS 'Subtracts the right amount from the left and returns the difference and TRUE, if the left amount is larger than the right, or an invalid amount and FALSE otherwise.'; 126 127 CREATE FUNCTION account_balance_is_sufficient( 128 IN in_account_id INT8, 129 IN in_amount taler_amount, 130 IN in_wire_transfer_fees taler_amount, 131 IN in_min_amount taler_amount, 132 IN in_max_amount taler_amount, 133 OUT out_balance_insufficient BOOLEAN, 134 OUT out_bad_amount BOOLEAN 135 ) 136 LANGUAGE plpgsql STABLE AS $$ 137 DECLARE 138 account_has_debt BOOLEAN; 139 account_balance taler_amount; 140 account_max_debt taler_amount; 141 amount_with_fee taler_amount; 142 BEGIN 143 144 -- Check min and max 145 SELECT (SELECT in_min_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_amount, in_min_amount)) OR 146 (SELECT in_max_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_max_amount, in_amount)) 147 INTO out_bad_amount; 148 IF out_bad_amount THEN 149 RETURN; 150 END IF; 151 152 -- Add fees to the amount 153 IF in_wire_transfer_fees IS NOT NULL AND in_wire_transfer_fees != (0, 0)::taler_amount THEN 154 SELECT sum.val, sum.frac 155 INTO amount_with_fee.val, amount_with_fee.frac 156 FROM amount_add(in_amount, in_wire_transfer_fees) as sum; 157 ELSE 158 amount_with_fee = in_amount; 159 END IF; 160 161 -- Get account info, we expect the account to exist 162 SELECT 163 has_debt, 164 (balance).val, (balance).frac, 165 (max_debt).val, (max_debt).frac 166 INTO 167 account_has_debt, 168 account_balance.val, account_balance.frac, 169 account_max_debt.val, account_max_debt.frac 170 FROM bank_accounts WHERE bank_account_id=in_account_id; 171 172 -- Check enough funds 173 IF account_has_debt THEN 174 -- debt case: simply checking against the max debt allowed. 175 SELECT sum.val, sum.frac 176 INTO account_balance.val, account_balance.frac 177 FROM amount_add(account_balance, amount_with_fee) as sum; 178 SELECT NOT ok 179 INTO out_balance_insufficient 180 FROM amount_left_minus_right(account_max_debt, account_balance); 181 IF out_balance_insufficient THEN 182 RETURN; 183 END IF; 184 ELSE -- not a debt account 185 SELECT NOT ok 186 INTO out_balance_insufficient 187 FROM amount_left_minus_right(account_balance, amount_with_fee); 188 IF out_balance_insufficient THEN 189 -- debtor will switch to debt: determine their new negative balance. 190 SELECT 191 (diff).val, (diff).frac 192 INTO 193 account_balance.val, account_balance.frac 194 FROM amount_left_minus_right(amount_with_fee, account_balance); 195 SELECT NOT ok 196 INTO out_balance_insufficient 197 FROM amount_left_minus_right(account_max_debt, account_balance); 198 IF out_balance_insufficient THEN 199 RETURN; 200 END IF; 201 END IF; 202 END IF; 203 END $$; 204 COMMENT ON FUNCTION account_balance_is_sufficient IS 'Check if an account have enough fund to transfer an amount.'; 205 206 CREATE FUNCTION account_max_amount( 207 IN in_account_id INT8, 208 IN in_max_amount taler_amount, 209 OUT out_max_amount taler_amount 210 ) 211 LANGUAGE plpgsql STABLE AS $$ 212 BEGIN 213 -- add balance and max_debt 214 WITH computed AS ( 215 SELECT CASE has_debt 216 WHEN false THEN amount_add(balance, max_debt) 217 ELSE (SELECT diff FROM amount_left_minus_right(max_debt, balance)) 218 END AS amount 219 FROM bank_accounts WHERE bank_account_id=in_account_id 220 ) SELECT (amount).val, (amount).frac 221 INTO out_max_amount.val, out_max_amount.frac 222 FROM computed; 223 224 IF in_max_amount.val < out_max_amount.val 225 OR (in_max_amount.val = out_max_amount.val OR in_max_amount.frac < out_max_amount.frac) THEN 226 out_max_amount = in_max_amount; 227 END IF; 228 END $$; 229 230 CREATE FUNCTION create_token( 231 IN in_username TEXT, 232 IN in_content BYTEA, 233 IN in_creation_time INT8, 234 IN in_expiration_time INT8, 235 IN in_scope token_scope_enum, 236 IN in_refreshable BOOLEAN, 237 IN in_description TEXT, 238 IN in_is_tan BOOLEAN, 239 OUT out_tan_required BOOLEAN 240 ) 241 LANGUAGE plpgsql AS $$ 242 DECLARE 243 local_customer_id INT8; 244 BEGIN 245 -- Get account id and check if 2FA is required 246 SELECT customer_id, NOT in_is_tan AND cardinality(tan_channels) > 0 247 INTO local_customer_id, out_tan_required 248 FROM customers JOIN bank_accounts ON owning_customer_id = customer_id 249 WHERE username = in_username AND deleted_at IS NULL; 250 IF out_tan_required THEN 251 RETURN; 252 END IF; 253 INSERT INTO bearer_tokens ( 254 content, 255 creation_time, 256 expiration_time, 257 scope, 258 bank_customer, 259 is_refreshable, 260 description, 261 last_access 262 ) VALUES ( 263 in_content, 264 in_creation_time, 265 in_expiration_time, 266 in_scope, 267 local_customer_id, 268 in_refreshable, 269 in_description, 270 in_creation_time 271 ); 272 END $$; 273 274 CREATE FUNCTION bank_wire_transfer( 275 IN in_creditor_account_id INT8, 276 IN in_debtor_account_id INT8, 277 IN in_subject TEXT, 278 IN in_amount taler_amount, 279 IN in_timestamp INT8, 280 IN in_wire_transfer_fees taler_amount, 281 IN in_min_amount taler_amount, 282 IN in_max_amount taler_amount, 283 -- Error status 284 OUT out_balance_insufficient BOOLEAN, 285 OUT out_bad_amount BOOLEAN, 286 -- Success return 287 OUT out_credit_row_id INT8, 288 OUT out_debit_row_id INT8 289 ) 290 LANGUAGE plpgsql AS $$ 291 DECLARE 292 has_fee BOOLEAN; 293 amount_with_fee taler_amount; 294 admin_account_id INT8; 295 admin_has_debt BOOLEAN; 296 admin_balance taler_amount; 297 admin_payto TEXT; 298 admin_name TEXT; 299 debtor_has_debt BOOLEAN; 300 debtor_balance taler_amount; 301 debtor_max_debt taler_amount; 302 debtor_payto TEXT; 303 debtor_name TEXT; 304 creditor_has_debt BOOLEAN; 305 creditor_balance taler_amount; 306 creditor_payto TEXT; 307 creditor_name TEXT; 308 tmp_balance taler_amount; 309 BEGIN 310 -- Check min and max 311 SELECT (SELECT in_min_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_amount, in_min_amount)) OR 312 (SELECT in_max_amount IS NOT NULL AND NOT ok FROM amount_left_minus_right(in_max_amount, in_amount)) 313 INTO out_bad_amount; 314 IF out_bad_amount THEN 315 RETURN; 316 END IF; 317 318 has_fee = in_wire_transfer_fees IS NOT NULL AND in_wire_transfer_fees != (0, 0)::taler_amount; 319 IF has_fee THEN 320 -- Retrieve admin info 321 SELECT 322 bank_account_id, has_debt, 323 (balance).val, (balance).frac, 324 internal_payto, customers.name 325 INTO 326 admin_account_id, admin_has_debt, 327 admin_balance.val, admin_balance.frac, 328 admin_payto, admin_name 329 FROM bank_accounts 330 JOIN customers ON customer_id=owning_customer_id 331 WHERE username = 'admin'; 332 IF NOT FOUND THEN 333 RAISE EXCEPTION 'No admin'; 334 END IF; 335 END IF; 336 337 -- Retrieve debtor info 338 SELECT 339 has_debt, 340 (balance).val, (balance).frac, 341 (max_debt).val, (max_debt).frac, 342 internal_payto, customers.name 343 INTO 344 debtor_has_debt, 345 debtor_balance.val, debtor_balance.frac, 346 debtor_max_debt.val, debtor_max_debt.frac, 347 debtor_payto, debtor_name 348 FROM bank_accounts 349 JOIN customers ON customer_id=owning_customer_id 350 WHERE bank_account_id=in_debtor_account_id; 351 IF NOT FOUND THEN 352 RAISE EXCEPTION 'Unknown debtor %', in_debtor_account_id; 353 END IF; 354 -- Retrieve creditor info 355 SELECT 356 has_debt, 357 (balance).val, (balance).frac, 358 internal_payto, customers.name 359 INTO 360 creditor_has_debt, 361 creditor_balance.val, creditor_balance.frac, 362 creditor_payto, creditor_name 363 FROM bank_accounts 364 JOIN customers ON customer_id=owning_customer_id 365 WHERE bank_account_id=in_creditor_account_id; 366 IF NOT FOUND THEN 367 RAISE EXCEPTION 'Unknown creditor %', in_creditor_account_id; 368 END IF; 369 370 -- Add fees to the amount 371 IF has_fee AND admin_account_id != in_debtor_account_id THEN 372 SELECT sum.val, sum.frac 373 INTO amount_with_fee.val, amount_with_fee.frac 374 FROM amount_add(in_amount, in_wire_transfer_fees) as sum; 375 ELSE 376 has_fee=false; 377 amount_with_fee = in_amount; 378 END IF; 379 380 -- DEBTOR SIDE 381 -- check debtor has enough funds. 382 IF debtor_has_debt THEN 383 -- debt case: simply checking against the max debt allowed. 384 SELECT sum.val, sum.frac 385 INTO debtor_balance.val, debtor_balance.frac 386 FROM amount_add(debtor_balance, amount_with_fee) as sum; 387 SELECT NOT ok 388 INTO out_balance_insufficient 389 FROM amount_left_minus_right(debtor_max_debt, 390 debtor_balance); 391 IF out_balance_insufficient THEN 392 RETURN; 393 END IF; 394 ELSE -- not a debt account 395 SELECT 396 NOT ok, 397 (diff).val, (diff).frac 398 INTO 399 out_balance_insufficient, 400 tmp_balance.val, 401 tmp_balance.frac 402 FROM amount_left_minus_right(debtor_balance, 403 amount_with_fee); 404 IF NOT out_balance_insufficient THEN -- debtor has enough funds in the (positive) balance. 405 debtor_balance=tmp_balance; 406 ELSE -- debtor will switch to debt: determine their new negative balance. 407 SELECT 408 (diff).val, (diff).frac 409 INTO 410 debtor_balance.val, debtor_balance.frac 411 FROM amount_left_minus_right(amount_with_fee, 412 debtor_balance); 413 debtor_has_debt=TRUE; 414 SELECT NOT ok 415 INTO out_balance_insufficient 416 FROM amount_left_minus_right(debtor_max_debt, 417 debtor_balance); 418 IF out_balance_insufficient THEN 419 RETURN; 420 END IF; 421 END IF; 422 END IF; 423 424 -- CREDITOR SIDE. 425 -- Here we figure out whether the creditor would switch 426 -- from debit to a credit situation, and adjust the balance 427 -- accordingly. 428 IF NOT creditor_has_debt THEN -- easy case. 429 SELECT sum.val, sum.frac 430 INTO creditor_balance.val, creditor_balance.frac 431 FROM amount_add(creditor_balance, in_amount) as sum; 432 ELSE -- creditor had debit but MIGHT switch to credit. 433 SELECT 434 (diff).val, (diff).frac, 435 NOT ok 436 INTO 437 tmp_balance.val, tmp_balance.frac, 438 creditor_has_debt 439 FROM amount_left_minus_right(in_amount, 440 creditor_balance); 441 IF NOT creditor_has_debt THEN 442 creditor_balance=tmp_balance; 443 ELSE 444 -- the amount is not enough to bring the receiver 445 -- to a credit state, switch operators to calculate the new balance. 446 SELECT 447 (diff).val, (diff).frac 448 INTO creditor_balance.val, creditor_balance.frac 449 FROM amount_left_minus_right(creditor_balance, 450 in_amount); 451 END IF; 452 END IF; 453 454 -- ADMIN SIDE. 455 -- Here we figure out whether the administrator would switch 456 -- from debit to a credit situation, and adjust the balance 457 -- accordingly. 458 IF has_fee THEN 459 IF NOT admin_has_debt THEN -- easy case. 460 SELECT sum.val, sum.frac 461 INTO admin_balance.val, admin_balance.frac 462 FROM amount_add(admin_balance, in_wire_transfer_fees) as sum; 463 ELSE -- creditor had debit but MIGHT switch to credit. 464 SELECT (diff).val, (diff).frac, NOT ok 465 INTO 466 tmp_balance.val, tmp_balance.frac, 467 admin_has_debt 468 FROM amount_left_minus_right(in_wire_transfer_fees, admin_balance); 469 IF NOT admin_has_debt THEN 470 admin_balance=tmp_balance; 471 ELSE 472 -- the amount is not enough to bring the receiver 473 -- to a credit state, switch operators to calculate the new balance. 474 SELECT (diff).val, (diff).frac 475 INTO admin_balance.val, admin_balance.frac 476 FROM amount_left_minus_right(admin_balance, in_wire_transfer_fees); 477 END IF; 478 END IF; 479 END IF; 480 481 -- Lock account in order to prevent deadlocks 482 PERFORM FROM bank_accounts 483 WHERE bank_account_id IN (in_debtor_account_id, in_creditor_account_id, admin_account_id) 484 ORDER BY bank_account_id 485 FOR UPDATE; 486 487 -- now actually create the bank transaction. 488 -- debtor side: 489 INSERT INTO bank_account_transactions ( 490 creditor_payto 491 ,creditor_name 492 ,debtor_payto 493 ,debtor_name 494 ,subject 495 ,amount 496 ,transaction_date 497 ,direction 498 ,bank_account_id 499 ) 500 VALUES ( 501 creditor_payto, 502 creditor_name, 503 debtor_payto, 504 debtor_name, 505 in_subject, 506 in_amount, 507 in_timestamp, 508 'debit', 509 in_debtor_account_id 510 ) RETURNING bank_transaction_id INTO out_debit_row_id; 511 512 -- debtor side: 513 INSERT INTO bank_account_transactions ( 514 creditor_payto 515 ,creditor_name 516 ,debtor_payto 517 ,debtor_name 518 ,subject 519 ,amount 520 ,transaction_date 521 ,direction 522 ,bank_account_id 523 ) 524 VALUES ( 525 creditor_payto, 526 creditor_name, 527 debtor_payto, 528 debtor_name, 529 in_subject, 530 in_amount, 531 in_timestamp, 532 'credit', 533 in_creditor_account_id 534 ) RETURNING bank_transaction_id INTO out_credit_row_id; 535 536 -- checks and balances set up, now update bank accounts. 537 UPDATE bank_accounts 538 SET 539 balance=debtor_balance, 540 has_debt=debtor_has_debt 541 WHERE bank_account_id=in_debtor_account_id; 542 543 UPDATE bank_accounts 544 SET 545 balance=creditor_balance, 546 has_debt=creditor_has_debt 547 WHERE bank_account_id=in_creditor_account_id; 548 549 -- Fee part 550 IF has_fee THEN 551 INSERT INTO bank_account_transactions ( 552 creditor_payto 553 ,creditor_name 554 ,debtor_payto 555 ,debtor_name 556 ,subject 557 ,amount 558 ,transaction_date 559 ,direction 560 ,bank_account_id 561 ) 562 VALUES ( 563 admin_payto, 564 admin_name, 565 debtor_payto, 566 debtor_name, 567 'wire transfer fees for tx ' || out_debit_row_id, 568 in_wire_transfer_fees, 569 in_timestamp, 570 'debit', 571 in_debtor_account_id 572 ), ( 573 admin_payto, 574 admin_name, 575 debtor_payto, 576 debtor_name, 577 'wire transfer fees for tx ' || out_debit_row_id, 578 in_amount, 579 in_timestamp, 580 'credit', 581 admin_account_id 582 ); 583 584 UPDATE bank_accounts 585 SET 586 balance=admin_balance, 587 has_debt=admin_has_debt 588 WHERE bank_account_id=admin_account_id; 589 END IF; 590 591 -- notify new transaction 592 PERFORM pg_notify('bank_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || out_debit_row_id || ' ' || out_credit_row_id); 593 END $$; 594 595 CREATE FUNCTION account_delete( 596 IN in_username TEXT, 597 IN in_timestamp INT8, 598 IN in_is_tan BOOLEAN, 599 OUT out_not_found BOOLEAN, 600 OUT out_balance_not_zero BOOLEAN, 601 OUT out_tan_required BOOLEAN 602 ) 603 LANGUAGE plpgsql AS $$ 604 DECLARE 605 my_customer_id INT8; 606 BEGIN 607 -- check if account exists, has zero balance and if 2FA is required 608 SELECT 609 customer_id 610 ,NOT in_is_tan AND cardinality(tan_channels) > 0 611 ,(balance).val != 0 OR (balance).frac != 0 612 INTO 613 my_customer_id 614 ,out_tan_required 615 ,out_balance_not_zero 616 FROM customers 617 JOIN bank_accounts ON owning_customer_id = customer_id 618 WHERE username = in_username AND deleted_at IS NULL; 619 IF NOT FOUND OR out_balance_not_zero OR out_tan_required THEN 620 out_not_found=NOT FOUND; 621 RETURN; 622 END IF; 623 624 -- actual deletion 625 UPDATE customers SET deleted_at = in_timestamp WHERE customer_id = my_customer_id; 626 END $$; 627 COMMENT ON FUNCTION account_delete IS 'Deletes an account if the balance is zero'; 628 629 CREATE FUNCTION register_incoming( 630 IN in_tx_row_id INT8, 631 IN in_type taler_incoming_type, 632 IN in_metadata BYTEA, 633 IN in_account_id INT8, 634 IN in_authorization_pub BYTEA, 635 IN in_authorization_sig BYTEA 636 ) 637 RETURNS void 638 LANGUAGE plpgsql AS $$ 639 DECLARE 640 local_amount taler_amount; 641 BEGIN 642 -- Register incoming transaction 643 INSERT INTO taler_exchange_incoming ( 644 metadata, 645 bank_transaction, 646 type, 647 authorization_pub, 648 authorization_sig 649 ) VALUES ( 650 in_metadata, 651 in_tx_row_id, 652 in_type, 653 in_authorization_pub, 654 in_authorization_sig 655 ); 656 -- Update stats 657 IF in_type = 'reserve' THEN 658 SELECT (amount).val, (amount).frac 659 INTO local_amount.val, local_amount.frac 660 FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id; 661 CALL stats_register_payment('taler_in', NULL, local_amount, null); 662 END IF; 663 -- Notify new incoming transaction 664 PERFORM pg_notify('bank_incoming_tx', in_account_id || ' ' || in_tx_row_id); 665 END $$; 666 COMMENT ON FUNCTION register_incoming 667 IS 'Register a bank transaction as a taler incoming transaction and announce it'; 668 669 CREATE FUNCTION bounce( 670 IN in_debtor_account_id INT8, 671 IN in_credit_transaction_id INT8, 672 IN in_bounce_cause TEXT, 673 IN in_timestamp INT8 674 ) 675 RETURNS void 676 LANGUAGE plpgsql AS $$ 677 DECLARE 678 local_creditor_account_id INT8; 679 local_amount taler_amount; 680 BEGIN 681 -- Load transaction info 682 SELECT (amount).frac, (amount).val, bank_account_id 683 INTO local_amount.frac, local_amount.val, local_creditor_account_id 684 FROM bank_account_transactions 685 WHERE bank_transaction_id=in_credit_transaction_id; 686 687 -- No error can happens because an opposite transaction already took place in the same transaction 688 PERFORM bank_wire_transfer( 689 in_debtor_account_id, 690 local_creditor_account_id, 691 'Bounce ' || in_credit_transaction_id || ': ' || in_bounce_cause, 692 local_amount, 693 in_timestamp, 694 NULL, 695 NULL, 696 NULL 697 ); 698 699 -- Delete from pending if any 700 DELETE FROM pending_recurrent_incoming_transactions WHERE bank_transaction_id = in_credit_transaction_id; 701 END$$; 702 703 CREATE FUNCTION make_incoming( 704 IN in_creditor_account_id INT8, 705 IN in_debtor_account_id INT8, 706 IN in_subject TEXT, 707 IN in_amount taler_amount, 708 IN in_timestamp INT8, 709 IN in_type taler_incoming_type, 710 IN in_metadata BYTEA, 711 IN in_wire_transfer_fees taler_amount, 712 IN in_min_amount taler_amount, 713 IN in_max_amount taler_amount, 714 -- Error status 715 OUT out_balance_insufficient BOOLEAN, 716 OUT out_bad_amount BOOLEAN, 717 OUT out_reserve_pub_reuse BOOLEAN, 718 OUT out_mapping_reuse BOOLEAN, 719 OUT out_unknown_mapping BOOLEAN, 720 -- Success return 721 OUT out_pending BOOLEAN, 722 OUT out_credit_row_id INT8, 723 OUT out_debit_row_id INT8 724 ) 725 LANGUAGE plpgsql AS $$ 726 DECLARE 727 local_withdrawal_uuid UUID; 728 local_authorization_pub BYTEA; 729 local_authorization_sig BYTEA; 730 BEGIN 731 out_pending=FALSE; 732 733 -- Resolve mapping logic 734 IF in_type = 'map' THEN 735 SELECT prepared_transfers.type, account_pub, authorization_pub, authorization_sig, withdrawal_uuid, 736 bank_transaction_id IS NOT NULL AND NOT recurrent, 737 bank_transaction_id IS NOT NULL AND recurrent 738 INTO in_type, in_metadata, local_authorization_pub, local_authorization_sig, local_withdrawal_uuid, out_mapping_reuse, out_pending 739 FROM prepared_transfers 740 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id) 741 WHERE authorization_pub = in_metadata; 742 out_unknown_mapping = NOT FOUND; 743 IF out_unknown_mapping OR out_mapping_reuse THEN 744 RETURN; 745 END IF; 746 END IF; 747 748 -- Check reserve pub reuse 749 out_reserve_pub_reuse=in_type = 'reserve' AND NOT out_pending AND EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_metadata AND type = 'reserve'); 750 IF out_reserve_pub_reuse THEN 751 RETURN; 752 END IF; 753 754 -- Perform bank wire transfer 755 SELECT 756 transfer.out_balance_insufficient, 757 transfer.out_bad_amount, 758 transfer.out_credit_row_id, 759 transfer.out_debit_row_id 760 INTO 761 out_balance_insufficient, 762 out_bad_amount, 763 out_credit_row_id, 764 out_debit_row_id 765 FROM bank_wire_transfer( 766 in_creditor_account_id, 767 in_debtor_account_id, 768 in_subject, 769 in_amount, 770 in_timestamp, 771 in_wire_transfer_fees, 772 in_min_amount, 773 in_max_amount 774 ) as transfer; 775 IF out_balance_insufficient OR out_bad_amount THEN 776 RETURN; 777 END IF; 778 779 IF out_pending THEN 780 -- Delay talerable registration until mapping again 781 INSERT INTO pending_recurrent_incoming_transactions (bank_transaction_id, debtor_account_id, authorization_pub) 782 VALUES (out_credit_row_id, in_debtor_account_id, local_authorization_pub); 783 ELSE 784 UPDATE prepared_transfers 785 SET bank_transaction_id = out_credit_row_id 786 WHERE (bank_transaction_id IS NULL AND account_pub = in_metadata) OR authorization_pub = local_authorization_pub; 787 IF local_withdrawal_uuid IS NOT NULL THEN 788 PERFORM abort_taler_withdrawal(local_withdrawal_uuid); 789 END IF; 790 PERFORM register_incoming(out_credit_row_id, in_type, in_metadata, in_creditor_account_id, local_authorization_pub, local_authorization_sig); 791 END IF; 792 END $$; 793 794 795 CREATE FUNCTION taler_transfer( 796 IN in_request_uid BYTEA, 797 IN in_wtid BYTEA, 798 IN in_subject TEXT, 799 IN in_amount taler_amount, 800 IN in_exchange_base_url TEXT, 801 IN in_metadata TEXT, 802 IN in_credit_account_payto TEXT, 803 IN in_username TEXT, 804 IN in_timestamp INT8, 805 IN in_conversion BOOLEAN, 806 -- Error status 807 OUT out_debtor_not_found BOOLEAN, 808 OUT out_debtor_not_exchange BOOLEAN, 809 OUT out_both_exchanges BOOLEAN, 810 OUT out_creditor_admin BOOLEAN, 811 OUT out_request_uid_reuse BOOLEAN, 812 OUT out_wtid_reuse BOOLEAN, 813 OUT out_exchange_balance_insufficient BOOLEAN, 814 -- Success return 815 OUT out_tx_row_id INT8, 816 OUT out_timestamp INT8 817 ) 818 LANGUAGE plpgsql AS $$ 819 DECLARE 820 exchange_account_id INT8; 821 creditor_account_id INT8; 822 account_conversion_rate_class_id INT8; 823 creditor_name TEXT; 824 creditor_admin BOOLEAN; 825 credit_row_id INT8; 826 debit_row_id INT8; 827 outgoing_id INT8; 828 bounce_tx INT8; 829 bounce_amount taler_amount; 830 BEGIN 831 -- Check for idempotence and conflict 832 SELECT (amount != in_amount 833 OR creditor_payto != in_credit_account_payto 834 OR exchange_base_url != in_exchange_base_url 835 OR metadata != in_metadata 836 OR wtid != in_wtid) 837 ,transfer_operation_id, transfer_date 838 INTO out_request_uid_reuse, out_tx_row_id, out_timestamp 839 FROM transfer_operations 840 WHERE request_uid = in_request_uid; 841 IF found THEN 842 RETURN; 843 END IF; 844 out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid); 845 IF out_wtid_reuse THEN 846 RETURN; 847 END IF; 848 out_timestamp=in_timestamp; 849 -- Find exchange bank account id 850 SELECT 851 bank_account_id, NOT is_taler_exchange, conversion_rate_class_id 852 INTO exchange_account_id, out_debtor_not_exchange, account_conversion_rate_class_id 853 FROM bank_accounts 854 JOIN customers 855 ON customer_id=owning_customer_id 856 WHERE username = in_username AND deleted_at IS NULL; 857 out_debtor_not_found=NOT FOUND; 858 IF out_debtor_not_found OR out_debtor_not_exchange THEN 859 RETURN; 860 END IF; 861 -- Find creditor bank account id 862 SELECT 863 bank_account_id, is_taler_exchange, username = 'admin' 864 INTO creditor_account_id, out_both_exchanges, creditor_admin 865 FROM bank_accounts 866 JOIN customers ON owning_customer_id=customer_id 867 WHERE internal_payto = in_credit_account_payto; 868 IF NOT FOUND THEN 869 -- Register failure 870 INSERT INTO transfer_operations ( 871 request_uid, 872 wtid, 873 amount, 874 exchange_base_url, 875 metadata, 876 transfer_date, 877 exchange_outgoing_id, 878 creditor_payto, 879 status, 880 status_msg, 881 exchange_id 882 ) VALUES ( 883 in_request_uid, 884 in_wtid, 885 in_amount, 886 in_exchange_base_url, 887 in_metadata, 888 in_timestamp, 889 NULL, 890 in_credit_account_payto, 891 'permanent_failure', 892 'Unknown account', 893 exchange_account_id 894 ) RETURNING transfer_operation_id INTO out_tx_row_id; 895 RETURN; 896 ELSIF out_both_exchanges THEN 897 RETURN; 898 END IF; 899 900 IF creditor_admin THEN 901 -- Check if this is a conversion bounce 902 IF NOT in_conversion THEN 903 out_creditor_admin=TRUE; 904 RETURN; 905 END IF; 906 907 -- Find the bounced transaction 908 SELECT (amount).val, (amount).frac, incoming_transaction_id 909 INTO bounce_amount.val, bounce_amount.frac, bounce_tx 910 FROM libeufin_nexus.incoming_transactions 911 JOIN libeufin_nexus.talerable_incoming_transactions USING (incoming_transaction_id) 912 WHERE metadata=in_wtid AND type='reserve'; 913 IF NOT FOUND THEN 914 -- Register failure 915 INSERT INTO transfer_operations ( 916 request_uid, 917 wtid, 918 amount, 919 exchange_base_url, 920 metadata, 921 transfer_date, 922 exchange_outgoing_id, 923 creditor_payto, 924 status, 925 status_msg, 926 exchange_id 927 ) VALUES ( 928 in_request_uid, 929 in_wtid, 930 in_amount, 931 in_exchange_base_url, 932 in_metadata, 933 in_timestamp, 934 NULL, 935 in_credit_account_payto, 936 'permanent_failure', 937 'Unknown bounced transaction', 938 exchange_account_id 939 ) RETURNING transfer_operation_id INTO out_tx_row_id; 940 RETURN; 941 END IF; 942 943 -- Bounce the transaction 944 PERFORM libeufin_nexus.bounce_incoming( 945 bounce_tx 946 ,((bounce_amount).val, (bounce_amount).frac)::libeufin_nexus.taler_amount 947 ,libeufin_nexus.ebics_id_gen() 948 ,in_timestamp 949 ,'exchange bounced' 950 ); 951 END IF; 952 -- Perform bank transfer 953 SELECT 954 out_balance_insufficient, 955 out_debit_row_id, out_credit_row_id 956 INTO 957 out_exchange_balance_insufficient, 958 debit_row_id, credit_row_id 959 FROM bank_wire_transfer( 960 creditor_account_id, 961 exchange_account_id, 962 in_subject, 963 in_amount, 964 in_timestamp, 965 NULL, 966 NULL, 967 NULL 968 ); 969 IF out_exchange_balance_insufficient THEN 970 RETURN; 971 END IF; 972 -- Register outgoing transaction 973 INSERT INTO taler_exchange_outgoing ( 974 bank_transaction 975 ) VALUES ( 976 debit_row_id 977 ) RETURNING exchange_outgoing_id INTO outgoing_id; 978 -- Update stats 979 CALL stats_register_payment('taler_out', NULL, in_amount, null); 980 -- Register success 981 INSERT INTO transfer_operations ( 982 request_uid, 983 wtid, 984 amount, 985 exchange_base_url, 986 metadata, 987 transfer_date, 988 exchange_outgoing_id, 989 creditor_payto, 990 status, 991 status_msg, 992 exchange_id 993 ) VALUES ( 994 in_request_uid, 995 in_wtid, 996 in_amount, 997 in_exchange_base_url, 998 in_metadata, 999 in_timestamp, 1000 outgoing_id, 1001 in_credit_account_payto, 1002 'success', 1003 NULL, 1004 exchange_account_id 1005 ) RETURNING transfer_operation_id INTO out_tx_row_id; 1006 1007 -- Notify new transaction 1008 PERFORM pg_notify('bank_outgoing_tx', exchange_account_id || ' ' || creditor_account_id || ' ' || debit_row_id || ' ' || credit_row_id); 1009 1010 IF creditor_admin THEN 1011 -- Create cashout operation 1012 INSERT INTO cashout_operations ( 1013 request_uid 1014 ,amount_debit 1015 ,amount_credit 1016 ,creation_time 1017 ,bank_account 1018 ,subject 1019 ,local_transaction 1020 ) VALUES ( 1021 NULL 1022 ,in_amount 1023 ,bounce_amount 1024 ,in_timestamp 1025 ,exchange_account_id 1026 ,in_subject 1027 ,debit_row_id 1028 ); 1029 1030 -- update stats 1031 CALL stats_register_payment('cashout', NULL, in_amount, bounce_amount); 1032 END IF; 1033 END $$; 1034 COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it'; 1035 1036 CREATE FUNCTION taler_add_incoming( 1037 IN in_key BYTEA, 1038 IN in_subject TEXT, 1039 IN in_amount taler_amount, 1040 IN in_debit_account_payto TEXT, 1041 IN in_username TEXT, 1042 IN in_timestamp INT8, 1043 IN in_type taler_incoming_type, 1044 -- Error status 1045 OUT out_creditor_not_found BOOLEAN, 1046 OUT out_creditor_not_exchange BOOLEAN, 1047 OUT out_debtor_not_found BOOLEAN, 1048 OUT out_both_exchanges BOOLEAN, 1049 OUT out_reserve_pub_reuse BOOLEAN, 1050 OUT out_mapping_reuse BOOLEAN, 1051 OUT out_unknown_mapping BOOLEAN, 1052 OUT out_debitor_balance_insufficient BOOLEAN, 1053 -- Success return 1054 OUT out_tx_row_id INT8, 1055 OUT out_pending INT8 1056 ) 1057 LANGUAGE plpgsql AS $$ 1058 DECLARE 1059 exchange_bank_account_id INT8; 1060 sender_bank_account_id INT8; 1061 BEGIN 1062 -- Find exchange bank account id 1063 SELECT 1064 bank_account_id, NOT is_taler_exchange 1065 INTO exchange_bank_account_id, out_creditor_not_exchange 1066 FROM bank_accounts 1067 JOIN customers 1068 ON customer_id=owning_customer_id 1069 WHERE username = in_username AND deleted_at IS NULL; 1070 IF NOT FOUND OR out_creditor_not_exchange THEN 1071 out_creditor_not_found=NOT FOUND; 1072 RETURN; 1073 END IF; 1074 -- Find sender bank account id 1075 SELECT 1076 bank_account_id, is_taler_exchange 1077 INTO sender_bank_account_id, out_both_exchanges 1078 FROM bank_accounts 1079 WHERE internal_payto = in_debit_account_payto; 1080 IF NOT FOUND OR out_both_exchanges THEN 1081 out_debtor_not_found=NOT FOUND; 1082 RETURN; 1083 END IF; 1084 -- Perform bank transfer 1085 SELECT 1086 out_balance_insufficient, 1087 out_credit_row_id, 1088 t.out_reserve_pub_reuse, 1089 t.out_mapping_reuse, 1090 t.out_unknown_mapping 1091 INTO 1092 out_debitor_balance_insufficient, 1093 out_tx_row_id, 1094 out_reserve_pub_reuse, 1095 out_mapping_reuse, 1096 out_unknown_mapping 1097 FROM make_incoming( 1098 exchange_bank_account_id, 1099 sender_bank_account_id, 1100 in_subject, 1101 in_amount, 1102 in_timestamp, 1103 in_type, 1104 in_key, 1105 NULL, 1106 NULL, 1107 NULL 1108 ) as t; 1109 END $$; 1110 COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler transaction and register it'; 1111 1112 CREATE FUNCTION bank_transaction( 1113 IN in_credit_account_payto TEXT, 1114 IN in_debit_account_username TEXT, 1115 IN in_subject TEXT, 1116 IN in_amount taler_amount, 1117 IN in_timestamp INT8, 1118 IN in_is_tan BOOLEAN, 1119 IN in_request_uid BYTEA, 1120 IN in_wire_transfer_fees taler_amount, 1121 IN in_min_amount taler_amount, 1122 IN in_max_amount taler_amount, 1123 IN in_type taler_incoming_type, 1124 IN in_metadata BYTEA, 1125 IN in_bounce_cause TEXT, 1126 -- Error status 1127 OUT out_creditor_not_found BOOLEAN, 1128 OUT out_debtor_not_found BOOLEAN, 1129 OUT out_same_account BOOLEAN, 1130 OUT out_balance_insufficient BOOLEAN, 1131 OUT out_creditor_admin BOOLEAN, 1132 OUT out_tan_required BOOLEAN, 1133 OUT out_request_uid_reuse BOOLEAN, 1134 OUT out_bad_amount BOOLEAN, 1135 -- Success return 1136 OUT out_credit_bank_account_id INT8, 1137 OUT out_debit_bank_account_id INT8, 1138 OUT out_credit_row_id INT8, 1139 OUT out_debit_row_id INT8, 1140 OUT out_creditor_is_exchange BOOLEAN, 1141 OUT out_debtor_is_exchange BOOLEAN, 1142 OUT out_idempotent BOOLEAN 1143 ) 1144 LANGUAGE plpgsql AS $$ 1145 DECLARE 1146 local_reserve_pub_reuse BOOLEAN; 1147 local_mapping_reuse BOOLEAN; 1148 local_unknown_mapping BOOLEAN; 1149 BEGIN 1150 -- Find credit bank account id and check it's not admin 1151 SELECT bank_account_id, is_taler_exchange, username='admin' 1152 INTO out_credit_bank_account_id, out_creditor_is_exchange, out_creditor_admin 1153 FROM bank_accounts 1154 JOIN customers ON customer_id=owning_customer_id 1155 WHERE internal_payto = in_credit_account_payto AND deleted_at IS NULL; 1156 IF NOT FOUND OR out_creditor_admin THEN 1157 out_creditor_not_found=NOT FOUND; 1158 RETURN; 1159 END IF; 1160 -- Find debit bank account ID and check it's a different account and if 2FA is required 1161 SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0 1162 INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required 1163 FROM bank_accounts 1164 JOIN customers ON customer_id=owning_customer_id 1165 WHERE username = in_debit_account_username AND deleted_at IS NULL; 1166 IF NOT FOUND OR out_same_account THEN 1167 out_debtor_not_found=NOT FOUND; 1168 RETURN; 1169 END IF; 1170 -- Check for idempotence and conflict 1171 IF in_request_uid IS NOT NULL THEN 1172 SELECT (amount != in_amount 1173 OR subject != in_subject 1174 OR bank_account_id != out_debit_bank_account_id), bank_transaction 1175 INTO out_request_uid_reuse, out_debit_row_id 1176 FROM bank_transaction_operations 1177 JOIN bank_account_transactions ON bank_transaction = bank_transaction_id 1178 WHERE request_uid = in_request_uid; 1179 IF found OR out_tan_required THEN 1180 out_idempotent = found AND NOT out_request_uid_reuse; 1181 RETURN; 1182 END IF; 1183 ELSIF out_tan_required THEN 1184 RETURN; 1185 END IF; 1186 1187 -- Try to perform an incoming transfer 1188 IF out_creditor_is_exchange AND NOT out_debtor_is_exchange AND in_bounce_cause IS NULL THEN 1189 -- Perform an incoming transfer 1190 SELECT 1191 transfer.out_balance_insufficient, 1192 transfer.out_bad_amount, 1193 transfer.out_credit_row_id, 1194 transfer.out_debit_row_id, 1195 out_reserve_pub_reuse, 1196 out_mapping_reuse, 1197 out_unknown_mapping 1198 INTO 1199 out_balance_insufficient, 1200 out_bad_amount, 1201 out_credit_row_id, 1202 out_debit_row_id, 1203 local_reserve_pub_reuse, 1204 local_mapping_reuse, 1205 local_unknown_mapping 1206 FROM make_incoming( 1207 out_credit_bank_account_id, 1208 out_debit_bank_account_id, 1209 in_subject, 1210 in_amount, 1211 in_timestamp, 1212 in_type, 1213 in_metadata, 1214 in_wire_transfer_fees, 1215 in_min_amount, 1216 in_max_amount 1217 ) as transfer; 1218 IF out_balance_insufficient OR out_bad_amount THEN 1219 RETURN; 1220 END IF; 1221 IF local_reserve_pub_reuse THEN 1222 in_bounce_cause = 'reserve public key reuse'; 1223 ELSIF local_mapping_reuse THEN 1224 in_bounce_cause = 'mapping public key reuse'; 1225 ELSIF local_unknown_mapping THEN 1226 in_bounce_cause = 'unknown mapping public key'; 1227 END IF; 1228 END IF; 1229 1230 IF out_credit_row_id IS NULL THEN 1231 -- Perform common bank transfer 1232 SELECT 1233 transfer.out_balance_insufficient, 1234 transfer.out_bad_amount, 1235 transfer.out_credit_row_id, 1236 transfer.out_debit_row_id 1237 INTO 1238 out_balance_insufficient, 1239 out_bad_amount, 1240 out_credit_row_id, 1241 out_debit_row_id 1242 FROM bank_wire_transfer( 1243 out_credit_bank_account_id, 1244 out_debit_bank_account_id, 1245 in_subject, 1246 in_amount, 1247 in_timestamp, 1248 in_wire_transfer_fees, 1249 in_min_amount, 1250 in_max_amount 1251 ) as transfer; 1252 IF out_balance_insufficient OR out_bad_amount THEN 1253 RETURN; 1254 END IF; 1255 END IF; 1256 1257 -- Bounce if necessary 1258 IF out_creditor_is_exchange AND in_bounce_cause IS NOT NULL THEN 1259 PERFORM bounce(out_debit_bank_account_id, out_credit_row_id, in_bounce_cause, in_timestamp); 1260 END IF; 1261 1262 -- Store operation 1263 IF in_request_uid IS NOT NULL THEN 1264 INSERT INTO bank_transaction_operations (request_uid, bank_transaction) 1265 VALUES (in_request_uid, out_debit_row_id); 1266 END IF; 1267 END $$; 1268 COMMENT ON FUNCTION bank_transaction IS 'Create a bank transaction'; 1269 1270 CREATE FUNCTION create_taler_withdrawal( 1271 IN in_account_username TEXT, 1272 IN in_withdrawal_uuid UUID, 1273 IN in_amount taler_amount, 1274 IN in_suggested_amount taler_amount, 1275 IN in_no_amount_to_wallet BOOLEAN, 1276 IN in_timestamp INT8, 1277 IN in_wire_transfer_fees taler_amount, 1278 IN in_min_amount taler_amount, 1279 IN in_max_amount taler_amount, 1280 -- Error status 1281 OUT out_account_not_found BOOLEAN, 1282 OUT out_account_is_exchange BOOLEAN, 1283 OUT out_balance_insufficient BOOLEAN, 1284 OUT out_bad_amount BOOLEAN 1285 ) 1286 LANGUAGE plpgsql AS $$ 1287 DECLARE 1288 account_id INT8; 1289 amount_with_fee taler_amount; 1290 BEGIN 1291 IF in_account_username IS NOT NULL THEN 1292 -- Check account exists 1293 SELECT bank_account_id, is_taler_exchange 1294 INTO account_id, out_account_is_exchange 1295 FROM bank_accounts 1296 JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id 1297 WHERE username=in_account_username AND deleted_at IS NULL; 1298 out_account_not_found=NOT FOUND; 1299 IF out_account_not_found OR out_account_is_exchange THEN 1300 RETURN; 1301 END IF; 1302 1303 -- Check enough funds 1304 IF in_amount IS NOT NULL OR in_suggested_amount IS NOT NULL THEN 1305 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1306 account_id, 1307 COALESCE(in_amount, in_suggested_amount), 1308 in_wire_transfer_fees, 1309 in_min_amount, 1310 in_max_amount 1311 ) AS test INTO out_balance_insufficient, out_bad_amount; 1312 IF out_balance_insufficient OR out_bad_amount THEN 1313 RETURN; 1314 END IF; 1315 END IF; 1316 END IF; 1317 1318 -- Create withdrawal operation 1319 INSERT INTO taler_withdrawal_operations ( 1320 withdrawal_uuid, 1321 wallet_bank_account, 1322 amount, 1323 suggested_amount, 1324 no_amount_to_wallet, 1325 type, 1326 creation_date 1327 ) VALUES ( 1328 in_withdrawal_uuid, 1329 account_id, 1330 in_amount, 1331 in_suggested_amount, 1332 in_no_amount_to_wallet, 1333 'reserve', 1334 in_timestamp 1335 ); 1336 END $$; 1337 COMMENT ON FUNCTION create_taler_withdrawal IS 'Create a new withdrawal operation'; 1338 1339 CREATE FUNCTION select_taler_withdrawal( 1340 IN in_withdrawal_uuid uuid, 1341 IN in_reserve_pub BYTEA, 1342 IN in_subject TEXT, 1343 IN in_selected_exchange_payto TEXT, 1344 IN in_amount taler_amount, 1345 IN in_wire_transfer_fees taler_amount, 1346 IN in_min_amount taler_amount, 1347 IN in_max_amount taler_amount, 1348 -- Error status 1349 OUT out_no_op BOOLEAN, 1350 OUT out_already_selected BOOLEAN, 1351 OUT out_reserve_pub_reuse BOOLEAN, 1352 OUT out_account_not_found BOOLEAN, 1353 OUT out_account_is_not_exchange BOOLEAN, 1354 OUT out_amount_differs BOOLEAN, 1355 OUT out_balance_insufficient BOOLEAN, 1356 OUT out_bad_amount BOOLEAN, 1357 OUT out_aborted BOOLEAN, 1358 -- Success return 1359 OUT out_status TEXT 1360 ) 1361 LANGUAGE plpgsql AS $$ 1362 DECLARE 1363 selected BOOLEAN; 1364 account_id INT8; 1365 exchange_account_id INT8; 1366 amount_with_fee taler_amount; 1367 BEGIN 1368 -- Check exchange account 1369 SELECT bank_account_id, NOT is_taler_exchange 1370 INTO exchange_account_id, out_account_is_not_exchange 1371 FROM bank_accounts 1372 WHERE internal_payto=in_selected_exchange_payto; 1373 out_account_not_found=NOT FOUND; 1374 IF out_account_not_found OR out_account_is_not_exchange THEN 1375 RETURN; 1376 END IF; 1377 1378 -- Check for conflict and idempotence 1379 SELECT 1380 selection_done, 1381 aborted, 1382 CASE 1383 WHEN confirmation_done THEN 'confirmed' 1384 ELSE 'selected' 1385 END, 1386 selection_done 1387 AND (exchange_bank_account != exchange_account_id OR reserve_pub != in_reserve_pub OR amount != in_amount), 1388 amount != in_amount, 1389 wallet_bank_account 1390 INTO selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id 1391 FROM taler_withdrawal_operations 1392 WHERE withdrawal_uuid=in_withdrawal_uuid; 1393 out_no_op = NOT FOUND; 1394 IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs OR selected THEN 1395 RETURN; 1396 END IF; 1397 1398 -- Check reserve_pub reuse 1399 out_reserve_pub_reuse=EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_reserve_pub AND type = 'reserve') OR 1400 EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub AND type = 'reserve'); 1401 IF out_reserve_pub_reuse THEN 1402 RETURN; 1403 END IF; 1404 1405 IF in_amount IS NOT NULL THEN 1406 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1407 account_id, 1408 in_amount, 1409 in_wire_transfer_fees, 1410 in_min_amount, 1411 in_max_amount 1412 ) AS test INTO out_balance_insufficient, out_bad_amount; 1413 IF out_balance_insufficient OR out_bad_amount THEN 1414 RETURN; 1415 END IF; 1416 END IF; 1417 1418 -- Update withdrawal operation 1419 UPDATE taler_withdrawal_operations 1420 SET exchange_bank_account=exchange_account_id, 1421 reserve_pub=in_reserve_pub, 1422 subject=in_subject, 1423 selection_done=true, 1424 amount=COALESCE(amount, in_amount) 1425 WHERE withdrawal_uuid=in_withdrawal_uuid; 1426 1427 -- Notify status change 1428 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected'); 1429 END $$; 1430 COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation'; 1431 1432 CREATE FUNCTION abort_taler_withdrawal( 1433 IN in_withdrawal_uuid uuid, 1434 OUT out_no_op BOOLEAN, 1435 OUT out_already_confirmed BOOLEAN 1436 ) 1437 LANGUAGE plpgsql AS $$ 1438 BEGIN 1439 UPDATE taler_withdrawal_operations 1440 SET aborted = NOT confirmation_done 1441 WHERE withdrawal_uuid=in_withdrawal_uuid 1442 RETURNING confirmation_done 1443 INTO out_already_confirmed; 1444 IF NOT FOUND OR out_already_confirmed THEN 1445 out_no_op=NOT FOUND; 1446 RETURN; 1447 END IF; 1448 1449 -- Notify status change 1450 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted'); 1451 END $$; 1452 COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.'; 1453 1454 CREATE FUNCTION confirm_taler_withdrawal( 1455 IN in_username TEXT, 1456 IN in_withdrawal_uuid uuid, 1457 IN in_timestamp INT8, 1458 IN in_is_tan BOOLEAN, 1459 IN in_wire_transfer_fees taler_amount, 1460 IN in_min_amount taler_amount, 1461 IN in_max_amount taler_amount, 1462 IN in_amount taler_amount, 1463 OUT out_no_op BOOLEAN, 1464 OUT out_balance_insufficient BOOLEAN, 1465 OUT out_reserve_pub_reuse BOOLEAN, 1466 OUT out_bad_amount BOOLEAN, 1467 OUT out_creditor_not_found BOOLEAN, 1468 OUT out_not_selected BOOLEAN, 1469 OUT out_missing_amount BOOLEAN, 1470 OUT out_amount_differs BOOLEAN, 1471 OUT out_aborted BOOLEAN, 1472 OUT out_tan_required BOOLEAN 1473 ) 1474 LANGUAGE plpgsql AS $$ 1475 DECLARE 1476 already_confirmed BOOLEAN; 1477 subject_local TEXT; 1478 reserve_pub_local BYTEA; 1479 wallet_bank_account_local INT8; 1480 amount_local taler_amount; 1481 exchange_bank_account_id INT8; 1482 tx_row_id INT8; 1483 local_type taler_incoming_type; 1484 BEGIN 1485 -- Load account info 1486 SELECT bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0 1487 INTO wallet_bank_account_local, out_tan_required 1488 FROM bank_accounts 1489 JOIN customers ON owning_customer_id=customer_id 1490 WHERE username=in_username AND deleted_at IS NULL; 1491 1492 -- Check op exists and conflict 1493 SELECT 1494 confirmation_done, 1495 aborted, NOT selection_done, 1496 reserve_pub, subject, type, 1497 exchange_bank_account, 1498 (amount).val, (amount).frac, 1499 amount IS NULL AND in_amount IS NULL, 1500 amount != in_amount 1501 INTO 1502 already_confirmed, 1503 out_aborted, out_not_selected, 1504 reserve_pub_local, subject_local, local_type, 1505 exchange_bank_account_id, 1506 amount_local.val, amount_local.frac, 1507 out_missing_amount, 1508 out_amount_differs 1509 FROM taler_withdrawal_operations 1510 WHERE withdrawal_uuid=in_withdrawal_uuid; 1511 out_no_op=NOT FOUND; 1512 IF out_no_op OR already_confirmed OR out_aborted OR out_not_selected OR out_missing_amount OR out_amount_differs OR out_tan_required THEN 1513 RETURN; 1514 ELSIF in_amount IS NOT NULL THEN 1515 amount_local = in_amount; 1516 END IF; 1517 1518 SELECT -- not checking for accounts existence, as it was done above. 1519 transfer.out_balance_insufficient, 1520 transfer.out_bad_amount, 1521 transfer.out_reserve_pub_reuse 1522 out_credit_row_id 1523 INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse, tx_row_id 1524 FROM make_incoming( 1525 exchange_bank_account_id, 1526 wallet_bank_account_local, 1527 subject_local, 1528 amount_local, 1529 in_timestamp, 1530 local_type, 1531 reserve_pub_local, 1532 in_wire_transfer_fees, 1533 in_min_amount, 1534 in_max_amount 1535 ) as transfer; 1536 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN 1537 RETURN; 1538 END IF; 1539 1540 -- Confirm operation and update amount 1541 UPDATE taler_withdrawal_operations 1542 SET amount=amount_local, confirmation_done=true 1543 WHERE withdrawal_uuid=in_withdrawal_uuid; 1544 1545 -- Notify status change 1546 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed'); 1547 END $$; 1548 COMMENT ON FUNCTION confirm_taler_withdrawal 1549 IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.'; 1550 1551 CREATE FUNCTION cashin( 1552 IN in_timestamp INT8, 1553 IN in_reserve_pub BYTEA, 1554 IN in_amount taler_amount, 1555 IN in_subject TEXT, 1556 -- Error status 1557 OUT out_no_account BOOLEAN, 1558 OUT out_too_small BOOLEAN, 1559 OUT out_balance_insufficient BOOLEAN 1560 ) 1561 LANGUAGE plpgsql AS $$ 1562 DECLARE 1563 converted_amount taler_amount; 1564 admin_account_id INT8; 1565 exchange_account_id INT8; 1566 exchange_conversion_rate_class_id INT8; 1567 tx_row_id INT8; 1568 BEGIN 1569 -- TODO check reserve_pub reuse ? 1570 1571 -- Recover exchange account info 1572 SELECT bank_account_id, conversion_rate_class_id 1573 INTO exchange_account_id, exchange_conversion_rate_class_id 1574 FROM bank_accounts 1575 JOIN customers 1576 ON customer_id=owning_customer_id 1577 WHERE username = 'exchange'; 1578 IF NOT FOUND THEN 1579 out_no_account = true; 1580 RETURN; 1581 END IF; 1582 1583 -- Retrieve admin account id 1584 SELECT bank_account_id 1585 INTO admin_account_id 1586 FROM bank_accounts 1587 JOIN customers 1588 ON customer_id=owning_customer_id 1589 WHERE username = 'admin'; 1590 1591 -- Perform conversion 1592 SELECT (converted).val, (converted).frac, too_small 1593 INTO converted_amount.val, converted_amount.frac, out_too_small 1594 FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id); 1595 IF out_too_small THEN 1596 RETURN; 1597 END IF; 1598 1599 -- Perform incoming transaction 1600 SELECT 1601 transfer.out_balance_insufficient, 1602 transfer.out_credit_row_id 1603 INTO 1604 out_balance_insufficient, 1605 tx_row_id 1606 FROM make_incoming( 1607 exchange_account_id, 1608 admin_account_id, 1609 in_subject, 1610 converted_amount, 1611 in_timestamp, 1612 'reserve'::taler_incoming_type, 1613 in_reserve_pub, 1614 NULL, 1615 NULL, 1616 NULL 1617 ) as transfer; 1618 IF out_balance_insufficient THEN 1619 RETURN; 1620 END IF; 1621 1622 -- update stats 1623 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount); 1624 1625 END $$; 1626 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation'; 1627 1628 1629 CREATE FUNCTION cashout_create( 1630 IN in_username TEXT, 1631 IN in_request_uid BYTEA, 1632 IN in_amount_debit taler_amount, 1633 IN in_amount_credit taler_amount, 1634 IN in_subject TEXT, 1635 IN in_timestamp INT8, 1636 IN in_is_tan BOOLEAN, 1637 -- Error status 1638 OUT out_bad_conversion BOOLEAN, 1639 OUT out_account_not_found BOOLEAN, 1640 OUT out_account_is_exchange BOOLEAN, 1641 OUT out_balance_insufficient BOOLEAN, 1642 OUT out_request_uid_reuse BOOLEAN, 1643 OUT out_no_cashout_payto BOOLEAN, 1644 OUT out_tan_required BOOLEAN, 1645 OUT out_under_min BOOLEAN, 1646 -- Success return 1647 OUT out_cashout_id INT8 1648 ) 1649 LANGUAGE plpgsql AS $$ 1650 DECLARE 1651 account_id INT8; 1652 account_conversion_rate_class_id INT8; 1653 account_cashout_payto TEXT; 1654 admin_account_id INT8; 1655 tx_id INT8; 1656 BEGIN 1657 1658 -- Check account exists, has all info and if 2FA is required 1659 SELECT 1660 bank_account_id, is_taler_exchange, conversion_rate_class_id, 1661 -- Remove potential residual query string an add the receiver_name 1662 split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name), 1663 NOT in_is_tan AND cardinality(tan_channels) > 0 1664 INTO 1665 account_id, out_account_is_exchange, account_conversion_rate_class_id, 1666 account_cashout_payto, out_tan_required 1667 FROM bank_accounts 1668 JOIN customers ON owning_customer_id=customer_id 1669 WHERE username=in_username; 1670 IF NOT FOUND THEN 1671 out_account_not_found=TRUE; 1672 RETURN; 1673 ELSIF account_cashout_payto IS NULL THEN 1674 out_no_cashout_payto=TRUE; 1675 RETURN; 1676 ELSIF out_account_is_exchange THEN 1677 RETURN; 1678 END IF; 1679 1680 -- check conversion 1681 SELECT under_min, too_small OR in_amount_credit!=converted 1682 INTO out_under_min, out_bad_conversion 1683 FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id); 1684 IF out_bad_conversion THEN 1685 RETURN; 1686 END IF; 1687 1688 -- Retrieve admin account id 1689 SELECT bank_account_id 1690 INTO admin_account_id 1691 FROM bank_accounts 1692 JOIN customers 1693 ON customer_id=owning_customer_id 1694 WHERE username = 'admin'; 1695 1696 -- Check for idempotence and conflict 1697 SELECT (amount_debit != in_amount_debit 1698 OR subject != in_subject 1699 OR bank_account != account_id) 1700 , cashout_id 1701 INTO out_request_uid_reuse, out_cashout_id 1702 FROM cashout_operations 1703 WHERE request_uid = in_request_uid; 1704 IF found OR out_request_uid_reuse OR out_tan_required THEN 1705 RETURN; 1706 END IF; 1707 1708 -- Perform bank wire transfer 1709 SELECT transfer.out_balance_insufficient, out_debit_row_id 1710 INTO out_balance_insufficient, tx_id 1711 FROM bank_wire_transfer( 1712 admin_account_id, 1713 account_id, 1714 in_subject, 1715 in_amount_debit, 1716 in_timestamp, 1717 NULL, 1718 NULL, 1719 NULL 1720 ) as transfer; 1721 IF out_balance_insufficient THEN 1722 RETURN; 1723 END IF; 1724 1725 -- Create cashout operation 1726 INSERT INTO cashout_operations ( 1727 request_uid 1728 ,amount_debit 1729 ,amount_credit 1730 ,creation_time 1731 ,bank_account 1732 ,subject 1733 ,local_transaction 1734 ) VALUES ( 1735 in_request_uid 1736 ,in_amount_debit 1737 ,in_amount_credit 1738 ,in_timestamp 1739 ,account_id 1740 ,in_subject 1741 ,tx_id 1742 ) RETURNING cashout_id INTO out_cashout_id; 1743 1744 -- Initiate libeufin-nexus transaction 1745 INSERT INTO libeufin_nexus.initiated_outgoing_transactions ( 1746 amount 1747 ,subject 1748 ,credit_payto 1749 ,initiation_time 1750 ,end_to_end_id 1751 ) VALUES ( 1752 ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount 1753 ,in_subject 1754 ,account_cashout_payto 1755 ,in_timestamp 1756 ,libeufin_nexus.ebics_id_gen() 1757 ); 1758 1759 -- update stats 1760 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit); 1761 END $$; 1762 1763 CREATE FUNCTION tan_challenge_mark_sent ( 1764 IN in_uuid UUID, 1765 IN in_timestamp INT8, 1766 IN in_retransmission_period INT8 1767 ) RETURNS void 1768 LANGUAGE sql AS $$ 1769 UPDATE tan_challenges SET 1770 retransmission_date = in_timestamp + in_retransmission_period 1771 WHERE uuid = in_uuid; 1772 $$; 1773 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent'; 1774 1775 CREATE FUNCTION tan_challenge_try ( 1776 IN in_uuid UUID, 1777 IN in_code TEXT, 1778 IN in_timestamp INT8, 1779 -- Error status 1780 OUT out_ok BOOLEAN, 1781 OUT out_no_op BOOLEAN, 1782 OUT out_no_retry BOOLEAN, 1783 OUT out_expired BOOLEAN, 1784 -- Success return 1785 OUT out_op op_enum, 1786 OUT out_channel tan_enum, 1787 OUT out_info TEXT 1788 ) 1789 LANGUAGE plpgsql as $$ 1790 DECLARE 1791 account_id INT8; 1792 token_creation BOOLEAN; 1793 BEGIN 1794 1795 -- Try to solve challenge 1796 UPDATE tan_challenges SET 1797 confirmation_date = CASE 1798 WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp 1799 ELSE confirmation_date 1800 END, 1801 retry_counter = retry_counter - 1 1802 WHERE uuid = in_uuid 1803 RETURNING 1804 confirmation_date IS NOT NULL, 1805 retry_counter <= 0 AND confirmation_date IS NULL, 1806 in_timestamp >= expiration_date AND confirmation_date IS NULL, 1807 op = 'create_token', 1808 customer 1809 INTO out_ok, out_no_retry, out_expired, token_creation, account_id; 1810 out_no_op = NOT FOUND; 1811 1812 IF NOT out_ok AND token_creation THEN 1813 UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id; 1814 END IF; 1815 1816 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN 1817 RETURN; 1818 END IF; 1819 1820 -- Recover body and op from challenge 1821 SELECT op, tan_channel, tan_info 1822 INTO out_op, out_channel, out_info 1823 FROM tan_challenges WHERE uuid = in_uuid; 1824 END $$; 1825 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed'; 1826 1827 CREATE FUNCTION stats_get_frame( 1828 IN date TIMESTAMP, 1829 IN in_timeframe stat_timeframe_enum, 1830 OUT cashin_count INT8, 1831 OUT cashin_regional_volume taler_amount, 1832 OUT cashin_fiat_volume taler_amount, 1833 OUT cashout_count INT8, 1834 OUT cashout_regional_volume taler_amount, 1835 OUT cashout_fiat_volume taler_amount, 1836 OUT taler_in_count INT8, 1837 OUT taler_in_volume taler_amount, 1838 OUT taler_out_count INT8, 1839 OUT taler_out_volume taler_amount 1840 ) 1841 LANGUAGE plpgsql AS $$ 1842 BEGIN 1843 date = date_trunc(in_timeframe::text, date); 1844 SELECT 1845 s.cashin_count 1846 ,(s.cashin_regional_volume).val 1847 ,(s.cashin_regional_volume).frac 1848 ,(s.cashin_fiat_volume).val 1849 ,(s.cashin_fiat_volume).frac 1850 ,s.cashout_count 1851 ,(s.cashout_regional_volume).val 1852 ,(s.cashout_regional_volume).frac 1853 ,(s.cashout_fiat_volume).val 1854 ,(s.cashout_fiat_volume).frac 1855 ,s.taler_in_count 1856 ,(s.taler_in_volume).val 1857 ,(s.taler_in_volume).frac 1858 ,s.taler_out_count 1859 ,(s.taler_out_volume).val 1860 ,(s.taler_out_volume).frac 1861 INTO 1862 cashin_count 1863 ,cashin_regional_volume.val 1864 ,cashin_regional_volume.frac 1865 ,cashin_fiat_volume.val 1866 ,cashin_fiat_volume.frac 1867 ,cashout_count 1868 ,cashout_regional_volume.val 1869 ,cashout_regional_volume.frac 1870 ,cashout_fiat_volume.val 1871 ,cashout_fiat_volume.frac 1872 ,taler_in_count 1873 ,taler_in_volume.val 1874 ,taler_in_volume.frac 1875 ,taler_out_count 1876 ,taler_out_volume.val 1877 ,taler_out_volume.frac 1878 FROM bank_stats AS s 1879 WHERE s.timeframe = in_timeframe 1880 AND s.start_time = date; 1881 END $$; 1882 1883 CREATE PROCEDURE stats_register_payment( 1884 IN name TEXT, 1885 IN now TIMESTAMP, 1886 IN regional_amount taler_amount, 1887 IN fiat_amount taler_amount 1888 ) 1889 LANGUAGE plpgsql AS $$ 1890 BEGIN 1891 IF now IS NULL THEN 1892 now = timezone('utc', now())::TIMESTAMP; 1893 END IF; 1894 IF name = 'taler_in' THEN 1895 INSERT INTO bank_stats AS s ( 1896 timeframe, 1897 start_time, 1898 taler_in_count, 1899 taler_in_volume 1900 ) SELECT 1901 frame, 1902 date_trunc(frame::text, now), 1903 1, 1904 regional_amount 1905 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1906 ON CONFLICT (timeframe, start_time) DO UPDATE 1907 SET taler_in_count=s.taler_in_count+1, 1908 taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount)); 1909 ELSIF name = 'taler_out' THEN 1910 INSERT INTO bank_stats AS s ( 1911 timeframe, 1912 start_time, 1913 taler_out_count, 1914 taler_out_volume 1915 ) SELECT 1916 frame, 1917 date_trunc(frame::text, now), 1918 1, 1919 regional_amount 1920 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1921 ON CONFLICT (timeframe, start_time) DO UPDATE 1922 SET taler_out_count=s.taler_out_count+1, 1923 taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount)); 1924 ELSIF name = 'cashin' THEN 1925 INSERT INTO bank_stats AS s ( 1926 timeframe, 1927 start_time, 1928 cashin_count, 1929 cashin_regional_volume, 1930 cashin_fiat_volume 1931 ) SELECT 1932 frame, 1933 date_trunc(frame::text, now), 1934 1, 1935 regional_amount, 1936 fiat_amount 1937 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1938 ON CONFLICT (timeframe, start_time) DO UPDATE 1939 SET cashin_count=s.cashin_count+1, 1940 cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)), 1941 cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount)); 1942 ELSIF name = 'cashout' THEN 1943 INSERT INTO bank_stats AS s ( 1944 timeframe, 1945 start_time, 1946 cashout_count, 1947 cashout_regional_volume, 1948 cashout_fiat_volume 1949 ) SELECT 1950 frame, 1951 date_trunc(frame::text, now), 1952 1, 1953 regional_amount, 1954 fiat_amount 1955 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1956 ON CONFLICT (timeframe, start_time) DO UPDATE 1957 SET cashout_count=s.cashout_count+1, 1958 cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)), 1959 cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount)); 1960 ELSE 1961 RAISE EXCEPTION 'Unknown stat %', name; 1962 END IF; 1963 END $$; 1964 1965 CREATE FUNCTION conversion_apply_ratio( 1966 IN amount taler_amount 1967 ,IN ratio taler_amount 1968 ,IN fee taler_amount 1969 ,IN tiny taler_amount -- Result is rounded to this amount 1970 ,IN rounding rounding_mode -- With this rounding mode 1971 ,OUT result taler_amount 1972 ,OUT out_too_small BOOLEAN 1973 ) 1974 LANGUAGE plpgsql IMMUTABLE AS $$ 1975 DECLARE 1976 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 1977 tiny_numeric NUMERIC(24); 1978 BEGIN 1979 -- Handle no config case 1980 IF ratio = (0, 0)::taler_amount THEN 1981 out_too_small=TRUE; 1982 RETURN; 1983 END IF; 1984 1985 -- Perform multiplication using big numbers 1986 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 1987 1988 -- Apply fees 1989 amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 1990 IF (sign(amount_numeric) != 1) THEN 1991 out_too_small = TRUE; 1992 result = (0, 0); 1993 RETURN; 1994 END IF; 1995 1996 -- Round to tiny amounts 1997 tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24)); 1998 case rounding 1999 when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 2000 when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric; 2001 when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric; 2002 end case; 2003 2004 -- Extract product parts 2005 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2006 2007 IF (result.val > 1::INT8<<52) THEN 2008 RAISE EXCEPTION 'amount value overflowed'; 2009 END IF; 2010 END $$; 2011 COMMENT ON FUNCTION conversion_apply_ratio 2012 IS 'Apply a ratio to an amount rounding the result to a tiny amount following a rounding mode. It raises an exception when the resulting .val is larger than 2^52'; 2013 2014 CREATE FUNCTION conversion_revert_ratio( 2015 IN amount taler_amount 2016 ,IN ratio taler_amount 2017 ,IN fee taler_amount 2018 ,IN tiny taler_amount -- Result is rounded to this amount 2019 ,IN rounding rounding_mode -- With this rounding mode 2020 ,IN reverse_tiny taler_amount 2021 ,OUT result taler_amount 2022 ,OUT bad_value BOOLEAN 2023 ) 2024 LANGUAGE plpgsql IMMUTABLE AS $$ 2025 DECLARE 2026 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 2027 tiny_numeric NUMERIC(24); 2028 roundtrip BOOLEAN; 2029 BEGIN 2030 -- Handle no config case 2031 IF ratio = (0, 0)::taler_amount THEN 2032 bad_value=TRUE; 2033 RETURN; 2034 END IF; 2035 2036 -- Apply fees 2037 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 2038 2039 -- Perform division using big numbers 2040 amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 2041 2042 -- Round to input digits 2043 tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24)); 2044 amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 2045 2046 -- Extract division parts 2047 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2048 2049 -- Recover potentially lost tiny amount during rounding 2050 -- There must be a clever way to compute this but I am a little limited with math 2051 -- and revert ratio computation is not a hot function so I just use the apply ratio 2052 -- function to be conservative and correct 2053 SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount); 2054 IF NOT roundtrip THEN 2055 amount_numeric = amount_numeric + tiny_numeric; 2056 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2057 END IF; 2058 2059 IF (result.val > 1::INT8<<52) THEN 2060 RAISE EXCEPTION 'amount value overflowed'; 2061 END IF; 2062 END $$; 2063 COMMENT ON FUNCTION conversion_revert_ratio 2064 IS 'Revert the application of a ratio. This function does not always return the smallest possible amount. It raises an exception when the resulting .val is larger than 2^52'; 2065 2066 2067 CREATE FUNCTION conversion_to( 2068 IN amount taler_amount, 2069 IN direction TEXT, 2070 IN conversion_rate_class_id INT8, 2071 OUT converted taler_amount, 2072 OUT too_small BOOLEAN, 2073 OUT under_min BOOLEAN 2074 ) 2075 LANGUAGE plpgsql STABLE AS $$ 2076 DECLARE 2077 at_ratio taler_amount; 2078 out_fee taler_amount; 2079 tiny_amount taler_amount; 2080 min_amount taler_amount; 2081 mode rounding_mode; 2082 BEGIN 2083 -- Load rate 2084 IF direction='cashin' THEN 2085 SELECT 2086 (cashin_ratio).val, (cashin_ratio).frac, 2087 (cashin_fee).val, (cashin_fee).frac, 2088 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2089 (cashin_min_amount).val, (cashin_min_amount).frac, 2090 cashin_rounding_mode 2091 INTO 2092 at_ratio.val, at_ratio.frac, 2093 out_fee.val, out_fee.frac, 2094 tiny_amount.val, tiny_amount.frac, 2095 min_amount.val, min_amount.frac, 2096 mode 2097 FROM get_conversion_class_rate(conversion_rate_class_id); 2098 ELSE 2099 SELECT 2100 (cashout_ratio).val, (cashout_ratio).frac, 2101 (cashout_fee).val, (cashout_fee).frac, 2102 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2103 (cashout_min_amount).val, (cashout_min_amount).frac, 2104 cashout_rounding_mode 2105 INTO 2106 at_ratio.val, at_ratio.frac, 2107 out_fee.val, out_fee.frac, 2108 tiny_amount.val, tiny_amount.frac, 2109 min_amount.val, min_amount.frac, 2110 mode 2111 FROM get_conversion_class_rate(conversion_rate_class_id); 2112 END IF; 2113 2114 -- Check min amount 2115 SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount); 2116 IF too_small THEN 2117 under_min = true; 2118 converted = (0, 0); 2119 RETURN; 2120 END IF; 2121 2122 -- Perform conversion 2123 SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 2124 FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode); 2125 END $$; 2126 2127 CREATE FUNCTION conversion_from( 2128 IN amount taler_amount, 2129 IN direction TEXT, 2130 IN conversion_rate_class_id INT8, 2131 OUT converted taler_amount, 2132 OUT too_small BOOLEAN, 2133 OUT under_min BOOLEAN 2134 ) 2135 LANGUAGE plpgsql STABLE AS $$ 2136 DECLARE 2137 ratio taler_amount; 2138 out_fee taler_amount; 2139 tiny_amount taler_amount; 2140 reverse_tiny_amount taler_amount; 2141 min_amount taler_amount; 2142 mode rounding_mode; 2143 BEGIN 2144 -- Load rate 2145 IF direction='cashin' THEN 2146 SELECT 2147 (cashin_ratio).val, (cashin_ratio).frac, 2148 (cashin_fee).val, (cashin_fee).frac, 2149 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2150 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2151 (cashin_min_amount).val, (cashin_min_amount).frac, 2152 cashin_rounding_mode 2153 INTO 2154 ratio.val, ratio.frac, 2155 out_fee.val, out_fee.frac, 2156 tiny_amount.val, tiny_amount.frac, 2157 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2158 min_amount.val, min_amount.frac, 2159 mode 2160 FROM get_conversion_class_rate(conversion_rate_class_id); 2161 ELSE 2162 SELECT 2163 (cashout_ratio).val, (cashout_ratio).frac, 2164 (cashout_fee).val, (cashout_fee).frac, 2165 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2166 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2167 (cashout_min_amount).val, (cashout_min_amount).frac, 2168 cashout_rounding_mode 2169 INTO 2170 ratio.val, ratio.frac, 2171 out_fee.val, out_fee.frac, 2172 tiny_amount.val, tiny_amount.frac, 2173 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2174 min_amount.val, min_amount.frac, 2175 mode 2176 FROM get_conversion_class_rate(conversion_rate_class_id); 2177 END IF; 2178 2179 -- Perform conversion 2180 SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small 2181 FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount); 2182 IF too_small THEN 2183 RETURN; 2184 END IF; 2185 2186 -- Check min amount 2187 SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount); 2188 IF too_small THEN 2189 under_min = true; 2190 converted = (0, 0); 2191 END IF; 2192 END $$; 2193 2194 CREATE FUNCTION config_get_conversion_rate() 2195 RETURNS TABLE ( 2196 cashin_ratio taler_amount, 2197 cashin_fee taler_amount, 2198 cashin_tiny_amount taler_amount, 2199 cashin_min_amount taler_amount, 2200 cashin_rounding_mode rounding_mode, 2201 cashout_ratio taler_amount, 2202 cashout_fee taler_amount, 2203 cashout_tiny_amount taler_amount, 2204 cashout_min_amount taler_amount, 2205 cashout_rounding_mode rounding_mode 2206 ) 2207 LANGUAGE sql STABLE AS $$ 2208 SELECT 2209 (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount, 2210 (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount, 2211 (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount, 2212 (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount, 2213 (value->'cashin'->>'rounding_mode')::rounding_mode, 2214 (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount, 2215 (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount, 2216 (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount, 2217 (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount, 2218 (value->'cashout'->>'rounding_mode')::rounding_mode 2219 FROM config WHERE key='conversion_rate' 2220 UNION ALL 2221 SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode, 2222 (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode 2223 LIMIT 1 2224 $$; 2225 2226 CREATE FUNCTION get_conversion_class_rate( 2227 IN in_conversion_rate_class_id INT8 2228 ) 2229 RETURNS TABLE ( 2230 cashin_ratio taler_amount, 2231 cashin_fee taler_amount, 2232 cashin_tiny_amount taler_amount, 2233 cashin_min_amount taler_amount, 2234 cashin_rounding_mode rounding_mode, 2235 cashout_ratio taler_amount, 2236 cashout_fee taler_amount, 2237 cashout_tiny_amount taler_amount, 2238 cashout_min_amount taler_amount, 2239 cashout_rounding_mode rounding_mode 2240 ) 2241 LANGUAGE sql STABLE AS $$ 2242 SELECT 2243 COALESCE(class.cashin_ratio, cfg.cashin_ratio), 2244 COALESCE(class.cashin_fee, cfg.cashin_fee), 2245 cashin_tiny_amount, 2246 COALESCE(class.cashin_min_amount, cfg.cashin_min_amount), 2247 COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode), 2248 COALESCE(class.cashout_ratio, cfg.cashout_ratio), 2249 COALESCE(class.cashout_fee, cfg.cashout_fee), 2250 cashout_tiny_amount, 2251 COALESCE(class.cashout_min_amount, cfg.cashout_min_amount), 2252 COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode) 2253 FROM config_get_conversion_rate() as cfg 2254 LEFT JOIN conversion_rate_classes as class 2255 ON (conversion_rate_class_id=in_conversion_rate_class_id) 2256 $$; 2257 2258 CREATE PROCEDURE config_set_conversion_rate( 2259 IN cashin_ratio taler_amount, 2260 IN cashin_fee taler_amount, 2261 IN cashin_tiny_amount taler_amount, 2262 IN cashin_min_amount taler_amount, 2263 IN cashin_rounding_mode rounding_mode, 2264 IN cashout_ratio taler_amount, 2265 IN cashout_fee taler_amount, 2266 IN cashout_tiny_amount taler_amount, 2267 IN cashout_min_amount taler_amount, 2268 IN cashout_rounding_mode rounding_mode 2269 ) 2270 LANGUAGE sql AS $$ 2271 INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object( 2272 'cashin', jsonb_build_object( 2273 'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac), 2274 'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac), 2275 'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac), 2276 'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac), 2277 'rounding_mode', cashin_rounding_mode 2278 ), 2279 'cashout', jsonb_build_object( 2280 'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac), 2281 'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac), 2282 'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac), 2283 'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac), 2284 'rounding_mode', cashout_rounding_mode 2285 ) 2286 )) ON CONFLICT (key) DO UPDATE SET value = excluded.value 2287 $$; 2288 2289 CREATE FUNCTION register_prepared_transfers ( 2290 IN in_exchange_username TEXT, 2291 IN in_type taler_incoming_type, 2292 IN in_account_pub BYTEA, 2293 IN in_authorization_pub BYTEA, 2294 IN in_authorization_sig BYTEA, 2295 IN in_recurrent BOOLEAN, 2296 IN in_amount taler_amount, 2297 IN in_timestamp INT8, 2298 IN in_subject TEXT, 2299 -- Error status 2300 OUT out_unknown_account BOOLEAN, 2301 OUT out_not_exchange BOOLEAN, 2302 OUT out_reserve_pub_reuse BOOLEAN, 2303 -- Success status 2304 OUT out_withdrawal_uuid UUID 2305 ) 2306 LANGUAGE plpgsql AS $$ 2307 DECLARE 2308 local_withdrawal_id INT8; 2309 exchange_account_id INT8; 2310 talerable_tx INT8; 2311 idempotent BOOLEAN; 2312 BEGIN 2313 -- Retrieve exchange account if 2314 SELECT bank_account_id, NOT is_taler_exchange 2315 INTO exchange_account_id, out_not_exchange 2316 FROM bank_accounts 2317 JOIN customers ON customer_id=owning_customer_id 2318 WHERE username = in_exchange_username; 2319 out_unknown_account=NOT FOUND; 2320 if out_unknown_account OR out_not_exchange THEN RETURN; END IF; 2321 2322 -- Check idempotency 2323 SELECT withdrawal_uuid, prepared_transfers.type = in_type 2324 AND account_pub = in_account_pub 2325 AND recurrent = in_recurrent 2326 AND amount = in_amount 2327 INTO out_withdrawal_uuid, idempotent 2328 FROM prepared_transfers 2329 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id) 2330 WHERE authorization_pub = in_authorization_pub; 2331 2332 -- Check idempotency and delay garbage collection 2333 IF FOUND AND idempotent THEN 2334 UPDATE prepared_transfers 2335 SET registered_at=in_timestamp 2336 WHERE authorization_pub=in_authorization_pub; 2337 RETURN; 2338 END IF; 2339 2340 -- Check reserve pub reuse 2341 out_reserve_pub_reuse=in_type = 'reserve' AND ( 2342 EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve') 2343 ); 2344 IF out_reserve_pub_reuse THEN 2345 RETURN; 2346 END IF; 2347 2348 -- Create/replace withdrawal 2349 IF out_withdrawal_uuid IS NOT NULL THEN 2350 PERFORM abort_taler_withdrawal(out_withdrawal_uuid); 2351 END IF; 2352 out_withdrawal_uuid=null; 2353 2354 IF in_recurrent THEN 2355 -- Finalize one pending right now 2356 DELETE FROM pending_recurrent_incoming_transactions 2357 WHERE bank_transaction_id = ( 2358 SELECT bank_transaction_id 2359 FROM pending_recurrent_incoming_transactions 2360 JOIN bank_account_transactions USING (bank_transaction_id) 2361 WHERE authorization_pub = in_authorization_pub 2362 ORDER BY transaction_date ASC 2363 LIMIT 1 2364 ) 2365 RETURNING bank_transaction_id 2366 INTO talerable_tx; 2367 IF FOUND THEN 2368 PERFORM register_incoming(talerable_tx, in_type, in_account_pub, exchange_account_id, in_authorization_pub, in_authorization_sig); 2369 END IF; 2370 ELSE 2371 -- Bounce all pending 2372 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp) 2373 FROM pending_recurrent_incoming_transactions 2374 WHERE authorization_pub = in_authorization_pub; 2375 2376 -- Create withdrawal 2377 INSERT INTO taler_withdrawal_operations ( 2378 withdrawal_uuid, 2379 wallet_bank_account, 2380 amount, 2381 suggested_amount, 2382 no_amount_to_wallet, 2383 exchange_bank_account, 2384 type, 2385 reserve_pub, 2386 subject, 2387 selection_done, 2388 creation_date 2389 ) VALUES ( 2390 gen_random_uuid(), 2391 NULL, 2392 in_amount, 2393 NULL, 2394 true, 2395 exchange_account_id, 2396 'map', 2397 in_account_pub, 2398 in_subject, 2399 true, 2400 in_timestamp 2401 ) RETURNING withdrawal_uuid, withdrawal_id 2402 INTO out_withdrawal_uuid, local_withdrawal_id; 2403 END IF; 2404 2405 -- Upsert registration 2406 INSERT INTO prepared_transfers ( 2407 type, 2408 account_pub, 2409 authorization_pub, 2410 authorization_sig, 2411 recurrent, 2412 registered_at, 2413 bank_transaction_id, 2414 withdrawal_id 2415 ) VALUES ( 2416 in_type, 2417 in_account_pub, 2418 in_authorization_pub, 2419 in_authorization_sig, 2420 in_recurrent, 2421 in_timestamp, 2422 talerable_tx, 2423 local_withdrawal_id 2424 ) ON CONFLICT (authorization_pub) 2425 DO UPDATE SET 2426 type = EXCLUDED.type, 2427 account_pub = EXCLUDED.account_pub, 2428 recurrent = EXCLUDED.recurrent, 2429 registered_at = EXCLUDED.registered_at, 2430 bank_transaction_id = EXCLUDED.bank_transaction_id, 2431 withdrawal_id = EXCLUDED.withdrawal_id, 2432 authorization_sig = EXCLUDED.authorization_sig; 2433 END $$; 2434 2435 CREATE FUNCTION delete_prepared_transfers ( 2436 IN in_authorization_pub BYTEA, 2437 IN in_timestamp INT8, 2438 OUT out_found BOOLEAN 2439 ) 2440 LANGUAGE plpgsql AS $$ 2441 BEGIN 2442 -- Bounce all pending 2443 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp) 2444 FROM pending_recurrent_incoming_transactions 2445 WHERE authorization_pub = in_authorization_pub; 2446 2447 -- Delete registration 2448 DELETE FROM prepared_transfers 2449 WHERE authorization_pub = in_authorization_pub; 2450 out_found = FOUND; 2451 2452 -- TODO abort withdrawal 2453 END $$; 2454 2455 COMMIT;