libeufin-bank-procedures.sql (71643B)
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 IF local_authorization_pub IS NOT NULL THEN 785 UPDATE prepared_transfers 786 SET bank_transaction_id = out_credit_row_id 787 WHERE authorization_pub = local_authorization_pub; 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 transfer.out_reserve_pub_reuse 1089 INTO 1090 out_debitor_balance_insufficient, 1091 out_tx_row_id, 1092 out_reserve_pub_reuse 1093 FROM make_incoming( 1094 exchange_bank_account_id, 1095 sender_bank_account_id, 1096 in_subject, 1097 in_amount, 1098 in_timestamp, 1099 in_type, 1100 in_key, 1101 NULL, 1102 NULL, 1103 NULL 1104 ) as transfer; 1105 IF out_debitor_balance_insufficient THEN 1106 RETURN; 1107 END IF; 1108 END $$; 1109 COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler transaction and register it'; 1110 1111 CREATE FUNCTION bank_transaction( 1112 IN in_credit_account_payto TEXT, 1113 IN in_debit_account_username TEXT, 1114 IN in_subject TEXT, 1115 IN in_amount taler_amount, 1116 IN in_timestamp INT8, 1117 IN in_is_tan BOOLEAN, 1118 IN in_request_uid BYTEA, 1119 IN in_wire_transfer_fees taler_amount, 1120 IN in_min_amount taler_amount, 1121 IN in_max_amount taler_amount, 1122 IN in_type taler_incoming_type, 1123 IN in_metadata BYTEA, 1124 IN in_bounce_cause TEXT, 1125 -- Error status 1126 OUT out_creditor_not_found BOOLEAN, 1127 OUT out_debtor_not_found BOOLEAN, 1128 OUT out_same_account BOOLEAN, 1129 OUT out_balance_insufficient BOOLEAN, 1130 OUT out_creditor_admin BOOLEAN, 1131 OUT out_tan_required BOOLEAN, 1132 OUT out_request_uid_reuse BOOLEAN, 1133 OUT out_bad_amount BOOLEAN, 1134 -- Success return 1135 OUT out_credit_bank_account_id INT8, 1136 OUT out_debit_bank_account_id INT8, 1137 OUT out_credit_row_id INT8, 1138 OUT out_debit_row_id INT8, 1139 OUT out_creditor_is_exchange BOOLEAN, 1140 OUT out_debtor_is_exchange BOOLEAN, 1141 OUT out_idempotent BOOLEAN 1142 ) 1143 LANGUAGE plpgsql AS $$ 1144 DECLARE 1145 local_reserve_pub_reuse BOOLEAN; 1146 local_mapping_reuse BOOLEAN; 1147 local_unknown_mapping BOOLEAN; 1148 BEGIN 1149 -- Find credit bank account id and check it's not admin 1150 SELECT bank_account_id, is_taler_exchange, username='admin' 1151 INTO out_credit_bank_account_id, out_creditor_is_exchange, out_creditor_admin 1152 FROM bank_accounts 1153 JOIN customers ON customer_id=owning_customer_id 1154 WHERE internal_payto = in_credit_account_payto AND deleted_at IS NULL; 1155 IF NOT FOUND OR out_creditor_admin THEN 1156 out_creditor_not_found=NOT FOUND; 1157 RETURN; 1158 END IF; 1159 -- Find debit bank account ID and check it's a different account and if 2FA is required 1160 SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0 1161 INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required 1162 FROM bank_accounts 1163 JOIN customers ON customer_id=owning_customer_id 1164 WHERE username = in_debit_account_username AND deleted_at IS NULL; 1165 IF NOT FOUND OR out_same_account THEN 1166 out_debtor_not_found=NOT FOUND; 1167 RETURN; 1168 END IF; 1169 -- Check for idempotence and conflict 1170 IF in_request_uid IS NOT NULL THEN 1171 SELECT (amount != in_amount 1172 OR subject != in_subject 1173 OR bank_account_id != out_debit_bank_account_id), bank_transaction 1174 INTO out_request_uid_reuse, out_debit_row_id 1175 FROM bank_transaction_operations 1176 JOIN bank_account_transactions ON bank_transaction = bank_transaction_id 1177 WHERE request_uid = in_request_uid; 1178 IF found OR out_tan_required THEN 1179 out_idempotent = found AND NOT out_request_uid_reuse; 1180 RETURN; 1181 END IF; 1182 ELSIF out_tan_required THEN 1183 RETURN; 1184 END IF; 1185 1186 -- Try to perform an incoming transfer 1187 IF out_creditor_is_exchange AND NOT out_debtor_is_exchange AND in_bounce_cause IS NULL THEN 1188 -- Perform an incoming transfer 1189 SELECT 1190 transfer.out_balance_insufficient, 1191 transfer.out_bad_amount, 1192 transfer.out_credit_row_id, 1193 transfer.out_debit_row_id, 1194 out_reserve_pub_reuse, 1195 out_mapping_reuse, 1196 out_unknown_mapping 1197 INTO 1198 out_balance_insufficient, 1199 out_bad_amount, 1200 out_credit_row_id, 1201 out_debit_row_id, 1202 local_reserve_pub_reuse, 1203 local_mapping_reuse, 1204 local_unknown_mapping 1205 FROM make_incoming( 1206 out_credit_bank_account_id, 1207 out_debit_bank_account_id, 1208 in_subject, 1209 in_amount, 1210 in_timestamp, 1211 in_type, 1212 in_metadata, 1213 in_wire_transfer_fees, 1214 in_min_amount, 1215 in_max_amount 1216 ) as transfer; 1217 IF out_balance_insufficient OR out_bad_amount THEN 1218 RETURN; 1219 END IF; 1220 IF local_reserve_pub_reuse THEN 1221 in_bounce_cause = 'reserve public key reuse'; 1222 ELSIF local_mapping_reuse THEN 1223 in_bounce_cause = 'mapping public key reuse'; 1224 ELSIF local_unknown_mapping THEN 1225 in_bounce_cause = 'unknown mapping public key'; 1226 END IF; 1227 END IF; 1228 1229 IF out_credit_row_id IS NULL THEN 1230 -- Perform common bank transfer 1231 SELECT 1232 transfer.out_balance_insufficient, 1233 transfer.out_bad_amount, 1234 transfer.out_credit_row_id, 1235 transfer.out_debit_row_id 1236 INTO 1237 out_balance_insufficient, 1238 out_bad_amount, 1239 out_credit_row_id, 1240 out_debit_row_id 1241 FROM bank_wire_transfer( 1242 out_credit_bank_account_id, 1243 out_debit_bank_account_id, 1244 in_subject, 1245 in_amount, 1246 in_timestamp, 1247 in_wire_transfer_fees, 1248 in_min_amount, 1249 in_max_amount 1250 ) as transfer; 1251 IF out_balance_insufficient OR out_bad_amount THEN 1252 RETURN; 1253 END IF; 1254 END IF; 1255 1256 -- Bounce if necessary 1257 IF out_creditor_is_exchange AND in_bounce_cause IS NOT NULL THEN 1258 PERFORM bounce(out_debit_bank_account_id, out_credit_row_id, in_bounce_cause, in_timestamp); 1259 END IF; 1260 1261 -- Store operation 1262 IF in_request_uid IS NOT NULL THEN 1263 INSERT INTO bank_transaction_operations (request_uid, bank_transaction) 1264 VALUES (in_request_uid, out_debit_row_id); 1265 END IF; 1266 END $$; 1267 COMMENT ON FUNCTION bank_transaction IS 'Create a bank transaction'; 1268 1269 CREATE FUNCTION create_taler_withdrawal( 1270 IN in_account_username TEXT, 1271 IN in_withdrawal_uuid UUID, 1272 IN in_amount taler_amount, 1273 IN in_suggested_amount taler_amount, 1274 IN in_no_amount_to_wallet BOOLEAN, 1275 IN in_timestamp INT8, 1276 IN in_wire_transfer_fees taler_amount, 1277 IN in_min_amount taler_amount, 1278 IN in_max_amount taler_amount, 1279 -- Error status 1280 OUT out_account_not_found BOOLEAN, 1281 OUT out_account_is_exchange BOOLEAN, 1282 OUT out_balance_insufficient BOOLEAN, 1283 OUT out_bad_amount BOOLEAN 1284 ) 1285 LANGUAGE plpgsql AS $$ 1286 DECLARE 1287 account_id INT8; 1288 amount_with_fee taler_amount; 1289 BEGIN 1290 IF in_account_username IS NOT NULL THEN 1291 -- Check account exists 1292 SELECT bank_account_id, is_taler_exchange 1293 INTO account_id, out_account_is_exchange 1294 FROM bank_accounts 1295 JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id 1296 WHERE username=in_account_username AND deleted_at IS NULL; 1297 out_account_not_found=NOT FOUND; 1298 IF out_account_not_found OR out_account_is_exchange THEN 1299 RETURN; 1300 END IF; 1301 1302 -- Check enough funds 1303 IF in_amount IS NOT NULL OR in_suggested_amount IS NOT NULL THEN 1304 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1305 account_id, 1306 COALESCE(in_amount, in_suggested_amount), 1307 in_wire_transfer_fees, 1308 in_min_amount, 1309 in_max_amount 1310 ) AS test INTO out_balance_insufficient, out_bad_amount; 1311 IF out_balance_insufficient OR out_bad_amount THEN 1312 RETURN; 1313 END IF; 1314 END IF; 1315 END IF; 1316 1317 -- Create withdrawal operation 1318 INSERT INTO taler_withdrawal_operations ( 1319 withdrawal_uuid, 1320 wallet_bank_account, 1321 amount, 1322 suggested_amount, 1323 no_amount_to_wallet, 1324 type, 1325 creation_date 1326 ) VALUES ( 1327 in_withdrawal_uuid, 1328 account_id, 1329 in_amount, 1330 in_suggested_amount, 1331 in_no_amount_to_wallet, 1332 'reserve', 1333 in_timestamp 1334 ); 1335 END $$; 1336 COMMENT ON FUNCTION create_taler_withdrawal IS 'Create a new withdrawal operation'; 1337 1338 CREATE FUNCTION select_taler_withdrawal( 1339 IN in_withdrawal_uuid uuid, 1340 IN in_reserve_pub BYTEA, 1341 IN in_subject TEXT, 1342 IN in_selected_exchange_payto TEXT, 1343 IN in_amount taler_amount, 1344 IN in_wire_transfer_fees taler_amount, 1345 IN in_min_amount taler_amount, 1346 IN in_max_amount taler_amount, 1347 -- Error status 1348 OUT out_no_op BOOLEAN, 1349 OUT out_already_selected BOOLEAN, 1350 OUT out_reserve_pub_reuse BOOLEAN, 1351 OUT out_account_not_found BOOLEAN, 1352 OUT out_account_is_not_exchange BOOLEAN, 1353 OUT out_amount_differs BOOLEAN, 1354 OUT out_balance_insufficient BOOLEAN, 1355 OUT out_bad_amount BOOLEAN, 1356 OUT out_aborted BOOLEAN, 1357 -- Success return 1358 OUT out_status TEXT 1359 ) 1360 LANGUAGE plpgsql AS $$ 1361 DECLARE 1362 selected BOOLEAN; 1363 account_id INT8; 1364 exchange_account_id INT8; 1365 amount_with_fee taler_amount; 1366 BEGIN 1367 -- Check exchange account 1368 SELECT bank_account_id, NOT is_taler_exchange 1369 INTO exchange_account_id, out_account_is_not_exchange 1370 FROM bank_accounts 1371 WHERE internal_payto=in_selected_exchange_payto; 1372 out_account_not_found=NOT FOUND; 1373 IF out_account_not_found OR out_account_is_not_exchange THEN 1374 RETURN; 1375 END IF; 1376 1377 -- Check for conflict and idempotence 1378 SELECT 1379 selection_done, 1380 aborted, 1381 CASE 1382 WHEN confirmation_done THEN 'confirmed' 1383 ELSE 'selected' 1384 END, 1385 selection_done 1386 AND (exchange_bank_account != exchange_account_id OR reserve_pub != in_reserve_pub OR amount != in_amount), 1387 amount != in_amount, 1388 wallet_bank_account 1389 INTO selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id 1390 FROM taler_withdrawal_operations 1391 WHERE withdrawal_uuid=in_withdrawal_uuid; 1392 out_no_op = NOT FOUND; 1393 IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs OR selected THEN 1394 RETURN; 1395 END IF; 1396 1397 -- Check reserve_pub reuse 1398 out_reserve_pub_reuse=EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_reserve_pub AND type = 'reserve') OR 1399 EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub AND type = 'reserve'); 1400 IF out_reserve_pub_reuse THEN 1401 RETURN; 1402 END IF; 1403 1404 IF in_amount IS NOT NULL THEN 1405 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1406 account_id, 1407 in_amount, 1408 in_wire_transfer_fees, 1409 in_min_amount, 1410 in_max_amount 1411 ) AS test INTO out_balance_insufficient, out_bad_amount; 1412 IF out_balance_insufficient OR out_bad_amount THEN 1413 RETURN; 1414 END IF; 1415 END IF; 1416 1417 -- Update withdrawal operation 1418 UPDATE taler_withdrawal_operations 1419 SET exchange_bank_account=exchange_account_id, 1420 reserve_pub=in_reserve_pub, 1421 subject=in_subject, 1422 selection_done=true, 1423 amount=COALESCE(amount, in_amount) 1424 WHERE withdrawal_uuid=in_withdrawal_uuid; 1425 1426 -- Notify status change 1427 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected'); 1428 END $$; 1429 COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation'; 1430 1431 CREATE FUNCTION abort_taler_withdrawal( 1432 IN in_withdrawal_uuid uuid, 1433 OUT out_no_op BOOLEAN, 1434 OUT out_already_confirmed BOOLEAN 1435 ) 1436 LANGUAGE plpgsql AS $$ 1437 BEGIN 1438 UPDATE taler_withdrawal_operations 1439 SET aborted = NOT confirmation_done 1440 WHERE withdrawal_uuid=in_withdrawal_uuid 1441 RETURNING confirmation_done 1442 INTO out_already_confirmed; 1443 IF NOT FOUND OR out_already_confirmed THEN 1444 out_no_op=NOT FOUND; 1445 RETURN; 1446 END IF; 1447 1448 -- Notify status change 1449 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted'); 1450 END $$; 1451 COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.'; 1452 1453 CREATE FUNCTION confirm_taler_withdrawal( 1454 IN in_username TEXT, 1455 IN in_withdrawal_uuid uuid, 1456 IN in_timestamp INT8, 1457 IN in_is_tan BOOLEAN, 1458 IN in_wire_transfer_fees taler_amount, 1459 IN in_min_amount taler_amount, 1460 IN in_max_amount taler_amount, 1461 IN in_amount taler_amount, 1462 OUT out_no_op BOOLEAN, 1463 OUT out_balance_insufficient BOOLEAN, 1464 OUT out_reserve_pub_reuse BOOLEAN, 1465 OUT out_bad_amount BOOLEAN, 1466 OUT out_creditor_not_found BOOLEAN, 1467 OUT out_not_selected BOOLEAN, 1468 OUT out_missing_amount BOOLEAN, 1469 OUT out_amount_differs BOOLEAN, 1470 OUT out_aborted BOOLEAN, 1471 OUT out_tan_required BOOLEAN 1472 ) 1473 LANGUAGE plpgsql AS $$ 1474 DECLARE 1475 already_confirmed BOOLEAN; 1476 subject_local TEXT; 1477 reserve_pub_local BYTEA; 1478 wallet_bank_account_local INT8; 1479 amount_local taler_amount; 1480 exchange_bank_account_id INT8; 1481 tx_row_id INT8; 1482 local_type taler_incoming_type; 1483 BEGIN 1484 -- Load account info 1485 SELECT bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0 1486 INTO wallet_bank_account_local, out_tan_required 1487 FROM bank_accounts 1488 JOIN customers ON owning_customer_id=customer_id 1489 WHERE username=in_username AND deleted_at IS NULL; 1490 1491 -- Check op exists and conflict 1492 SELECT 1493 confirmation_done, 1494 aborted, NOT selection_done, 1495 reserve_pub, subject, type, 1496 exchange_bank_account, 1497 (amount).val, (amount).frac, 1498 amount IS NULL AND in_amount IS NULL, 1499 amount != in_amount 1500 INTO 1501 already_confirmed, 1502 out_aborted, out_not_selected, 1503 reserve_pub_local, subject_local, local_type, 1504 exchange_bank_account_id, 1505 amount_local.val, amount_local.frac, 1506 out_missing_amount, 1507 out_amount_differs 1508 FROM taler_withdrawal_operations 1509 WHERE withdrawal_uuid=in_withdrawal_uuid; 1510 out_no_op=NOT FOUND; 1511 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 1512 RETURN; 1513 ELSIF in_amount IS NOT NULL THEN 1514 amount_local = in_amount; 1515 END IF; 1516 1517 SELECT -- not checking for accounts existence, as it was done above. 1518 transfer.out_balance_insufficient, 1519 transfer.out_bad_amount, 1520 transfer.out_reserve_pub_reuse 1521 out_credit_row_id 1522 INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse, tx_row_id 1523 FROM make_incoming( 1524 exchange_bank_account_id, 1525 wallet_bank_account_local, 1526 subject_local, 1527 amount_local, 1528 in_timestamp, 1529 local_type, 1530 reserve_pub_local, 1531 in_wire_transfer_fees, 1532 in_min_amount, 1533 in_max_amount 1534 ) as transfer; 1535 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN 1536 RETURN; 1537 END IF; 1538 1539 -- Confirm operation and update amount 1540 UPDATE taler_withdrawal_operations 1541 SET amount=amount_local, confirmation_done=true 1542 WHERE withdrawal_uuid=in_withdrawal_uuid; 1543 1544 -- Notify status change 1545 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed'); 1546 END $$; 1547 COMMENT ON FUNCTION confirm_taler_withdrawal 1548 IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.'; 1549 1550 CREATE FUNCTION cashin( 1551 IN in_timestamp INT8, 1552 IN in_reserve_pub BYTEA, 1553 IN in_amount taler_amount, 1554 IN in_subject TEXT, 1555 -- Error status 1556 OUT out_no_account BOOLEAN, 1557 OUT out_too_small BOOLEAN, 1558 OUT out_balance_insufficient BOOLEAN 1559 ) 1560 LANGUAGE plpgsql AS $$ 1561 DECLARE 1562 converted_amount taler_amount; 1563 admin_account_id INT8; 1564 exchange_account_id INT8; 1565 exchange_conversion_rate_class_id INT8; 1566 tx_row_id INT8; 1567 BEGIN 1568 -- TODO check reserve_pub reuse ? 1569 1570 -- Recover exchange account info 1571 SELECT bank_account_id, conversion_rate_class_id 1572 INTO exchange_account_id, exchange_conversion_rate_class_id 1573 FROM bank_accounts 1574 JOIN customers 1575 ON customer_id=owning_customer_id 1576 WHERE username = 'exchange'; 1577 IF NOT FOUND THEN 1578 out_no_account = true; 1579 RETURN; 1580 END IF; 1581 1582 -- Retrieve admin account id 1583 SELECT bank_account_id 1584 INTO admin_account_id 1585 FROM bank_accounts 1586 JOIN customers 1587 ON customer_id=owning_customer_id 1588 WHERE username = 'admin'; 1589 1590 -- Perform conversion 1591 SELECT (converted).val, (converted).frac, too_small 1592 INTO converted_amount.val, converted_amount.frac, out_too_small 1593 FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id); 1594 IF out_too_small THEN 1595 RETURN; 1596 END IF; 1597 1598 -- Perform incoming transaction 1599 SELECT 1600 transfer.out_balance_insufficient, 1601 transfer.out_credit_row_id 1602 INTO 1603 out_balance_insufficient, 1604 tx_row_id 1605 FROM make_incoming( 1606 exchange_account_id, 1607 admin_account_id, 1608 in_subject, 1609 converted_amount, 1610 in_timestamp, 1611 'reserve'::taler_incoming_type, 1612 in_reserve_pub, 1613 NULL, 1614 NULL, 1615 NULL 1616 ) as transfer; 1617 IF out_balance_insufficient THEN 1618 RETURN; 1619 END IF; 1620 1621 -- update stats 1622 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount); 1623 1624 END $$; 1625 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation'; 1626 1627 1628 CREATE FUNCTION cashout_create( 1629 IN in_username TEXT, 1630 IN in_request_uid BYTEA, 1631 IN in_amount_debit taler_amount, 1632 IN in_amount_credit taler_amount, 1633 IN in_subject TEXT, 1634 IN in_timestamp INT8, 1635 IN in_is_tan BOOLEAN, 1636 -- Error status 1637 OUT out_bad_conversion BOOLEAN, 1638 OUT out_account_not_found BOOLEAN, 1639 OUT out_account_is_exchange BOOLEAN, 1640 OUT out_balance_insufficient BOOLEAN, 1641 OUT out_request_uid_reuse BOOLEAN, 1642 OUT out_no_cashout_payto BOOLEAN, 1643 OUT out_tan_required BOOLEAN, 1644 OUT out_under_min BOOLEAN, 1645 -- Success return 1646 OUT out_cashout_id INT8 1647 ) 1648 LANGUAGE plpgsql AS $$ 1649 DECLARE 1650 account_id INT8; 1651 account_conversion_rate_class_id INT8; 1652 account_cashout_payto TEXT; 1653 admin_account_id INT8; 1654 tx_id INT8; 1655 BEGIN 1656 1657 -- Check account exists, has all info and if 2FA is required 1658 SELECT 1659 bank_account_id, is_taler_exchange, conversion_rate_class_id, 1660 -- Remove potential residual query string an add the receiver_name 1661 split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name), 1662 NOT in_is_tan AND cardinality(tan_channels) > 0 1663 INTO 1664 account_id, out_account_is_exchange, account_conversion_rate_class_id, 1665 account_cashout_payto, out_tan_required 1666 FROM bank_accounts 1667 JOIN customers ON owning_customer_id=customer_id 1668 WHERE username=in_username; 1669 IF NOT FOUND THEN 1670 out_account_not_found=TRUE; 1671 RETURN; 1672 ELSIF account_cashout_payto IS NULL THEN 1673 out_no_cashout_payto=TRUE; 1674 RETURN; 1675 ELSIF out_account_is_exchange THEN 1676 RETURN; 1677 END IF; 1678 1679 -- check conversion 1680 SELECT under_min, too_small OR in_amount_credit!=converted 1681 INTO out_under_min, out_bad_conversion 1682 FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id); 1683 IF out_bad_conversion THEN 1684 RETURN; 1685 END IF; 1686 1687 -- Retrieve admin account id 1688 SELECT bank_account_id 1689 INTO admin_account_id 1690 FROM bank_accounts 1691 JOIN customers 1692 ON customer_id=owning_customer_id 1693 WHERE username = 'admin'; 1694 1695 -- Check for idempotence and conflict 1696 SELECT (amount_debit != in_amount_debit 1697 OR subject != in_subject 1698 OR bank_account != account_id) 1699 , cashout_id 1700 INTO out_request_uid_reuse, out_cashout_id 1701 FROM cashout_operations 1702 WHERE request_uid = in_request_uid; 1703 IF found OR out_request_uid_reuse OR out_tan_required THEN 1704 RETURN; 1705 END IF; 1706 1707 -- Perform bank wire transfer 1708 SELECT transfer.out_balance_insufficient, out_debit_row_id 1709 INTO out_balance_insufficient, tx_id 1710 FROM bank_wire_transfer( 1711 admin_account_id, 1712 account_id, 1713 in_subject, 1714 in_amount_debit, 1715 in_timestamp, 1716 NULL, 1717 NULL, 1718 NULL 1719 ) as transfer; 1720 IF out_balance_insufficient THEN 1721 RETURN; 1722 END IF; 1723 1724 -- Create cashout operation 1725 INSERT INTO cashout_operations ( 1726 request_uid 1727 ,amount_debit 1728 ,amount_credit 1729 ,creation_time 1730 ,bank_account 1731 ,subject 1732 ,local_transaction 1733 ) VALUES ( 1734 in_request_uid 1735 ,in_amount_debit 1736 ,in_amount_credit 1737 ,in_timestamp 1738 ,account_id 1739 ,in_subject 1740 ,tx_id 1741 ) RETURNING cashout_id INTO out_cashout_id; 1742 1743 -- Initiate libeufin-nexus transaction 1744 INSERT INTO libeufin_nexus.initiated_outgoing_transactions ( 1745 amount 1746 ,subject 1747 ,credit_payto 1748 ,initiation_time 1749 ,end_to_end_id 1750 ) VALUES ( 1751 ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount 1752 ,in_subject 1753 ,account_cashout_payto 1754 ,in_timestamp 1755 ,libeufin_nexus.ebics_id_gen() 1756 ); 1757 1758 -- update stats 1759 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit); 1760 END $$; 1761 1762 CREATE FUNCTION tan_challenge_mark_sent ( 1763 IN in_uuid UUID, 1764 IN in_timestamp INT8, 1765 IN in_retransmission_period INT8 1766 ) RETURNS void 1767 LANGUAGE sql AS $$ 1768 UPDATE tan_challenges SET 1769 retransmission_date = in_timestamp + in_retransmission_period 1770 WHERE uuid = in_uuid; 1771 $$; 1772 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent'; 1773 1774 CREATE FUNCTION tan_challenge_try ( 1775 IN in_uuid UUID, 1776 IN in_code TEXT, 1777 IN in_timestamp INT8, 1778 -- Error status 1779 OUT out_ok BOOLEAN, 1780 OUT out_no_op BOOLEAN, 1781 OUT out_no_retry BOOLEAN, 1782 OUT out_expired BOOLEAN, 1783 -- Success return 1784 OUT out_op op_enum, 1785 OUT out_channel tan_enum, 1786 OUT out_info TEXT 1787 ) 1788 LANGUAGE plpgsql as $$ 1789 DECLARE 1790 account_id INT8; 1791 token_creation BOOLEAN; 1792 BEGIN 1793 1794 -- Try to solve challenge 1795 UPDATE tan_challenges SET 1796 confirmation_date = CASE 1797 WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp 1798 ELSE confirmation_date 1799 END, 1800 retry_counter = retry_counter - 1 1801 WHERE uuid = in_uuid 1802 RETURNING 1803 confirmation_date IS NOT NULL, 1804 retry_counter <= 0 AND confirmation_date IS NULL, 1805 in_timestamp >= expiration_date AND confirmation_date IS NULL, 1806 op = 'create_token', 1807 customer 1808 INTO out_ok, out_no_retry, out_expired, token_creation, account_id; 1809 out_no_op = NOT FOUND; 1810 1811 IF NOT out_ok AND token_creation THEN 1812 UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id; 1813 END IF; 1814 1815 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN 1816 RETURN; 1817 END IF; 1818 1819 -- Recover body and op from challenge 1820 SELECT op, tan_channel, tan_info 1821 INTO out_op, out_channel, out_info 1822 FROM tan_challenges WHERE uuid = in_uuid; 1823 END $$; 1824 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed'; 1825 1826 CREATE FUNCTION stats_get_frame( 1827 IN date TIMESTAMP, 1828 IN in_timeframe stat_timeframe_enum, 1829 OUT cashin_count INT8, 1830 OUT cashin_regional_volume taler_amount, 1831 OUT cashin_fiat_volume taler_amount, 1832 OUT cashout_count INT8, 1833 OUT cashout_regional_volume taler_amount, 1834 OUT cashout_fiat_volume taler_amount, 1835 OUT taler_in_count INT8, 1836 OUT taler_in_volume taler_amount, 1837 OUT taler_out_count INT8, 1838 OUT taler_out_volume taler_amount 1839 ) 1840 LANGUAGE plpgsql AS $$ 1841 BEGIN 1842 date = date_trunc(in_timeframe::text, date); 1843 SELECT 1844 s.cashin_count 1845 ,(s.cashin_regional_volume).val 1846 ,(s.cashin_regional_volume).frac 1847 ,(s.cashin_fiat_volume).val 1848 ,(s.cashin_fiat_volume).frac 1849 ,s.cashout_count 1850 ,(s.cashout_regional_volume).val 1851 ,(s.cashout_regional_volume).frac 1852 ,(s.cashout_fiat_volume).val 1853 ,(s.cashout_fiat_volume).frac 1854 ,s.taler_in_count 1855 ,(s.taler_in_volume).val 1856 ,(s.taler_in_volume).frac 1857 ,s.taler_out_count 1858 ,(s.taler_out_volume).val 1859 ,(s.taler_out_volume).frac 1860 INTO 1861 cashin_count 1862 ,cashin_regional_volume.val 1863 ,cashin_regional_volume.frac 1864 ,cashin_fiat_volume.val 1865 ,cashin_fiat_volume.frac 1866 ,cashout_count 1867 ,cashout_regional_volume.val 1868 ,cashout_regional_volume.frac 1869 ,cashout_fiat_volume.val 1870 ,cashout_fiat_volume.frac 1871 ,taler_in_count 1872 ,taler_in_volume.val 1873 ,taler_in_volume.frac 1874 ,taler_out_count 1875 ,taler_out_volume.val 1876 ,taler_out_volume.frac 1877 FROM bank_stats AS s 1878 WHERE s.timeframe = in_timeframe 1879 AND s.start_time = date; 1880 END $$; 1881 1882 CREATE PROCEDURE stats_register_payment( 1883 IN name TEXT, 1884 IN now TIMESTAMP, 1885 IN regional_amount taler_amount, 1886 IN fiat_amount taler_amount 1887 ) 1888 LANGUAGE plpgsql AS $$ 1889 BEGIN 1890 IF now IS NULL THEN 1891 now = timezone('utc', now())::TIMESTAMP; 1892 END IF; 1893 IF name = 'taler_in' THEN 1894 INSERT INTO bank_stats AS s ( 1895 timeframe, 1896 start_time, 1897 taler_in_count, 1898 taler_in_volume 1899 ) SELECT 1900 frame, 1901 date_trunc(frame::text, now), 1902 1, 1903 regional_amount 1904 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1905 ON CONFLICT (timeframe, start_time) DO UPDATE 1906 SET taler_in_count=s.taler_in_count+1, 1907 taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount)); 1908 ELSIF name = 'taler_out' THEN 1909 INSERT INTO bank_stats AS s ( 1910 timeframe, 1911 start_time, 1912 taler_out_count, 1913 taler_out_volume 1914 ) SELECT 1915 frame, 1916 date_trunc(frame::text, now), 1917 1, 1918 regional_amount 1919 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1920 ON CONFLICT (timeframe, start_time) DO UPDATE 1921 SET taler_out_count=s.taler_out_count+1, 1922 taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount)); 1923 ELSIF name = 'cashin' THEN 1924 INSERT INTO bank_stats AS s ( 1925 timeframe, 1926 start_time, 1927 cashin_count, 1928 cashin_regional_volume, 1929 cashin_fiat_volume 1930 ) SELECT 1931 frame, 1932 date_trunc(frame::text, now), 1933 1, 1934 regional_amount, 1935 fiat_amount 1936 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1937 ON CONFLICT (timeframe, start_time) DO UPDATE 1938 SET cashin_count=s.cashin_count+1, 1939 cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)), 1940 cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount)); 1941 ELSIF name = 'cashout' THEN 1942 INSERT INTO bank_stats AS s ( 1943 timeframe, 1944 start_time, 1945 cashout_count, 1946 cashout_regional_volume, 1947 cashout_fiat_volume 1948 ) SELECT 1949 frame, 1950 date_trunc(frame::text, now), 1951 1, 1952 regional_amount, 1953 fiat_amount 1954 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1955 ON CONFLICT (timeframe, start_time) DO UPDATE 1956 SET cashout_count=s.cashout_count+1, 1957 cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)), 1958 cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount)); 1959 ELSE 1960 RAISE EXCEPTION 'Unknown stat %', name; 1961 END IF; 1962 END $$; 1963 1964 CREATE FUNCTION conversion_apply_ratio( 1965 IN amount taler_amount 1966 ,IN ratio taler_amount 1967 ,IN fee taler_amount 1968 ,IN tiny taler_amount -- Result is rounded to this amount 1969 ,IN rounding rounding_mode -- With this rounding mode 1970 ,OUT result taler_amount 1971 ,OUT out_too_small BOOLEAN 1972 ) 1973 LANGUAGE plpgsql IMMUTABLE AS $$ 1974 DECLARE 1975 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 1976 tiny_numeric NUMERIC(24); 1977 BEGIN 1978 -- Handle no config case 1979 IF ratio = (0, 0)::taler_amount THEN 1980 out_too_small=TRUE; 1981 RETURN; 1982 END IF; 1983 1984 -- Perform multiplication using big numbers 1985 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 1986 1987 -- Apply fees 1988 amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 1989 IF (sign(amount_numeric) != 1) THEN 1990 out_too_small = TRUE; 1991 result = (0, 0); 1992 RETURN; 1993 END IF; 1994 1995 -- Round to tiny amounts 1996 tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24)); 1997 case rounding 1998 when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 1999 when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric; 2000 when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric; 2001 end case; 2002 2003 -- Extract product parts 2004 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2005 2006 IF (result.val > 1::INT8<<52) THEN 2007 RAISE EXCEPTION 'amount value overflowed'; 2008 END IF; 2009 END $$; 2010 COMMENT ON FUNCTION conversion_apply_ratio 2011 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'; 2012 2013 CREATE FUNCTION conversion_revert_ratio( 2014 IN amount taler_amount 2015 ,IN ratio taler_amount 2016 ,IN fee taler_amount 2017 ,IN tiny taler_amount -- Result is rounded to this amount 2018 ,IN rounding rounding_mode -- With this rounding mode 2019 ,IN reverse_tiny taler_amount 2020 ,OUT result taler_amount 2021 ,OUT bad_value BOOLEAN 2022 ) 2023 LANGUAGE plpgsql IMMUTABLE AS $$ 2024 DECLARE 2025 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 2026 tiny_numeric NUMERIC(24); 2027 roundtrip BOOLEAN; 2028 BEGIN 2029 -- Handle no config case 2030 IF ratio = (0, 0)::taler_amount THEN 2031 bad_value=TRUE; 2032 RETURN; 2033 END IF; 2034 2035 -- Apply fees 2036 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 2037 2038 -- Perform division using big numbers 2039 amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 2040 2041 -- Round to input digits 2042 tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24)); 2043 amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 2044 2045 -- Extract division parts 2046 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2047 2048 -- Recover potentially lost tiny amount during rounding 2049 -- There must be a clever way to compute this but I am a little limited with math 2050 -- and revert ratio computation is not a hot function so I just use the apply ratio 2051 -- function to be conservative and correct 2052 SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount); 2053 IF NOT roundtrip THEN 2054 amount_numeric = amount_numeric + tiny_numeric; 2055 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2056 END IF; 2057 2058 IF (result.val > 1::INT8<<52) THEN 2059 RAISE EXCEPTION 'amount value overflowed'; 2060 END IF; 2061 END $$; 2062 COMMENT ON FUNCTION conversion_revert_ratio 2063 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'; 2064 2065 2066 CREATE FUNCTION conversion_to( 2067 IN amount taler_amount, 2068 IN direction TEXT, 2069 IN conversion_rate_class_id INT8, 2070 OUT converted taler_amount, 2071 OUT too_small BOOLEAN, 2072 OUT under_min BOOLEAN 2073 ) 2074 LANGUAGE plpgsql STABLE AS $$ 2075 DECLARE 2076 at_ratio taler_amount; 2077 out_fee taler_amount; 2078 tiny_amount taler_amount; 2079 min_amount taler_amount; 2080 mode rounding_mode; 2081 BEGIN 2082 -- Load rate 2083 IF direction='cashin' THEN 2084 SELECT 2085 (cashin_ratio).val, (cashin_ratio).frac, 2086 (cashin_fee).val, (cashin_fee).frac, 2087 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2088 (cashin_min_amount).val, (cashin_min_amount).frac, 2089 cashin_rounding_mode 2090 INTO 2091 at_ratio.val, at_ratio.frac, 2092 out_fee.val, out_fee.frac, 2093 tiny_amount.val, tiny_amount.frac, 2094 min_amount.val, min_amount.frac, 2095 mode 2096 FROM get_conversion_class_rate(conversion_rate_class_id); 2097 ELSE 2098 SELECT 2099 (cashout_ratio).val, (cashout_ratio).frac, 2100 (cashout_fee).val, (cashout_fee).frac, 2101 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2102 (cashout_min_amount).val, (cashout_min_amount).frac, 2103 cashout_rounding_mode 2104 INTO 2105 at_ratio.val, at_ratio.frac, 2106 out_fee.val, out_fee.frac, 2107 tiny_amount.val, tiny_amount.frac, 2108 min_amount.val, min_amount.frac, 2109 mode 2110 FROM get_conversion_class_rate(conversion_rate_class_id); 2111 END IF; 2112 2113 -- Check min amount 2114 SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount); 2115 IF too_small THEN 2116 under_min = true; 2117 converted = (0, 0); 2118 RETURN; 2119 END IF; 2120 2121 -- Perform conversion 2122 SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 2123 FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode); 2124 END $$; 2125 2126 CREATE FUNCTION conversion_from( 2127 IN amount taler_amount, 2128 IN direction TEXT, 2129 IN conversion_rate_class_id INT8, 2130 OUT converted taler_amount, 2131 OUT too_small BOOLEAN, 2132 OUT under_min BOOLEAN 2133 ) 2134 LANGUAGE plpgsql STABLE AS $$ 2135 DECLARE 2136 ratio taler_amount; 2137 out_fee taler_amount; 2138 tiny_amount taler_amount; 2139 reverse_tiny_amount taler_amount; 2140 min_amount taler_amount; 2141 mode rounding_mode; 2142 BEGIN 2143 -- Load rate 2144 IF direction='cashin' THEN 2145 SELECT 2146 (cashin_ratio).val, (cashin_ratio).frac, 2147 (cashin_fee).val, (cashin_fee).frac, 2148 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2149 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2150 (cashin_min_amount).val, (cashin_min_amount).frac, 2151 cashin_rounding_mode 2152 INTO 2153 ratio.val, ratio.frac, 2154 out_fee.val, out_fee.frac, 2155 tiny_amount.val, tiny_amount.frac, 2156 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2157 min_amount.val, min_amount.frac, 2158 mode 2159 FROM get_conversion_class_rate(conversion_rate_class_id); 2160 ELSE 2161 SELECT 2162 (cashout_ratio).val, (cashout_ratio).frac, 2163 (cashout_fee).val, (cashout_fee).frac, 2164 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2165 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2166 (cashout_min_amount).val, (cashout_min_amount).frac, 2167 cashout_rounding_mode 2168 INTO 2169 ratio.val, ratio.frac, 2170 out_fee.val, out_fee.frac, 2171 tiny_amount.val, tiny_amount.frac, 2172 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2173 min_amount.val, min_amount.frac, 2174 mode 2175 FROM get_conversion_class_rate(conversion_rate_class_id); 2176 END IF; 2177 2178 -- Perform conversion 2179 SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small 2180 FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount); 2181 IF too_small THEN 2182 RETURN; 2183 END IF; 2184 2185 -- Check min amount 2186 SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount); 2187 IF too_small THEN 2188 under_min = true; 2189 converted = (0, 0); 2190 END IF; 2191 END $$; 2192 2193 CREATE FUNCTION config_get_conversion_rate() 2194 RETURNS TABLE ( 2195 cashin_ratio taler_amount, 2196 cashin_fee taler_amount, 2197 cashin_tiny_amount taler_amount, 2198 cashin_min_amount taler_amount, 2199 cashin_rounding_mode rounding_mode, 2200 cashout_ratio taler_amount, 2201 cashout_fee taler_amount, 2202 cashout_tiny_amount taler_amount, 2203 cashout_min_amount taler_amount, 2204 cashout_rounding_mode rounding_mode 2205 ) 2206 LANGUAGE sql STABLE AS $$ 2207 SELECT 2208 (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount, 2209 (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount, 2210 (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount, 2211 (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount, 2212 (value->'cashin'->>'rounding_mode')::rounding_mode, 2213 (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount, 2214 (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount, 2215 (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount, 2216 (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount, 2217 (value->'cashout'->>'rounding_mode')::rounding_mode 2218 FROM config WHERE key='conversion_rate' 2219 UNION ALL 2220 SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode, 2221 (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode 2222 LIMIT 1 2223 $$; 2224 2225 CREATE FUNCTION get_conversion_class_rate( 2226 IN in_conversion_rate_class_id INT8 2227 ) 2228 RETURNS TABLE ( 2229 cashin_ratio taler_amount, 2230 cashin_fee taler_amount, 2231 cashin_tiny_amount taler_amount, 2232 cashin_min_amount taler_amount, 2233 cashin_rounding_mode rounding_mode, 2234 cashout_ratio taler_amount, 2235 cashout_fee taler_amount, 2236 cashout_tiny_amount taler_amount, 2237 cashout_min_amount taler_amount, 2238 cashout_rounding_mode rounding_mode 2239 ) 2240 LANGUAGE sql STABLE AS $$ 2241 SELECT 2242 COALESCE(class.cashin_ratio, cfg.cashin_ratio), 2243 COALESCE(class.cashin_fee, cfg.cashin_fee), 2244 cashin_tiny_amount, 2245 COALESCE(class.cashin_min_amount, cfg.cashin_min_amount), 2246 COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode), 2247 COALESCE(class.cashout_ratio, cfg.cashout_ratio), 2248 COALESCE(class.cashout_fee, cfg.cashout_fee), 2249 cashout_tiny_amount, 2250 COALESCE(class.cashout_min_amount, cfg.cashout_min_amount), 2251 COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode) 2252 FROM config_get_conversion_rate() as cfg 2253 LEFT JOIN conversion_rate_classes as class 2254 ON (conversion_rate_class_id=in_conversion_rate_class_id) 2255 $$; 2256 2257 CREATE PROCEDURE config_set_conversion_rate( 2258 IN cashin_ratio taler_amount, 2259 IN cashin_fee taler_amount, 2260 IN cashin_tiny_amount taler_amount, 2261 IN cashin_min_amount taler_amount, 2262 IN cashin_rounding_mode rounding_mode, 2263 IN cashout_ratio taler_amount, 2264 IN cashout_fee taler_amount, 2265 IN cashout_tiny_amount taler_amount, 2266 IN cashout_min_amount taler_amount, 2267 IN cashout_rounding_mode rounding_mode 2268 ) 2269 LANGUAGE sql AS $$ 2270 INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object( 2271 'cashin', jsonb_build_object( 2272 'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac), 2273 'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac), 2274 'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac), 2275 'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac), 2276 'rounding_mode', cashin_rounding_mode 2277 ), 2278 'cashout', jsonb_build_object( 2279 'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac), 2280 'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac), 2281 'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac), 2282 'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac), 2283 'rounding_mode', cashout_rounding_mode 2284 ) 2285 )) ON CONFLICT (key) DO UPDATE SET value = excluded.value 2286 $$; 2287 2288 CREATE FUNCTION register_prepared_transfers ( 2289 IN in_exchange_username TEXT, 2290 IN in_type taler_incoming_type, 2291 IN in_account_pub BYTEA, 2292 IN in_authorization_pub BYTEA, 2293 IN in_authorization_sig BYTEA, 2294 IN in_recurrent BOOLEAN, 2295 IN in_amount taler_amount, 2296 IN in_timestamp INT8, 2297 IN in_subject TEXT, 2298 -- Error status 2299 OUT out_unknown_account BOOLEAN, 2300 OUT out_not_exchange BOOLEAN, 2301 OUT out_reserve_pub_reuse BOOLEAN, 2302 -- Success status 2303 OUT out_withdrawal_uuid UUID 2304 ) 2305 LANGUAGE plpgsql AS $$ 2306 DECLARE 2307 local_withdrawal_id INT8; 2308 exchange_account_id INT8; 2309 talerable_tx INT8; 2310 idempotent BOOLEAN; 2311 BEGIN 2312 -- Retrieve exchange account if 2313 SELECT bank_account_id, NOT is_taler_exchange 2314 INTO exchange_account_id, out_not_exchange 2315 FROM bank_accounts 2316 JOIN customers ON customer_id=owning_customer_id 2317 WHERE username = in_exchange_username; 2318 out_unknown_account=NOT FOUND; 2319 if out_unknown_account OR out_not_exchange THEN RETURN; END IF; 2320 2321 -- Check idempotency 2322 SELECT withdrawal_uuid, prepared_transfers.type = in_type 2323 AND account_pub = in_account_pub 2324 AND recurrent = in_recurrent 2325 AND amount = in_amount 2326 INTO out_withdrawal_uuid, idempotent 2327 FROM prepared_transfers 2328 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id) 2329 WHERE authorization_pub = in_authorization_pub; 2330 2331 -- Check idempotency and delay garbage collection 2332 IF FOUND AND idempotent THEN 2333 UPDATE prepared_transfers 2334 SET registered_at=in_timestamp 2335 WHERE authorization_pub=in_authorization_pub; 2336 RETURN; 2337 END IF; 2338 2339 -- Check reserve pub reuse 2340 out_reserve_pub_reuse=in_type = 'reserve' AND ( 2341 EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve') 2342 ); 2343 IF out_reserve_pub_reuse THEN 2344 RETURN; 2345 END IF; 2346 2347 -- Create/replace withdrawal 2348 IF out_withdrawal_uuid IS NOT NULL THEN 2349 PERFORM abort_taler_withdrawal(out_withdrawal_uuid); 2350 END IF; 2351 out_withdrawal_uuid=null; 2352 2353 IF in_recurrent THEN 2354 -- Finalize one pending right now 2355 DELETE FROM pending_recurrent_incoming_transactions 2356 WHERE bank_transaction_id = ( 2357 SELECT bank_transaction_id 2358 FROM pending_recurrent_incoming_transactions 2359 JOIN bank_account_transactions USING (bank_transaction_id) 2360 ORDER BY transaction_date ASC 2361 LIMIT 1 2362 ) 2363 RETURNING bank_transaction_id 2364 INTO talerable_tx; 2365 IF FOUND THEN 2366 PERFORM register_incoming(talerable_tx, in_type, in_account_pub, exchange_account_id, in_authorization_pub, in_authorization_sig); 2367 END IF; 2368 ELSE 2369 -- Bounce all pending 2370 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp) 2371 FROM pending_recurrent_incoming_transactions 2372 WHERE authorization_pub = in_authorization_pub; 2373 2374 -- Create withdrawal 2375 INSERT INTO taler_withdrawal_operations ( 2376 withdrawal_uuid, 2377 wallet_bank_account, 2378 amount, 2379 suggested_amount, 2380 no_amount_to_wallet, 2381 exchange_bank_account, 2382 type, 2383 reserve_pub, 2384 subject, 2385 selection_done, 2386 creation_date 2387 ) VALUES ( 2388 gen_random_uuid(), 2389 NULL, 2390 in_amount, 2391 NULL, 2392 true, 2393 exchange_account_id, 2394 'map', 2395 in_account_pub, 2396 in_subject, 2397 true, 2398 in_timestamp 2399 ) RETURNING withdrawal_uuid, withdrawal_id 2400 INTO out_withdrawal_uuid, local_withdrawal_id; 2401 END IF; 2402 2403 -- Upsert registration 2404 INSERT INTO prepared_transfers ( 2405 type, 2406 account_pub, 2407 authorization_pub, 2408 authorization_sig, 2409 recurrent, 2410 registered_at, 2411 bank_transaction_id, 2412 withdrawal_id 2413 ) VALUES ( 2414 in_type, 2415 in_account_pub, 2416 in_authorization_pub, 2417 in_authorization_sig, 2418 in_recurrent, 2419 in_timestamp, 2420 talerable_tx, 2421 local_withdrawal_id 2422 ) ON CONFLICT (authorization_pub) 2423 DO UPDATE SET 2424 type = EXCLUDED.type, 2425 account_pub = EXCLUDED.account_pub, 2426 recurrent = EXCLUDED.recurrent, 2427 registered_at = EXCLUDED.registered_at, 2428 bank_transaction_id = EXCLUDED.bank_transaction_id, 2429 withdrawal_id = EXCLUDED.withdrawal_id, 2430 authorization_sig = EXCLUDED.authorization_sig; 2431 END $$; 2432 2433 CREATE FUNCTION delete_prepared_transfers ( 2434 IN in_authorization_pub BYTEA, 2435 IN in_timestamp INT8, 2436 OUT out_found BOOLEAN 2437 ) 2438 LANGUAGE plpgsql AS $$ 2439 BEGIN 2440 -- Bounce all pending 2441 PERFORM bounce(debtor_account_id, bank_transaction_id, 'cancelled mapping', in_timestamp) 2442 FROM pending_recurrent_incoming_transactions 2443 WHERE authorization_pub = in_authorization_pub; 2444 2445 -- Delete registration 2446 DELETE FROM prepared_transfers 2447 WHERE authorization_pub = in_authorization_pub; 2448 out_found = FOUND; 2449 2450 -- TODO abort withdrawal 2451 END $$; 2452 2453 COMMIT;