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