libeufin-bank-procedures.sql (71736B)
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 AND 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_wire_transfer_fees, 579 in_timestamp, 580 'credit', 581 admin_account_id 582 ); 583 584 UPDATE bank_accounts 585 SET 586 balance=admin_balance, 587 has_debt=admin_has_debt 588 WHERE bank_account_id=admin_account_id; 589 END IF; 590 591 -- notify new transaction 592 PERFORM pg_notify('bank_tx', in_debtor_account_id || ' ' || in_creditor_account_id || ' ' || out_debit_row_id || ' ' || out_credit_row_id); 593 END $$; 594 595 CREATE FUNCTION account_delete( 596 IN in_username TEXT, 597 IN in_timestamp INT8, 598 IN in_is_tan BOOLEAN, 599 OUT out_not_found BOOLEAN, 600 OUT out_balance_not_zero BOOLEAN, 601 OUT out_tan_required BOOLEAN 602 ) 603 LANGUAGE plpgsql AS $$ 604 DECLARE 605 my_customer_id INT8; 606 BEGIN 607 -- check if account exists, has zero balance and if 2FA is required 608 SELECT 609 customer_id 610 ,NOT in_is_tan AND cardinality(tan_channels) > 0 611 ,(balance).val != 0 OR (balance).frac != 0 612 INTO 613 my_customer_id 614 ,out_tan_required 615 ,out_balance_not_zero 616 FROM customers 617 JOIN bank_accounts ON owning_customer_id = customer_id 618 WHERE username = in_username AND deleted_at IS NULL; 619 IF NOT FOUND OR out_balance_not_zero OR out_tan_required THEN 620 out_not_found=NOT FOUND; 621 RETURN; 622 END IF; 623 624 -- actual deletion 625 UPDATE customers SET deleted_at = in_timestamp WHERE customer_id = my_customer_id; 626 END $$; 627 COMMENT ON FUNCTION account_delete IS 'Deletes an account if the balance is zero'; 628 629 CREATE FUNCTION register_incoming( 630 IN in_tx_row_id INT8, 631 IN in_type taler_incoming_type, 632 IN in_metadata BYTEA, 633 IN in_account_id INT8, 634 IN in_authorization_pub BYTEA, 635 IN in_authorization_sig BYTEA 636 ) 637 RETURNS void 638 LANGUAGE plpgsql AS $$ 639 DECLARE 640 local_amount taler_amount; 641 BEGIN 642 -- Register incoming transaction 643 INSERT INTO taler_exchange_incoming ( 644 metadata, 645 bank_transaction, 646 type, 647 authorization_pub, 648 authorization_sig 649 ) VALUES ( 650 in_metadata, 651 in_tx_row_id, 652 in_type, 653 in_authorization_pub, 654 in_authorization_sig 655 ); 656 -- Update stats 657 IF in_type = 'reserve' THEN 658 SELECT (amount).val, (amount).frac 659 INTO local_amount.val, local_amount.frac 660 FROM bank_account_transactions WHERE bank_transaction_id=in_tx_row_id; 661 CALL stats_register_payment('taler_in', NULL, local_amount, null); 662 END IF; 663 -- Notify new incoming transaction 664 PERFORM pg_notify('bank_incoming_tx', in_account_id || ' ' || in_tx_row_id); 665 END $$; 666 COMMENT ON FUNCTION register_incoming 667 IS 'Register a bank transaction as a taler incoming transaction and announce it'; 668 669 CREATE FUNCTION bounce( 670 IN in_debtor_account_id INT8, 671 IN in_credit_transaction_id INT8, 672 IN in_bounce_cause TEXT, 673 IN in_timestamp INT8 674 ) 675 RETURNS void 676 LANGUAGE plpgsql AS $$ 677 DECLARE 678 local_creditor_account_id INT8; 679 local_amount taler_amount; 680 BEGIN 681 -- Load transaction info 682 SELECT (amount).frac, (amount).val, bank_account_id 683 INTO local_amount.frac, local_amount.val, local_creditor_account_id 684 FROM bank_account_transactions 685 WHERE bank_transaction_id=in_credit_transaction_id; 686 687 -- No error can happens because an opposite transaction already took place in the same transaction 688 PERFORM bank_wire_transfer( 689 in_debtor_account_id, 690 local_creditor_account_id, 691 'Bounce ' || in_credit_transaction_id || ': ' || in_bounce_cause, 692 local_amount, 693 in_timestamp, 694 NULL, 695 NULL, 696 NULL 697 ); 698 699 -- Delete from pending if any 700 DELETE FROM pending_recurrent_incoming_transactions WHERE bank_transaction_id = in_credit_transaction_id; 701 END$$; 702 703 CREATE FUNCTION make_incoming( 704 IN in_creditor_account_id INT8, 705 IN in_debtor_account_id INT8, 706 IN in_subject TEXT, 707 IN in_amount taler_amount, 708 IN in_timestamp INT8, 709 IN in_type taler_incoming_type, 710 IN in_metadata BYTEA, 711 IN in_wire_transfer_fees taler_amount, 712 IN in_min_amount taler_amount, 713 IN in_max_amount taler_amount, 714 -- Error status 715 OUT out_balance_insufficient BOOLEAN, 716 OUT out_bad_amount BOOLEAN, 717 OUT out_reserve_pub_reuse BOOLEAN, 718 OUT out_mapping_reuse BOOLEAN, 719 OUT out_unknown_mapping BOOLEAN, 720 -- Success return 721 OUT out_pending BOOLEAN, 722 OUT out_credit_row_id INT8, 723 OUT out_debit_row_id INT8 724 ) 725 LANGUAGE plpgsql AS $$ 726 DECLARE 727 local_withdrawal_uuid UUID; 728 local_authorization_pub BYTEA; 729 local_authorization_sig BYTEA; 730 BEGIN 731 out_pending=FALSE; 732 733 -- Resolve mapping logic 734 IF in_type = 'map' THEN 735 SELECT prepared_transfers.type, account_pub, authorization_pub, authorization_sig, withdrawal_uuid, 736 bank_transaction_id IS NOT NULL AND NOT recurrent, 737 bank_transaction_id IS NOT NULL AND recurrent 738 INTO in_type, in_metadata, local_authorization_pub, local_authorization_sig, local_withdrawal_uuid, out_mapping_reuse, out_pending 739 FROM prepared_transfers 740 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id) 741 WHERE authorization_pub = in_metadata; 742 out_unknown_mapping = NOT FOUND; 743 IF out_unknown_mapping OR out_mapping_reuse THEN 744 RETURN; 745 END IF; 746 END IF; 747 748 -- Check reserve pub reuse 749 out_reserve_pub_reuse=in_type = 'reserve' AND NOT out_pending AND EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_metadata AND type = 'reserve'); 750 IF out_reserve_pub_reuse THEN 751 RETURN; 752 END IF; 753 754 -- Perform bank wire transfer 755 SELECT 756 transfer.out_balance_insufficient, 757 transfer.out_bad_amount, 758 transfer.out_credit_row_id, 759 transfer.out_debit_row_id 760 INTO 761 out_balance_insufficient, 762 out_bad_amount, 763 out_credit_row_id, 764 out_debit_row_id 765 FROM bank_wire_transfer( 766 in_creditor_account_id, 767 in_debtor_account_id, 768 in_subject, 769 in_amount, 770 in_timestamp, 771 in_wire_transfer_fees, 772 in_min_amount, 773 in_max_amount 774 ) as transfer; 775 IF out_balance_insufficient OR out_bad_amount THEN 776 RETURN; 777 END IF; 778 779 IF out_pending THEN 780 -- Delay talerable registration until mapping again 781 INSERT INTO pending_recurrent_incoming_transactions (bank_transaction_id, debtor_account_id, authorization_pub) 782 VALUES (out_credit_row_id, in_debtor_account_id, local_authorization_pub); 783 ELSE 784 UPDATE prepared_transfers 785 SET bank_transaction_id = out_credit_row_id 786 WHERE (bank_transaction_id IS NULL AND account_pub = in_metadata) OR authorization_pub = local_authorization_pub; 787 IF local_withdrawal_uuid IS NOT NULL THEN 788 PERFORM abort_taler_withdrawal(local_withdrawal_uuid); 789 END IF; 790 PERFORM register_incoming(out_credit_row_id, in_type, in_metadata, in_creditor_account_id, local_authorization_pub, local_authorization_sig); 791 END IF; 792 END $$; 793 794 795 CREATE FUNCTION taler_transfer( 796 IN in_request_uid BYTEA, 797 IN in_wtid BYTEA, 798 IN in_subject TEXT, 799 IN in_amount taler_amount, 800 IN in_exchange_base_url TEXT, 801 IN in_metadata TEXT, 802 IN in_credit_account_payto TEXT, 803 IN in_username TEXT, 804 IN in_timestamp INT8, 805 IN in_conversion BOOLEAN, 806 -- Error status 807 OUT out_debtor_not_found BOOLEAN, 808 OUT out_debtor_not_exchange BOOLEAN, 809 OUT out_both_exchanges BOOLEAN, 810 OUT out_creditor_admin BOOLEAN, 811 OUT out_request_uid_reuse BOOLEAN, 812 OUT out_wtid_reuse BOOLEAN, 813 OUT out_exchange_balance_insufficient BOOLEAN, 814 -- Success return 815 OUT out_tx_row_id INT8, 816 OUT out_timestamp INT8 817 ) 818 LANGUAGE plpgsql AS $$ 819 DECLARE 820 exchange_account_id INT8; 821 creditor_account_id INT8; 822 account_conversion_rate_class_id INT8; 823 creditor_name TEXT; 824 creditor_admin BOOLEAN; 825 credit_row_id INT8; 826 debit_row_id INT8; 827 outgoing_id INT8; 828 bounce_tx INT8; 829 bounce_amount taler_amount; 830 BEGIN 831 -- Check for idempotence and conflict 832 SELECT (amount != in_amount 833 OR creditor_payto != in_credit_account_payto 834 OR exchange_base_url != in_exchange_base_url 835 OR metadata != in_metadata 836 OR wtid != in_wtid) 837 ,transfer_operation_id, transfer_date 838 INTO out_request_uid_reuse, out_tx_row_id, out_timestamp 839 FROM transfer_operations 840 WHERE request_uid = in_request_uid; 841 IF found THEN 842 RETURN; 843 END IF; 844 out_wtid_reuse = EXISTS(SELECT FROM transfer_operations WHERE wtid = in_wtid); 845 IF out_wtid_reuse THEN 846 RETURN; 847 END IF; 848 out_timestamp=in_timestamp; 849 -- Find exchange bank account id 850 SELECT 851 bank_account_id, NOT is_taler_exchange, conversion_rate_class_id 852 INTO exchange_account_id, out_debtor_not_exchange, account_conversion_rate_class_id 853 FROM bank_accounts 854 JOIN customers 855 ON customer_id=owning_customer_id 856 WHERE username = in_username AND deleted_at IS NULL; 857 out_debtor_not_found=NOT FOUND; 858 IF out_debtor_not_found OR out_debtor_not_exchange THEN 859 RETURN; 860 END IF; 861 -- Find creditor bank account id 862 SELECT 863 bank_account_id, is_taler_exchange, username = 'admin' 864 INTO creditor_account_id, out_both_exchanges, creditor_admin 865 FROM bank_accounts 866 JOIN customers ON owning_customer_id=customer_id 867 WHERE internal_payto = in_credit_account_payto; 868 IF NOT FOUND THEN 869 -- Register failure 870 INSERT INTO transfer_operations ( 871 request_uid, 872 wtid, 873 amount, 874 exchange_base_url, 875 metadata, 876 transfer_date, 877 exchange_outgoing_id, 878 creditor_payto, 879 status, 880 status_msg, 881 exchange_id 882 ) VALUES ( 883 in_request_uid, 884 in_wtid, 885 in_amount, 886 in_exchange_base_url, 887 in_metadata, 888 in_timestamp, 889 NULL, 890 in_credit_account_payto, 891 'permanent_failure', 892 'Unknown account', 893 exchange_account_id 894 ) RETURNING transfer_operation_id INTO out_tx_row_id; 895 RETURN; 896 ELSIF out_both_exchanges THEN 897 RETURN; 898 END IF; 899 900 IF creditor_admin THEN 901 -- Check if this is a conversion bounce 902 IF NOT in_conversion THEN 903 out_creditor_admin=TRUE; 904 RETURN; 905 END IF; 906 907 -- Find the bounced transaction 908 SELECT (amount).val, (amount).frac, incoming_transaction_id 909 INTO bounce_amount.val, bounce_amount.frac, bounce_tx 910 FROM libeufin_nexus.incoming_transactions 911 JOIN libeufin_nexus.talerable_incoming_transactions USING (incoming_transaction_id) 912 WHERE metadata=in_wtid AND type='reserve'; 913 IF NOT FOUND THEN 914 -- Register failure 915 INSERT INTO transfer_operations ( 916 request_uid, 917 wtid, 918 amount, 919 exchange_base_url, 920 metadata, 921 transfer_date, 922 exchange_outgoing_id, 923 creditor_payto, 924 status, 925 status_msg, 926 exchange_id 927 ) VALUES ( 928 in_request_uid, 929 in_wtid, 930 in_amount, 931 in_exchange_base_url, 932 in_metadata, 933 in_timestamp, 934 NULL, 935 in_credit_account_payto, 936 'permanent_failure', 937 'Unknown bounced transaction', 938 exchange_account_id 939 ) RETURNING transfer_operation_id INTO out_tx_row_id; 940 RETURN; 941 END IF; 942 943 -- Bounce the transaction 944 PERFORM libeufin_nexus.bounce_incoming( 945 bounce_tx 946 ,((bounce_amount).val, (bounce_amount).frac)::libeufin_nexus.taler_amount 947 ,libeufin_nexus.ebics_id_gen() 948 ,in_timestamp 949 ,'exchange bounced' 950 ); 951 END IF; 952 -- Perform bank transfer 953 SELECT 954 out_balance_insufficient, 955 out_debit_row_id, out_credit_row_id 956 INTO 957 out_exchange_balance_insufficient, 958 debit_row_id, credit_row_id 959 FROM bank_wire_transfer( 960 creditor_account_id, 961 exchange_account_id, 962 in_subject, 963 in_amount, 964 in_timestamp, 965 NULL, 966 NULL, 967 NULL 968 ); 969 IF out_exchange_balance_insufficient THEN 970 RETURN; 971 END IF; 972 -- Register outgoing transaction 973 INSERT INTO taler_exchange_outgoing ( 974 bank_transaction 975 ) VALUES ( 976 debit_row_id 977 ) RETURNING exchange_outgoing_id INTO outgoing_id; 978 -- Update stats 979 CALL stats_register_payment('taler_out', NULL, in_amount, null); 980 -- Register success 981 INSERT INTO transfer_operations ( 982 request_uid, 983 wtid, 984 amount, 985 exchange_base_url, 986 metadata, 987 transfer_date, 988 exchange_outgoing_id, 989 creditor_payto, 990 status, 991 status_msg, 992 exchange_id 993 ) VALUES ( 994 in_request_uid, 995 in_wtid, 996 in_amount, 997 in_exchange_base_url, 998 in_metadata, 999 in_timestamp, 1000 outgoing_id, 1001 in_credit_account_payto, 1002 'success', 1003 NULL, 1004 exchange_account_id 1005 ) RETURNING transfer_operation_id INTO out_tx_row_id; 1006 1007 -- Notify new transaction 1008 PERFORM pg_notify('bank_outgoing_tx', exchange_account_id || ' ' || creditor_account_id || ' ' || debit_row_id || ' ' || credit_row_id); 1009 1010 IF creditor_admin THEN 1011 -- Create cashout operation 1012 INSERT INTO cashout_operations ( 1013 request_uid 1014 ,amount_debit 1015 ,amount_credit 1016 ,creation_time 1017 ,bank_account 1018 ,subject 1019 ,local_transaction 1020 ) VALUES ( 1021 NULL 1022 ,in_amount 1023 ,bounce_amount 1024 ,in_timestamp 1025 ,exchange_account_id 1026 ,in_subject 1027 ,debit_row_id 1028 ); 1029 1030 -- update stats 1031 CALL stats_register_payment('cashout', NULL, in_amount, bounce_amount); 1032 END IF; 1033 END $$; 1034 COMMENT ON FUNCTION taler_transfer IS 'Create an outgoing taler transaction and register it'; 1035 1036 CREATE FUNCTION taler_add_incoming( 1037 IN in_key BYTEA, 1038 IN in_subject TEXT, 1039 IN in_amount taler_amount, 1040 IN in_debit_account_payto TEXT, 1041 IN in_username TEXT, 1042 IN in_timestamp INT8, 1043 IN in_type taler_incoming_type, 1044 -- Error status 1045 OUT out_creditor_not_found BOOLEAN, 1046 OUT out_creditor_not_exchange BOOLEAN, 1047 OUT out_debtor_not_found BOOLEAN, 1048 OUT out_both_exchanges BOOLEAN, 1049 OUT out_reserve_pub_reuse BOOLEAN, 1050 OUT out_mapping_reuse BOOLEAN, 1051 OUT out_unknown_mapping BOOLEAN, 1052 OUT out_debitor_balance_insufficient BOOLEAN, 1053 -- Success return 1054 OUT out_tx_row_id INT8, 1055 OUT out_pending INT8 1056 ) 1057 LANGUAGE plpgsql AS $$ 1058 DECLARE 1059 exchange_bank_account_id INT8; 1060 sender_bank_account_id INT8; 1061 BEGIN 1062 -- Find exchange bank account id 1063 SELECT 1064 bank_account_id, NOT is_taler_exchange 1065 INTO exchange_bank_account_id, out_creditor_not_exchange 1066 FROM bank_accounts 1067 JOIN customers 1068 ON customer_id=owning_customer_id 1069 WHERE username = in_username AND deleted_at IS NULL; 1070 IF NOT FOUND OR out_creditor_not_exchange THEN 1071 out_creditor_not_found=NOT FOUND; 1072 RETURN; 1073 END IF; 1074 -- Find sender bank account id 1075 SELECT 1076 bank_account_id, is_taler_exchange 1077 INTO sender_bank_account_id, out_both_exchanges 1078 FROM bank_accounts 1079 WHERE internal_payto = in_debit_account_payto; 1080 IF NOT FOUND OR out_both_exchanges THEN 1081 out_debtor_not_found=NOT FOUND; 1082 RETURN; 1083 END IF; 1084 -- Perform bank transfer 1085 SELECT 1086 out_balance_insufficient, 1087 out_credit_row_id, 1088 t.out_reserve_pub_reuse, 1089 t.out_mapping_reuse, 1090 t.out_unknown_mapping 1091 INTO 1092 out_debitor_balance_insufficient, 1093 out_tx_row_id, 1094 out_reserve_pub_reuse, 1095 out_mapping_reuse, 1096 out_unknown_mapping 1097 FROM make_incoming( 1098 exchange_bank_account_id, 1099 sender_bank_account_id, 1100 in_subject, 1101 in_amount, 1102 in_timestamp, 1103 in_type, 1104 in_key, 1105 NULL, 1106 NULL, 1107 NULL 1108 ) as t; 1109 END $$; 1110 COMMENT ON FUNCTION taler_add_incoming IS 'Create an incoming taler transaction and register it'; 1111 1112 CREATE FUNCTION bank_transaction( 1113 IN in_credit_account_payto TEXT, 1114 IN in_debit_account_username TEXT, 1115 IN in_subject TEXT, 1116 IN in_amount taler_amount, 1117 IN in_timestamp INT8, 1118 IN in_is_tan BOOLEAN, 1119 IN in_request_uid BYTEA, 1120 IN in_wire_transfer_fees taler_amount, 1121 IN in_min_amount taler_amount, 1122 IN in_max_amount taler_amount, 1123 IN in_type taler_incoming_type, 1124 IN in_metadata BYTEA, 1125 IN in_bounce_cause TEXT, 1126 -- Error status 1127 OUT out_creditor_not_found BOOLEAN, 1128 OUT out_debtor_not_found BOOLEAN, 1129 OUT out_same_account BOOLEAN, 1130 OUT out_balance_insufficient BOOLEAN, 1131 OUT out_creditor_admin BOOLEAN, 1132 OUT out_tan_required BOOLEAN, 1133 OUT out_request_uid_reuse BOOLEAN, 1134 OUT out_bad_amount BOOLEAN, 1135 -- Success return 1136 OUT out_credit_bank_account_id INT8, 1137 OUT out_debit_bank_account_id INT8, 1138 OUT out_credit_row_id INT8, 1139 OUT out_debit_row_id INT8, 1140 OUT out_creditor_is_exchange BOOLEAN, 1141 OUT out_debtor_is_exchange BOOLEAN, 1142 OUT out_idempotent BOOLEAN 1143 ) 1144 LANGUAGE plpgsql AS $$ 1145 DECLARE 1146 local_reserve_pub_reuse BOOLEAN; 1147 local_mapping_reuse BOOLEAN; 1148 local_unknown_mapping BOOLEAN; 1149 BEGIN 1150 -- Find credit bank account id and check it's not admin 1151 SELECT bank_account_id, is_taler_exchange, username='admin' 1152 INTO out_credit_bank_account_id, out_creditor_is_exchange, out_creditor_admin 1153 FROM bank_accounts 1154 JOIN customers ON customer_id=owning_customer_id 1155 WHERE internal_payto = in_credit_account_payto AND deleted_at IS NULL; 1156 IF NOT FOUND OR out_creditor_admin THEN 1157 out_creditor_not_found=NOT FOUND; 1158 RETURN; 1159 END IF; 1160 -- Find debit bank account ID and check it's a different account and if 2FA is required 1161 SELECT bank_account_id, is_taler_exchange, out_credit_bank_account_id=bank_account_id, NOT in_is_tan AND cardinality(tan_channels) > 0 1162 INTO out_debit_bank_account_id, out_debtor_is_exchange, out_same_account, out_tan_required 1163 FROM bank_accounts 1164 JOIN customers ON customer_id=owning_customer_id 1165 WHERE username = in_debit_account_username AND deleted_at IS NULL; 1166 IF NOT FOUND OR out_same_account THEN 1167 out_debtor_not_found=NOT FOUND; 1168 RETURN; 1169 END IF; 1170 -- Check for idempotence and conflict 1171 IF in_request_uid IS NOT NULL THEN 1172 SELECT (amount != in_amount 1173 OR subject != in_subject 1174 OR bank_account_id != out_debit_bank_account_id), bank_transaction 1175 INTO out_request_uid_reuse, out_debit_row_id 1176 FROM bank_transaction_operations 1177 JOIN bank_account_transactions ON bank_transaction = bank_transaction_id 1178 WHERE request_uid = in_request_uid; 1179 IF found OR out_tan_required THEN 1180 out_idempotent = found AND NOT out_request_uid_reuse; 1181 RETURN; 1182 END IF; 1183 ELSIF out_tan_required THEN 1184 RETURN; 1185 END IF; 1186 1187 -- Try to perform an incoming transfer 1188 IF out_creditor_is_exchange AND NOT out_debtor_is_exchange AND in_bounce_cause IS NULL THEN 1189 -- Perform an incoming transfer 1190 SELECT 1191 transfer.out_balance_insufficient, 1192 transfer.out_bad_amount, 1193 transfer.out_credit_row_id, 1194 transfer.out_debit_row_id, 1195 out_reserve_pub_reuse, 1196 out_mapping_reuse, 1197 out_unknown_mapping 1198 INTO 1199 out_balance_insufficient, 1200 out_bad_amount, 1201 out_credit_row_id, 1202 out_debit_row_id, 1203 local_reserve_pub_reuse, 1204 local_mapping_reuse, 1205 local_unknown_mapping 1206 FROM make_incoming( 1207 out_credit_bank_account_id, 1208 out_debit_bank_account_id, 1209 in_subject, 1210 in_amount, 1211 in_timestamp, 1212 in_type, 1213 in_metadata, 1214 in_wire_transfer_fees, 1215 in_min_amount, 1216 in_max_amount 1217 ) as transfer; 1218 IF out_balance_insufficient OR out_bad_amount THEN 1219 RETURN; 1220 END IF; 1221 IF local_reserve_pub_reuse THEN 1222 in_bounce_cause = 'reserve public key reuse'; 1223 ELSIF local_mapping_reuse THEN 1224 in_bounce_cause = 'mapping public key reuse'; 1225 ELSIF local_unknown_mapping THEN 1226 in_bounce_cause = 'unknown mapping public key'; 1227 END IF; 1228 END IF; 1229 1230 IF out_credit_row_id IS NULL THEN 1231 -- Perform common bank transfer 1232 SELECT 1233 transfer.out_balance_insufficient, 1234 transfer.out_bad_amount, 1235 transfer.out_credit_row_id, 1236 transfer.out_debit_row_id 1237 INTO 1238 out_balance_insufficient, 1239 out_bad_amount, 1240 out_credit_row_id, 1241 out_debit_row_id 1242 FROM bank_wire_transfer( 1243 out_credit_bank_account_id, 1244 out_debit_bank_account_id, 1245 in_subject, 1246 in_amount, 1247 in_timestamp, 1248 in_wire_transfer_fees, 1249 in_min_amount, 1250 in_max_amount 1251 ) as transfer; 1252 IF out_balance_insufficient OR out_bad_amount THEN 1253 RETURN; 1254 END IF; 1255 END IF; 1256 1257 -- Bounce if necessary 1258 IF out_creditor_is_exchange AND in_bounce_cause IS NOT NULL THEN 1259 PERFORM bounce(out_debit_bank_account_id, out_credit_row_id, in_bounce_cause, in_timestamp); 1260 END IF; 1261 1262 -- Store operation 1263 IF in_request_uid IS NOT NULL THEN 1264 INSERT INTO bank_transaction_operations (request_uid, bank_transaction) 1265 VALUES (in_request_uid, out_debit_row_id); 1266 END IF; 1267 END $$; 1268 COMMENT ON FUNCTION bank_transaction IS 'Create a bank transaction'; 1269 1270 CREATE FUNCTION create_taler_withdrawal( 1271 IN in_account_username TEXT, 1272 IN in_withdrawal_uuid UUID, 1273 IN in_amount taler_amount, 1274 IN in_suggested_amount taler_amount, 1275 IN in_no_amount_to_wallet BOOLEAN, 1276 IN in_timestamp INT8, 1277 IN in_wire_transfer_fees taler_amount, 1278 IN in_min_amount taler_amount, 1279 IN in_max_amount taler_amount, 1280 -- Error status 1281 OUT out_account_not_found BOOLEAN, 1282 OUT out_account_is_exchange BOOLEAN, 1283 OUT out_balance_insufficient BOOLEAN, 1284 OUT out_bad_amount BOOLEAN 1285 ) 1286 LANGUAGE plpgsql AS $$ 1287 DECLARE 1288 account_id INT8; 1289 amount_with_fee taler_amount; 1290 BEGIN 1291 IF in_account_username IS NOT NULL THEN 1292 -- Check account exists 1293 SELECT bank_account_id, is_taler_exchange 1294 INTO account_id, out_account_is_exchange 1295 FROM bank_accounts 1296 JOIN customers ON bank_accounts.owning_customer_id = customers.customer_id 1297 WHERE username=in_account_username AND deleted_at IS NULL; 1298 out_account_not_found=NOT FOUND; 1299 IF out_account_not_found OR out_account_is_exchange THEN 1300 RETURN; 1301 END IF; 1302 1303 -- Check enough funds 1304 IF in_amount IS NOT NULL OR in_suggested_amount IS NOT NULL THEN 1305 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1306 account_id, 1307 COALESCE(in_amount, in_suggested_amount), 1308 in_wire_transfer_fees, 1309 in_min_amount, 1310 in_max_amount 1311 ) AS test INTO out_balance_insufficient, out_bad_amount; 1312 IF out_balance_insufficient OR out_bad_amount THEN 1313 RETURN; 1314 END IF; 1315 END IF; 1316 END IF; 1317 1318 -- Create withdrawal operation 1319 INSERT INTO taler_withdrawal_operations ( 1320 withdrawal_uuid, 1321 wallet_bank_account, 1322 amount, 1323 suggested_amount, 1324 no_amount_to_wallet, 1325 type, 1326 creation_date 1327 ) VALUES ( 1328 in_withdrawal_uuid, 1329 account_id, 1330 in_amount, 1331 in_suggested_amount, 1332 in_no_amount_to_wallet, 1333 'reserve', 1334 in_timestamp 1335 ); 1336 END $$; 1337 COMMENT ON FUNCTION create_taler_withdrawal IS 'Create a new withdrawal operation'; 1338 1339 CREATE FUNCTION select_taler_withdrawal( 1340 IN in_withdrawal_uuid uuid, 1341 IN in_reserve_pub BYTEA, 1342 IN in_subject TEXT, 1343 IN in_selected_exchange_payto TEXT, 1344 IN in_amount taler_amount, 1345 IN in_wire_transfer_fees taler_amount, 1346 IN in_min_amount taler_amount, 1347 IN in_max_amount taler_amount, 1348 -- Error status 1349 OUT out_no_op BOOLEAN, 1350 OUT out_already_selected BOOLEAN, 1351 OUT out_reserve_pub_reuse BOOLEAN, 1352 OUT out_account_not_found BOOLEAN, 1353 OUT out_account_is_not_exchange BOOLEAN, 1354 OUT out_amount_differs BOOLEAN, 1355 OUT out_balance_insufficient BOOLEAN, 1356 OUT out_bad_amount BOOLEAN, 1357 OUT out_aborted BOOLEAN, 1358 -- Success return 1359 OUT out_status TEXT 1360 ) 1361 LANGUAGE plpgsql AS $$ 1362 DECLARE 1363 selected BOOLEAN; 1364 account_id INT8; 1365 exchange_account_id INT8; 1366 amount_with_fee taler_amount; 1367 BEGIN 1368 -- Check exchange account 1369 SELECT bank_account_id, NOT is_taler_exchange 1370 INTO exchange_account_id, out_account_is_not_exchange 1371 FROM bank_accounts 1372 WHERE internal_payto=in_selected_exchange_payto; 1373 out_account_not_found=NOT FOUND; 1374 IF out_account_not_found OR out_account_is_not_exchange THEN 1375 RETURN; 1376 END IF; 1377 1378 -- Check for conflict and idempotence 1379 SELECT 1380 selection_done, 1381 aborted, 1382 CASE 1383 WHEN confirmation_done THEN 'confirmed' 1384 ELSE 'selected' 1385 END, 1386 selection_done 1387 AND (exchange_bank_account != exchange_account_id OR reserve_pub != in_reserve_pub OR amount != in_amount), 1388 amount != in_amount, 1389 wallet_bank_account 1390 INTO selected, out_aborted, out_status, out_already_selected, out_amount_differs, account_id 1391 FROM taler_withdrawal_operations 1392 WHERE withdrawal_uuid=in_withdrawal_uuid; 1393 out_no_op = NOT FOUND; 1394 IF out_no_op OR out_aborted OR out_already_selected OR out_amount_differs OR selected THEN 1395 RETURN; 1396 END IF; 1397 1398 -- Check reserve_pub reuse 1399 out_reserve_pub_reuse=EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_reserve_pub AND type = 'reserve') OR 1400 EXISTS(SELECT FROM taler_withdrawal_operations WHERE reserve_pub = in_reserve_pub AND type = 'reserve'); 1401 IF out_reserve_pub_reuse THEN 1402 RETURN; 1403 END IF; 1404 1405 IF in_amount IS NOT NULL THEN 1406 SELECT test.out_balance_insufficient, test.out_bad_amount FROM account_balance_is_sufficient( 1407 account_id, 1408 in_amount, 1409 in_wire_transfer_fees, 1410 in_min_amount, 1411 in_max_amount 1412 ) AS test INTO out_balance_insufficient, out_bad_amount; 1413 IF out_balance_insufficient OR out_bad_amount THEN 1414 RETURN; 1415 END IF; 1416 END IF; 1417 1418 -- Update withdrawal operation 1419 UPDATE taler_withdrawal_operations 1420 SET exchange_bank_account=exchange_account_id, 1421 reserve_pub=in_reserve_pub, 1422 subject=in_subject, 1423 selection_done=true, 1424 amount=COALESCE(amount, in_amount) 1425 WHERE withdrawal_uuid=in_withdrawal_uuid; 1426 1427 -- Notify status change 1428 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' selected'); 1429 END $$; 1430 COMMENT ON FUNCTION select_taler_withdrawal IS 'Set details of a withdrawal operation'; 1431 1432 CREATE FUNCTION abort_taler_withdrawal( 1433 IN in_withdrawal_uuid uuid, 1434 OUT out_no_op BOOLEAN, 1435 OUT out_already_confirmed BOOLEAN 1436 ) 1437 LANGUAGE plpgsql AS $$ 1438 BEGIN 1439 UPDATE taler_withdrawal_operations 1440 SET aborted = NOT confirmation_done 1441 WHERE withdrawal_uuid=in_withdrawal_uuid 1442 RETURNING confirmation_done 1443 INTO out_already_confirmed; 1444 IF NOT FOUND OR out_already_confirmed THEN 1445 out_no_op=NOT FOUND; 1446 RETURN; 1447 END IF; 1448 1449 -- Notify status change 1450 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' aborted'); 1451 END $$; 1452 COMMENT ON FUNCTION abort_taler_withdrawal IS 'Abort a withdrawal operation.'; 1453 1454 CREATE FUNCTION confirm_taler_withdrawal( 1455 IN in_username TEXT, 1456 IN in_withdrawal_uuid uuid, 1457 IN in_timestamp INT8, 1458 IN in_is_tan BOOLEAN, 1459 IN in_wire_transfer_fees taler_amount, 1460 IN in_min_amount taler_amount, 1461 IN in_max_amount taler_amount, 1462 IN in_amount taler_amount, 1463 OUT out_no_op BOOLEAN, 1464 OUT out_balance_insufficient BOOLEAN, 1465 OUT out_reserve_pub_reuse BOOLEAN, 1466 OUT out_bad_amount BOOLEAN, 1467 OUT out_creditor_not_found BOOLEAN, 1468 OUT out_not_selected BOOLEAN, 1469 OUT out_missing_amount BOOLEAN, 1470 OUT out_amount_differs BOOLEAN, 1471 OUT out_aborted BOOLEAN, 1472 OUT out_tan_required BOOLEAN 1473 ) 1474 LANGUAGE plpgsql AS $$ 1475 DECLARE 1476 already_confirmed BOOLEAN; 1477 subject_local TEXT; 1478 reserve_pub_local BYTEA; 1479 wallet_bank_account_local INT8; 1480 amount_local taler_amount; 1481 exchange_bank_account_id INT8; 1482 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 INTO out_balance_insufficient, out_bad_amount, out_reserve_pub_reuse 1522 FROM make_incoming( 1523 exchange_bank_account_id, 1524 wallet_bank_account_local, 1525 subject_local, 1526 amount_local, 1527 in_timestamp, 1528 local_type, 1529 reserve_pub_local, 1530 in_wire_transfer_fees, 1531 in_min_amount, 1532 in_max_amount 1533 ) as transfer; 1534 IF out_balance_insufficient OR out_reserve_pub_reuse OR out_bad_amount THEN 1535 RETURN; 1536 END IF; 1537 1538 -- Confirm operation and update amount 1539 UPDATE taler_withdrawal_operations 1540 SET amount=amount_local, confirmation_done=true 1541 WHERE withdrawal_uuid=in_withdrawal_uuid; 1542 1543 -- Notify status change 1544 PERFORM pg_notify('bank_withdrawal_status', in_withdrawal_uuid::text || ' confirmed'); 1545 END $$; 1546 COMMENT ON FUNCTION confirm_taler_withdrawal 1547 IS 'Set a withdrawal operation as confirmed and wire the funds to the exchange.'; 1548 1549 CREATE FUNCTION cashin( 1550 IN in_timestamp INT8, 1551 IN in_reserve_pub BYTEA, 1552 IN in_amount taler_amount, 1553 IN in_subject TEXT, 1554 -- Error status 1555 OUT out_no_account BOOLEAN, 1556 OUT out_too_small BOOLEAN, 1557 OUT out_balance_insufficient BOOLEAN 1558 ) 1559 LANGUAGE plpgsql AS $$ 1560 DECLARE 1561 converted_amount taler_amount; 1562 admin_account_id INT8; 1563 exchange_account_id INT8; 1564 exchange_conversion_rate_class_id INT8; 1565 tx_row_id INT8; 1566 BEGIN 1567 -- TODO check reserve_pub reuse ? 1568 1569 -- Recover exchange account info 1570 SELECT bank_account_id, conversion_rate_class_id 1571 INTO exchange_account_id, exchange_conversion_rate_class_id 1572 FROM bank_accounts 1573 JOIN customers 1574 ON customer_id=owning_customer_id 1575 WHERE username = 'exchange'; 1576 IF NOT FOUND THEN 1577 out_no_account = true; 1578 RETURN; 1579 END IF; 1580 1581 -- Retrieve admin account id 1582 SELECT bank_account_id 1583 INTO admin_account_id 1584 FROM bank_accounts 1585 JOIN customers 1586 ON customer_id=owning_customer_id 1587 WHERE username = 'admin'; 1588 1589 -- Perform conversion 1590 SELECT (converted).val, (converted).frac, too_small 1591 INTO converted_amount.val, converted_amount.frac, out_too_small 1592 FROM conversion_to(in_amount, 'cashin'::text, exchange_conversion_rate_class_id); 1593 IF out_too_small THEN 1594 RETURN; 1595 END IF; 1596 1597 -- Perform incoming transaction 1598 SELECT 1599 transfer.out_balance_insufficient, 1600 transfer.out_credit_row_id 1601 INTO 1602 out_balance_insufficient, 1603 tx_row_id 1604 FROM make_incoming( 1605 exchange_account_id, 1606 admin_account_id, 1607 in_subject, 1608 converted_amount, 1609 in_timestamp, 1610 'reserve'::taler_incoming_type, 1611 in_reserve_pub, 1612 NULL, 1613 NULL, 1614 NULL 1615 ) as transfer; 1616 IF out_balance_insufficient THEN 1617 RETURN; 1618 END IF; 1619 1620 -- update stats 1621 CALL stats_register_payment('cashin', NULL, converted_amount, in_amount); 1622 1623 END $$; 1624 COMMENT ON FUNCTION cashin IS 'Perform a cashin operation'; 1625 1626 1627 CREATE FUNCTION cashout_create( 1628 IN in_username TEXT, 1629 IN in_request_uid BYTEA, 1630 IN in_amount_debit taler_amount, 1631 IN in_amount_credit taler_amount, 1632 IN in_subject TEXT, 1633 IN in_timestamp INT8, 1634 IN in_is_tan BOOLEAN, 1635 -- Error status 1636 OUT out_bad_conversion BOOLEAN, 1637 OUT out_account_not_found BOOLEAN, 1638 OUT out_account_is_exchange BOOLEAN, 1639 OUT out_balance_insufficient BOOLEAN, 1640 OUT out_request_uid_reuse BOOLEAN, 1641 OUT out_no_cashout_payto BOOLEAN, 1642 OUT out_tan_required BOOLEAN, 1643 OUT out_under_min BOOLEAN, 1644 -- Success return 1645 OUT out_cashout_id INT8 1646 ) 1647 LANGUAGE plpgsql AS $$ 1648 DECLARE 1649 account_id INT8; 1650 account_conversion_rate_class_id INT8; 1651 account_cashout_payto TEXT; 1652 admin_account_id INT8; 1653 tx_id INT8; 1654 BEGIN 1655 1656 -- Check account exists, has all info and if 2FA is required 1657 SELECT 1658 bank_account_id, is_taler_exchange, conversion_rate_class_id, 1659 -- Remove potential residual query string an add the receiver_name 1660 split_part(cashout_payto, '?', 1) || '?receiver-name=' || url_encode(name), 1661 NOT in_is_tan AND cardinality(tan_channels) > 0 1662 INTO 1663 account_id, out_account_is_exchange, account_conversion_rate_class_id, 1664 account_cashout_payto, out_tan_required 1665 FROM bank_accounts 1666 JOIN customers ON owning_customer_id=customer_id 1667 WHERE username=in_username; 1668 IF NOT FOUND THEN 1669 out_account_not_found=TRUE; 1670 RETURN; 1671 ELSIF account_cashout_payto IS NULL THEN 1672 out_no_cashout_payto=TRUE; 1673 RETURN; 1674 ELSIF out_account_is_exchange THEN 1675 RETURN; 1676 END IF; 1677 1678 -- check conversion 1679 SELECT under_min, too_small OR in_amount_credit!=converted 1680 INTO out_under_min, out_bad_conversion 1681 FROM conversion_to(in_amount_debit, 'cashout'::text, account_conversion_rate_class_id); 1682 IF out_bad_conversion THEN 1683 RETURN; 1684 END IF; 1685 1686 -- Retrieve admin account id 1687 SELECT bank_account_id 1688 INTO admin_account_id 1689 FROM bank_accounts 1690 JOIN customers 1691 ON customer_id=owning_customer_id 1692 WHERE username = 'admin'; 1693 1694 -- Check for idempotence and conflict 1695 SELECT (amount_debit != in_amount_debit 1696 OR subject != in_subject 1697 OR bank_account != account_id) 1698 , cashout_id 1699 INTO out_request_uid_reuse, out_cashout_id 1700 FROM cashout_operations 1701 WHERE request_uid = in_request_uid; 1702 IF found OR out_request_uid_reuse OR out_tan_required THEN 1703 RETURN; 1704 END IF; 1705 1706 -- Perform bank wire transfer 1707 SELECT transfer.out_balance_insufficient, out_debit_row_id 1708 INTO out_balance_insufficient, tx_id 1709 FROM bank_wire_transfer( 1710 admin_account_id, 1711 account_id, 1712 in_subject, 1713 in_amount_debit, 1714 in_timestamp, 1715 NULL, 1716 NULL, 1717 NULL 1718 ) as transfer; 1719 IF out_balance_insufficient THEN 1720 RETURN; 1721 END IF; 1722 1723 -- Create cashout operation 1724 INSERT INTO cashout_operations ( 1725 request_uid 1726 ,amount_debit 1727 ,amount_credit 1728 ,creation_time 1729 ,bank_account 1730 ,subject 1731 ,local_transaction 1732 ) VALUES ( 1733 in_request_uid 1734 ,in_amount_debit 1735 ,in_amount_credit 1736 ,in_timestamp 1737 ,account_id 1738 ,in_subject 1739 ,tx_id 1740 ) RETURNING cashout_id INTO out_cashout_id; 1741 1742 -- Initiate libeufin-nexus transaction 1743 INSERT INTO libeufin_nexus.initiated_outgoing_transactions ( 1744 amount 1745 ,subject 1746 ,credit_payto 1747 ,initiation_time 1748 ,end_to_end_id 1749 ) VALUES ( 1750 ((in_amount_credit).val, (in_amount_credit).frac)::libeufin_nexus.taler_amount 1751 ,in_subject 1752 ,account_cashout_payto 1753 ,in_timestamp 1754 ,libeufin_nexus.ebics_id_gen() 1755 ); 1756 1757 -- update stats 1758 CALL stats_register_payment('cashout', NULL, in_amount_debit, in_amount_credit); 1759 END $$; 1760 1761 CREATE FUNCTION tan_challenge_mark_sent ( 1762 IN in_uuid UUID, 1763 IN in_timestamp INT8, 1764 IN in_retransmission_period INT8 1765 ) RETURNS void 1766 LANGUAGE sql AS $$ 1767 UPDATE tan_challenges SET 1768 retransmission_date = in_timestamp + in_retransmission_period 1769 WHERE uuid = in_uuid; 1770 $$; 1771 COMMENT ON FUNCTION tan_challenge_mark_sent IS 'Register a challenge as successfully sent'; 1772 1773 CREATE FUNCTION tan_challenge_try ( 1774 IN in_uuid UUID, 1775 IN in_code TEXT, 1776 IN in_timestamp INT8, 1777 -- Error status 1778 OUT out_ok BOOLEAN, 1779 OUT out_no_op BOOLEAN, 1780 OUT out_no_retry BOOLEAN, 1781 OUT out_expired BOOLEAN, 1782 -- Success return 1783 OUT out_op op_enum, 1784 OUT out_channel tan_enum, 1785 OUT out_info TEXT 1786 ) 1787 LANGUAGE plpgsql as $$ 1788 DECLARE 1789 account_id INT8; 1790 token_creation BOOLEAN; 1791 BEGIN 1792 1793 -- Try to solve challenge 1794 UPDATE tan_challenges SET 1795 confirmation_date = CASE 1796 WHEN (retry_counter > 0 AND in_timestamp < expiration_date AND code = in_code) THEN in_timestamp 1797 ELSE confirmation_date 1798 END, 1799 retry_counter = retry_counter - 1 1800 WHERE uuid = in_uuid 1801 RETURNING 1802 confirmation_date IS NOT NULL, 1803 retry_counter <= 0 AND confirmation_date IS NULL, 1804 in_timestamp >= expiration_date AND confirmation_date IS NULL, 1805 op = 'create_token', 1806 customer 1807 INTO out_ok, out_no_retry, out_expired, token_creation, account_id; 1808 out_no_op = NOT FOUND; 1809 1810 IF NOT out_ok AND token_creation THEN 1811 UPDATE customers SET token_creation_counter=token_creation_counter+1 WHERE customer_id=account_id; 1812 END IF; 1813 1814 IF out_no_op OR NOT out_ok OR out_no_retry OR out_expired THEN 1815 RETURN; 1816 END IF; 1817 1818 -- Recover body and op from challenge 1819 SELECT op, tan_channel, tan_info 1820 INTO out_op, out_channel, out_info 1821 FROM tan_challenges WHERE uuid = in_uuid; 1822 END $$; 1823 COMMENT ON FUNCTION tan_challenge_try IS 'Try to confirm a challenge, return true if the challenge have been confirmed'; 1824 1825 CREATE FUNCTION stats_get_frame( 1826 IN date TIMESTAMP, 1827 IN in_timeframe stat_timeframe_enum, 1828 OUT cashin_count INT8, 1829 OUT cashin_regional_volume taler_amount, 1830 OUT cashin_fiat_volume taler_amount, 1831 OUT cashout_count INT8, 1832 OUT cashout_regional_volume taler_amount, 1833 OUT cashout_fiat_volume taler_amount, 1834 OUT taler_in_count INT8, 1835 OUT taler_in_volume taler_amount, 1836 OUT taler_out_count INT8, 1837 OUT taler_out_volume taler_amount 1838 ) 1839 LANGUAGE plpgsql AS $$ 1840 BEGIN 1841 date = date_trunc(in_timeframe::text, date); 1842 SELECT 1843 s.cashin_count 1844 ,(s.cashin_regional_volume).val 1845 ,(s.cashin_regional_volume).frac 1846 ,(s.cashin_fiat_volume).val 1847 ,(s.cashin_fiat_volume).frac 1848 ,s.cashout_count 1849 ,(s.cashout_regional_volume).val 1850 ,(s.cashout_regional_volume).frac 1851 ,(s.cashout_fiat_volume).val 1852 ,(s.cashout_fiat_volume).frac 1853 ,s.taler_in_count 1854 ,(s.taler_in_volume).val 1855 ,(s.taler_in_volume).frac 1856 ,s.taler_out_count 1857 ,(s.taler_out_volume).val 1858 ,(s.taler_out_volume).frac 1859 INTO 1860 cashin_count 1861 ,cashin_regional_volume.val 1862 ,cashin_regional_volume.frac 1863 ,cashin_fiat_volume.val 1864 ,cashin_fiat_volume.frac 1865 ,cashout_count 1866 ,cashout_regional_volume.val 1867 ,cashout_regional_volume.frac 1868 ,cashout_fiat_volume.val 1869 ,cashout_fiat_volume.frac 1870 ,taler_in_count 1871 ,taler_in_volume.val 1872 ,taler_in_volume.frac 1873 ,taler_out_count 1874 ,taler_out_volume.val 1875 ,taler_out_volume.frac 1876 FROM bank_stats AS s 1877 WHERE s.timeframe = in_timeframe 1878 AND s.start_time = date; 1879 END $$; 1880 1881 CREATE PROCEDURE stats_register_payment( 1882 IN name TEXT, 1883 IN now TIMESTAMP, 1884 IN regional_amount taler_amount, 1885 IN fiat_amount taler_amount 1886 ) 1887 LANGUAGE plpgsql AS $$ 1888 BEGIN 1889 IF now IS NULL THEN 1890 now = timezone('utc', now())::TIMESTAMP; 1891 END IF; 1892 IF name = 'taler_in' THEN 1893 INSERT INTO bank_stats AS s ( 1894 timeframe, 1895 start_time, 1896 taler_in_count, 1897 taler_in_volume 1898 ) SELECT 1899 frame, 1900 date_trunc(frame::text, now), 1901 1, 1902 regional_amount 1903 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1904 ON CONFLICT (timeframe, start_time) DO UPDATE 1905 SET taler_in_count=s.taler_in_count+1, 1906 taler_in_volume=(SELECT amount_add(s.taler_in_volume, regional_amount)); 1907 ELSIF name = 'taler_out' THEN 1908 INSERT INTO bank_stats AS s ( 1909 timeframe, 1910 start_time, 1911 taler_out_count, 1912 taler_out_volume 1913 ) SELECT 1914 frame, 1915 date_trunc(frame::text, now), 1916 1, 1917 regional_amount 1918 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1919 ON CONFLICT (timeframe, start_time) DO UPDATE 1920 SET taler_out_count=s.taler_out_count+1, 1921 taler_out_volume=(SELECT amount_add(s.taler_out_volume, regional_amount)); 1922 ELSIF name = 'cashin' THEN 1923 INSERT INTO bank_stats AS s ( 1924 timeframe, 1925 start_time, 1926 cashin_count, 1927 cashin_regional_volume, 1928 cashin_fiat_volume 1929 ) SELECT 1930 frame, 1931 date_trunc(frame::text, now), 1932 1, 1933 regional_amount, 1934 fiat_amount 1935 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1936 ON CONFLICT (timeframe, start_time) DO UPDATE 1937 SET cashin_count=s.cashin_count+1, 1938 cashin_regional_volume=(SELECT amount_add(s.cashin_regional_volume, regional_amount)), 1939 cashin_fiat_volume=(SELECT amount_add(s.cashin_fiat_volume, fiat_amount)); 1940 ELSIF name = 'cashout' THEN 1941 INSERT INTO bank_stats AS s ( 1942 timeframe, 1943 start_time, 1944 cashout_count, 1945 cashout_regional_volume, 1946 cashout_fiat_volume 1947 ) SELECT 1948 frame, 1949 date_trunc(frame::text, now), 1950 1, 1951 regional_amount, 1952 fiat_amount 1953 FROM unnest(enum_range(null::stat_timeframe_enum)) AS frame 1954 ON CONFLICT (timeframe, start_time) DO UPDATE 1955 SET cashout_count=s.cashout_count+1, 1956 cashout_regional_volume=(SELECT amount_add(s.cashout_regional_volume, regional_amount)), 1957 cashout_fiat_volume=(SELECT amount_add(s.cashout_fiat_volume, fiat_amount)); 1958 ELSE 1959 RAISE EXCEPTION 'Unknown stat %', name; 1960 END IF; 1961 END $$; 1962 1963 CREATE FUNCTION conversion_apply_ratio( 1964 IN amount taler_amount 1965 ,IN ratio taler_amount 1966 ,IN fee taler_amount 1967 ,IN tiny taler_amount -- Result is rounded to this amount 1968 ,IN rounding rounding_mode -- With this rounding mode 1969 ,OUT result taler_amount 1970 ,OUT out_too_small BOOLEAN 1971 ) 1972 LANGUAGE plpgsql IMMUTABLE AS $$ 1973 DECLARE 1974 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 1975 tiny_numeric NUMERIC(24); 1976 BEGIN 1977 -- Handle no config case 1978 IF ratio = (0, 0)::taler_amount THEN 1979 out_too_small=TRUE; 1980 RETURN; 1981 END IF; 1982 1983 -- Perform multiplication using big numbers 1984 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) * (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 1985 1986 -- Apply fees 1987 amount_numeric = amount_numeric - (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 1988 IF (sign(amount_numeric) != 1) THEN 1989 out_too_small = TRUE; 1990 result = (0, 0); 1991 RETURN; 1992 END IF; 1993 1994 -- Round to tiny amounts 1995 tiny_numeric = (tiny.val::numeric(24) * 100000000 + tiny.frac::numeric(24)); 1996 case rounding 1997 when 'zero' then amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 1998 when 'up' then amount_numeric = ceil(amount_numeric / tiny_numeric) * tiny_numeric; 1999 when 'nearest' then amount_numeric = round(amount_numeric / tiny_numeric) * tiny_numeric; 2000 end case; 2001 2002 -- Extract product parts 2003 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2004 2005 IF (result.val > 1::INT8<<52) THEN 2006 RAISE EXCEPTION 'amount value overflowed'; 2007 END IF; 2008 END $$; 2009 COMMENT ON FUNCTION conversion_apply_ratio 2010 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'; 2011 2012 CREATE FUNCTION conversion_revert_ratio( 2013 IN amount taler_amount 2014 ,IN ratio taler_amount 2015 ,IN fee taler_amount 2016 ,IN tiny taler_amount -- Result is rounded to this amount 2017 ,IN rounding rounding_mode -- With this rounding mode 2018 ,IN reverse_tiny taler_amount 2019 ,OUT result taler_amount 2020 ,OUT bad_value BOOLEAN 2021 ) 2022 LANGUAGE plpgsql IMMUTABLE AS $$ 2023 DECLARE 2024 amount_numeric NUMERIC(33, 8); -- 16 digit for val, 8 for frac and 1 for rounding error 2025 tiny_numeric NUMERIC(24); 2026 roundtrip BOOLEAN; 2027 BEGIN 2028 -- Handle no config case 2029 IF ratio = (0, 0)::taler_amount THEN 2030 bad_value=TRUE; 2031 RETURN; 2032 END IF; 2033 2034 -- Apply fees 2035 amount_numeric = (amount.val::numeric(24) * 100000000 + amount.frac::numeric(24)) + (fee.val::numeric(24) * 100000000 + fee.frac::numeric(24)); 2036 2037 -- Perform division using big numbers 2038 amount_numeric = amount_numeric / (ratio.val::numeric(24, 8) + ratio.frac::numeric(24, 8) / 100000000); 2039 2040 -- Round to input digits 2041 tiny_numeric = (reverse_tiny.val::numeric(24) * 100000000 + reverse_tiny.frac::numeric(24)); 2042 amount_numeric = trunc(amount_numeric / tiny_numeric) * tiny_numeric; 2043 2044 -- Extract division parts 2045 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2046 2047 -- Recover potentially lost tiny amount during rounding 2048 -- There must be a clever way to compute this but I am a little limited with math 2049 -- and revert ratio computation is not a hot function so I just use the apply ratio 2050 -- function to be conservative and correct 2051 SELECT ok INTO roundtrip FROM amount_left_minus_right((SELECT conversion_apply_ratio.result FROM conversion_apply_ratio(result, ratio, fee, tiny, rounding)), amount); 2052 IF NOT roundtrip THEN 2053 amount_numeric = amount_numeric + tiny_numeric; 2054 result = (trunc(amount_numeric / 100000000)::int8, (amount_numeric % 100000000)::int4); 2055 END IF; 2056 2057 IF (result.val > 1::INT8<<52) THEN 2058 RAISE EXCEPTION 'amount value overflowed'; 2059 END IF; 2060 END $$; 2061 COMMENT ON FUNCTION conversion_revert_ratio 2062 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'; 2063 2064 2065 CREATE FUNCTION conversion_to( 2066 IN amount taler_amount, 2067 IN direction TEXT, 2068 IN conversion_rate_class_id INT8, 2069 OUT converted taler_amount, 2070 OUT too_small BOOLEAN, 2071 OUT under_min BOOLEAN 2072 ) 2073 LANGUAGE plpgsql STABLE AS $$ 2074 DECLARE 2075 at_ratio taler_amount; 2076 out_fee taler_amount; 2077 tiny_amount taler_amount; 2078 min_amount taler_amount; 2079 mode rounding_mode; 2080 BEGIN 2081 -- Load rate 2082 IF direction='cashin' THEN 2083 SELECT 2084 (cashin_ratio).val, (cashin_ratio).frac, 2085 (cashin_fee).val, (cashin_fee).frac, 2086 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2087 (cashin_min_amount).val, (cashin_min_amount).frac, 2088 cashin_rounding_mode 2089 INTO 2090 at_ratio.val, at_ratio.frac, 2091 out_fee.val, out_fee.frac, 2092 tiny_amount.val, tiny_amount.frac, 2093 min_amount.val, min_amount.frac, 2094 mode 2095 FROM get_conversion_class_rate(conversion_rate_class_id); 2096 ELSE 2097 SELECT 2098 (cashout_ratio).val, (cashout_ratio).frac, 2099 (cashout_fee).val, (cashout_fee).frac, 2100 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2101 (cashout_min_amount).val, (cashout_min_amount).frac, 2102 cashout_rounding_mode 2103 INTO 2104 at_ratio.val, at_ratio.frac, 2105 out_fee.val, out_fee.frac, 2106 tiny_amount.val, tiny_amount.frac, 2107 min_amount.val, min_amount.frac, 2108 mode 2109 FROM get_conversion_class_rate(conversion_rate_class_id); 2110 END IF; 2111 2112 -- Check min amount 2113 SELECT NOT ok INTO too_small FROM amount_left_minus_right(amount, min_amount); 2114 IF too_small THEN 2115 under_min = true; 2116 converted = (0, 0); 2117 RETURN; 2118 END IF; 2119 2120 -- Perform conversion 2121 SELECT (result).val, (result).frac, out_too_small INTO converted.val, converted.frac, too_small 2122 FROM conversion_apply_ratio(amount, at_ratio, out_fee, tiny_amount, mode); 2123 END $$; 2124 2125 CREATE FUNCTION conversion_from( 2126 IN amount taler_amount, 2127 IN direction TEXT, 2128 IN conversion_rate_class_id INT8, 2129 OUT converted taler_amount, 2130 OUT too_small BOOLEAN, 2131 OUT under_min BOOLEAN 2132 ) 2133 LANGUAGE plpgsql STABLE AS $$ 2134 DECLARE 2135 ratio taler_amount; 2136 out_fee taler_amount; 2137 tiny_amount taler_amount; 2138 reverse_tiny_amount taler_amount; 2139 min_amount taler_amount; 2140 mode rounding_mode; 2141 BEGIN 2142 -- Load rate 2143 IF direction='cashin' THEN 2144 SELECT 2145 (cashin_ratio).val, (cashin_ratio).frac, 2146 (cashin_fee).val, (cashin_fee).frac, 2147 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2148 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2149 (cashin_min_amount).val, (cashin_min_amount).frac, 2150 cashin_rounding_mode 2151 INTO 2152 ratio.val, ratio.frac, 2153 out_fee.val, out_fee.frac, 2154 tiny_amount.val, tiny_amount.frac, 2155 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2156 min_amount.val, min_amount.frac, 2157 mode 2158 FROM get_conversion_class_rate(conversion_rate_class_id); 2159 ELSE 2160 SELECT 2161 (cashout_ratio).val, (cashout_ratio).frac, 2162 (cashout_fee).val, (cashout_fee).frac, 2163 (cashout_tiny_amount).val, (cashout_tiny_amount).frac, 2164 (cashin_tiny_amount).val, (cashin_tiny_amount).frac, 2165 (cashout_min_amount).val, (cashout_min_amount).frac, 2166 cashout_rounding_mode 2167 INTO 2168 ratio.val, ratio.frac, 2169 out_fee.val, out_fee.frac, 2170 tiny_amount.val, tiny_amount.frac, 2171 reverse_tiny_amount.val, reverse_tiny_amount.frac, 2172 min_amount.val, min_amount.frac, 2173 mode 2174 FROM get_conversion_class_rate(conversion_rate_class_id); 2175 END IF; 2176 2177 -- Perform conversion 2178 SELECT (result).val, (result).frac, bad_value INTO converted.val, converted.frac, too_small 2179 FROM conversion_revert_ratio(amount, ratio, out_fee, tiny_amount, mode, reverse_tiny_amount); 2180 IF too_small THEN 2181 RETURN; 2182 END IF; 2183 2184 -- Check min amount 2185 SELECT NOT ok INTO too_small FROM amount_left_minus_right(converted, min_amount); 2186 IF too_small THEN 2187 under_min = true; 2188 converted = (0, 0); 2189 END IF; 2190 END $$; 2191 2192 CREATE FUNCTION config_get_conversion_rate() 2193 RETURNS TABLE ( 2194 cashin_ratio taler_amount, 2195 cashin_fee taler_amount, 2196 cashin_tiny_amount taler_amount, 2197 cashin_min_amount taler_amount, 2198 cashin_rounding_mode rounding_mode, 2199 cashout_ratio taler_amount, 2200 cashout_fee taler_amount, 2201 cashout_tiny_amount taler_amount, 2202 cashout_min_amount taler_amount, 2203 cashout_rounding_mode rounding_mode 2204 ) 2205 LANGUAGE sql STABLE AS $$ 2206 SELECT 2207 (value->'cashin'->'ratio'->'val', value->'cashin'->'ratio'->'frac')::taler_amount, 2208 (value->'cashin'->'fee'->'val', value->'cashin'->'fee'->'frac')::taler_amount, 2209 (value->'cashin'->'tiny_amount'->'val', value->'cashin'->'tiny_amount'->'frac')::taler_amount, 2210 (value->'cashin'->'min_amount'->'val', value->'cashin'->'min_amount'->'frac')::taler_amount, 2211 (value->'cashin'->>'rounding_mode')::rounding_mode, 2212 (value->'cashout'->'ratio'->'val', value->'cashout'->'ratio'->'frac')::taler_amount, 2213 (value->'cashout'->'fee'->'val', value->'cashout'->'fee'->'frac')::taler_amount, 2214 (value->'cashout'->'tiny_amount'->'val', value->'cashout'->'tiny_amount'->'frac')::taler_amount, 2215 (value->'cashout'->'min_amount'->'val', value->'cashout'->'min_amount'->'frac')::taler_amount, 2216 (value->'cashout'->>'rounding_mode')::rounding_mode 2217 FROM config WHERE key='conversion_rate' 2218 UNION ALL 2219 SELECT (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode, 2220 (0, 0)::taler_amount, (0, 0)::taler_amount, (0, 1000000)::taler_amount, (0, 0)::taler_amount, 'zero'::rounding_mode 2221 LIMIT 1 2222 $$; 2223 2224 CREATE FUNCTION get_conversion_class_rate( 2225 IN in_conversion_rate_class_id INT8 2226 ) 2227 RETURNS TABLE ( 2228 cashin_ratio taler_amount, 2229 cashin_fee taler_amount, 2230 cashin_tiny_amount taler_amount, 2231 cashin_min_amount taler_amount, 2232 cashin_rounding_mode rounding_mode, 2233 cashout_ratio taler_amount, 2234 cashout_fee taler_amount, 2235 cashout_tiny_amount taler_amount, 2236 cashout_min_amount taler_amount, 2237 cashout_rounding_mode rounding_mode 2238 ) 2239 LANGUAGE sql STABLE AS $$ 2240 SELECT 2241 COALESCE(class.cashin_ratio, cfg.cashin_ratio), 2242 COALESCE(class.cashin_fee, cfg.cashin_fee), 2243 cashin_tiny_amount, 2244 COALESCE(class.cashin_min_amount, cfg.cashin_min_amount), 2245 COALESCE(class.cashin_rounding_mode, cfg.cashin_rounding_mode), 2246 COALESCE(class.cashout_ratio, cfg.cashout_ratio), 2247 COALESCE(class.cashout_fee, cfg.cashout_fee), 2248 cashout_tiny_amount, 2249 COALESCE(class.cashout_min_amount, cfg.cashout_min_amount), 2250 COALESCE(class.cashout_rounding_mode, cfg.cashout_rounding_mode) 2251 FROM config_get_conversion_rate() as cfg 2252 LEFT JOIN conversion_rate_classes as class 2253 ON (conversion_rate_class_id=in_conversion_rate_class_id) 2254 $$; 2255 2256 CREATE PROCEDURE config_set_conversion_rate( 2257 IN cashin_ratio taler_amount, 2258 IN cashin_fee taler_amount, 2259 IN cashin_tiny_amount taler_amount, 2260 IN cashin_min_amount taler_amount, 2261 IN cashin_rounding_mode rounding_mode, 2262 IN cashout_ratio taler_amount, 2263 IN cashout_fee taler_amount, 2264 IN cashout_tiny_amount taler_amount, 2265 IN cashout_min_amount taler_amount, 2266 IN cashout_rounding_mode rounding_mode 2267 ) 2268 LANGUAGE sql AS $$ 2269 INSERT INTO config (key, value) VALUES ('conversion_rate', jsonb_build_object( 2270 'cashin', jsonb_build_object( 2271 'ratio', jsonb_build_object('val', cashin_ratio.val, 'frac', cashin_ratio.frac), 2272 'fee', jsonb_build_object('val', cashin_fee.val, 'frac', cashin_fee.frac), 2273 'tiny_amount', jsonb_build_object('val', cashin_tiny_amount.val, 'frac', cashin_tiny_amount.frac), 2274 'min_amount', jsonb_build_object('val', cashin_min_amount.val, 'frac', cashin_min_amount.frac), 2275 'rounding_mode', cashin_rounding_mode 2276 ), 2277 'cashout', jsonb_build_object( 2278 'ratio', jsonb_build_object('val', cashout_ratio.val, 'frac', cashout_ratio.frac), 2279 'fee', jsonb_build_object('val', cashout_fee.val, 'frac', cashout_fee.frac), 2280 'tiny_amount', jsonb_build_object('val', cashout_tiny_amount.val, 'frac', cashout_tiny_amount.frac), 2281 'min_amount', jsonb_build_object('val', cashout_min_amount.val, 'frac', cashout_min_amount.frac), 2282 'rounding_mode', cashout_rounding_mode 2283 ) 2284 )) ON CONFLICT (key) DO UPDATE SET value = excluded.value 2285 $$; 2286 2287 CREATE FUNCTION register_prepared_transfers ( 2288 IN in_exchange_username TEXT, 2289 IN in_type taler_incoming_type, 2290 IN in_account_pub BYTEA, 2291 IN in_authorization_pub BYTEA, 2292 IN in_authorization_sig BYTEA, 2293 IN in_recurrent BOOLEAN, 2294 IN in_amount taler_amount, 2295 IN in_timestamp INT8, 2296 IN in_subject TEXT, 2297 -- Error status 2298 OUT out_unknown_account BOOLEAN, 2299 OUT out_not_exchange BOOLEAN, 2300 OUT out_reserve_pub_reuse BOOLEAN, 2301 -- Success status 2302 OUT out_withdrawal_uuid UUID 2303 ) 2304 LANGUAGE plpgsql AS $$ 2305 DECLARE 2306 local_withdrawal_id INT8; 2307 exchange_account_id INT8; 2308 talerable_tx INT8; 2309 idempotent BOOLEAN; 2310 BEGIN 2311 -- Retrieve exchange account if 2312 SELECT bank_account_id, NOT is_taler_exchange 2313 INTO exchange_account_id, out_not_exchange 2314 FROM bank_accounts 2315 JOIN customers ON customer_id=owning_customer_id 2316 WHERE username = in_exchange_username; 2317 out_unknown_account=NOT FOUND; 2318 if out_unknown_account OR out_not_exchange THEN RETURN; END IF; 2319 2320 -- Check idempotency 2321 SELECT withdrawal_uuid, prepared_transfers.type = in_type 2322 AND account_pub = in_account_pub 2323 AND recurrent = in_recurrent 2324 AND amount = in_amount 2325 INTO out_withdrawal_uuid, idempotent 2326 FROM prepared_transfers 2327 LEFT JOIN taler_withdrawal_operations USING (withdrawal_id) 2328 WHERE authorization_pub = in_authorization_pub; 2329 2330 -- Check idempotency and delay garbage collection 2331 IF FOUND AND idempotent THEN 2332 UPDATE prepared_transfers 2333 SET registered_at=in_timestamp 2334 WHERE authorization_pub=in_authorization_pub; 2335 RETURN; 2336 END IF; 2337 2338 -- Check reserve pub reuse 2339 out_reserve_pub_reuse=in_type = 'reserve' AND ( 2340 EXISTS(SELECT FROM taler_exchange_incoming WHERE metadata = in_account_pub AND type = 'reserve') 2341 ); 2342 IF out_reserve_pub_reuse THEN 2343 RETURN; 2344 END IF; 2345 2346 -- Create/replace withdrawal 2347 IF out_withdrawal_uuid IS NOT NULL THEN 2348 PERFORM abort_taler_withdrawal(out_withdrawal_uuid); 2349 END IF; 2350 out_withdrawal_uuid=null; 2351 2352 IF in_recurrent THEN 2353 -- Finalize one pending right now 2354 DELETE FROM pending_recurrent_incoming_transactions 2355 WHERE bank_transaction_id = ( 2356 SELECT bank_transaction_id 2357 FROM pending_recurrent_incoming_transactions 2358 JOIN bank_account_transactions USING (bank_transaction_id) 2359 WHERE authorization_pub = in_authorization_pub 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;