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