taler-exchange-httpd_transfers_get.c (20874B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2014-2018, 2021 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero 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 Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 /** 17 * @file taler-exchange-httpd_transfers_get.c 18 * @brief Handle wire transfer(s) GET requests 19 * @author Christian Grothoff 20 */ 21 #include "taler/platform.h" 22 #include <gnunet/gnunet_util_lib.h> 23 #include <jansson.h> 24 #include <microhttpd.h> 25 #include <pthread.h> 26 #include "taler/taler_signatures.h" 27 #include "taler-exchange-httpd_keys.h" 28 #include "taler-exchange-httpd_transfers_get.h" 29 #include "taler-exchange-httpd_responses.h" 30 #include "taler/taler_json_lib.h" 31 #include "taler/taler_mhd_lib.h" 32 33 34 /** 35 * Information about one of the transactions that was 36 * aggregated, to be returned in the /transfers response. 37 */ 38 struct AggregatedDepositDetail 39 { 40 41 /** 42 * We keep deposit details in a DLL. 43 */ 44 struct AggregatedDepositDetail *next; 45 46 /** 47 * We keep deposit details in a DLL. 48 */ 49 struct AggregatedDepositDetail *prev; 50 51 /** 52 * Hash of the contract terms. 53 */ 54 struct TALER_PrivateContractHashP h_contract_terms; 55 56 /** 57 * Coin's public key of the deposited coin. 58 */ 59 struct TALER_CoinSpendPublicKeyP coin_pub; 60 61 /** 62 * Total value of the coin in the deposit (after 63 * refunds). 64 */ 65 struct TALER_Amount deposit_value; 66 67 /** 68 * Fees charged by the exchange for the deposit of this coin (possibly after reduction due to refunds). 69 */ 70 struct TALER_Amount deposit_fee; 71 72 /** 73 * Total amount refunded for this coin. 74 */ 75 struct TALER_Amount refund_total; 76 }; 77 78 79 /** 80 * A merchant asked for transaction details about a wire transfer. 81 * Provide them. Generates the 200 reply. 82 * 83 * @param connection connection to the client 84 * @param total total amount that was transferred 85 * @param merchant_pub public key of the merchant 86 * @param payto_uri destination account 87 * @param wire_fee wire fee that was charged 88 * @param exec_time execution time of the wire transfer 89 * @param wdd_head linked list with details about the combined deposits 90 * @return MHD result code 91 */ 92 static MHD_RESULT 93 reply_transfer_details (struct MHD_Connection *connection, 94 const struct TALER_Amount *total, 95 const struct TALER_MerchantPublicKeyP *merchant_pub, 96 const struct TALER_FullPayto payto_uri, 97 const struct TALER_Amount *wire_fee, 98 struct GNUNET_TIME_Timestamp exec_time, 99 const struct AggregatedDepositDetail *wdd_head) 100 { 101 json_t *deposits; 102 struct GNUNET_HashContext *hash_context; 103 struct GNUNET_HashCode h_details; 104 struct TALER_ExchangePublicKeyP pub; 105 struct TALER_ExchangeSignatureP sig; 106 struct TALER_FullPaytoHashP h_payto; 107 108 deposits = json_array (); 109 GNUNET_assert (NULL != deposits); 110 hash_context = GNUNET_CRYPTO_hash_context_start (); 111 for (const struct AggregatedDepositDetail *wdd_pos = wdd_head; 112 NULL != wdd_pos; 113 wdd_pos = wdd_pos->next) 114 { 115 TALER_exchange_online_wire_deposit_append (hash_context, 116 &wdd_pos->h_contract_terms, 117 exec_time, 118 &wdd_pos->coin_pub, 119 &wdd_pos->deposit_value, 120 &wdd_pos->deposit_fee); 121 if (0 != 122 json_array_append_new ( 123 deposits, 124 GNUNET_JSON_PACK ( 125 GNUNET_JSON_pack_data_auto ("h_contract_terms", 126 &wdd_pos->h_contract_terms), 127 GNUNET_JSON_pack_data_auto ("coin_pub", 128 &wdd_pos->coin_pub), 129 130 GNUNET_JSON_pack_allow_null ( 131 TALER_JSON_pack_amount ("refund_total", 132 TALER_amount_is_zero ( 133 &wdd_pos->refund_total) 134 ? NULL 135 : &wdd_pos->refund_total)), 136 TALER_JSON_pack_amount ("deposit_value", 137 &wdd_pos->deposit_value), 138 TALER_JSON_pack_amount ("deposit_fee", 139 &wdd_pos->deposit_fee)))) 140 { 141 json_decref (deposits); 142 GNUNET_CRYPTO_hash_context_abort (hash_context); 143 return TALER_MHD_reply_with_error (connection, 144 MHD_HTTP_INTERNAL_SERVER_ERROR, 145 TALER_EC_GENERIC_JSON_ALLOCATION_FAILURE, 146 "json_array_append_new() failed"); 147 } 148 } 149 GNUNET_CRYPTO_hash_context_finish (hash_context, 150 &h_details); 151 { 152 enum TALER_ErrorCode ec; 153 154 if (TALER_EC_NONE != 155 (ec = TALER_exchange_online_wire_deposit_sign ( 156 &TEH_keys_exchange_sign_, 157 total, 158 wire_fee, 159 merchant_pub, 160 payto_uri, 161 &h_details, 162 &pub, 163 &sig))) 164 { 165 json_decref (deposits); 166 return TALER_MHD_reply_with_ec (connection, 167 ec, 168 NULL); 169 } 170 } 171 172 TALER_full_payto_hash (payto_uri, 173 &h_payto); 174 return TALER_MHD_REPLY_JSON_PACK ( 175 connection, 176 MHD_HTTP_OK, 177 TALER_JSON_pack_amount ("total", 178 total), 179 TALER_JSON_pack_amount ("wire_fee", 180 wire_fee), 181 GNUNET_JSON_pack_data_auto ("merchant_pub", 182 merchant_pub), 183 GNUNET_JSON_pack_data_auto ("h_payto", 184 &h_payto), 185 GNUNET_JSON_pack_timestamp ("execution_time", 186 exec_time), 187 GNUNET_JSON_pack_array_steal ("deposits", 188 deposits), 189 GNUNET_JSON_pack_data_auto ("exchange_sig", 190 &sig), 191 GNUNET_JSON_pack_data_auto ("exchange_pub", 192 &pub)); 193 } 194 195 196 /** 197 * Closure for #handle_transaction_data. 198 */ 199 struct WtidTransactionContext 200 { 201 202 /** 203 * Identifier of the wire transfer to track. 204 */ 205 struct TALER_WireTransferIdentifierRawP wtid; 206 207 /** 208 * Total amount of the wire transfer, as calculated by 209 * summing up the individual amounts. To be rounded down 210 * to calculate the real transfer amount at the end. 211 * Only valid if @e is_valid is #GNUNET_YES. 212 */ 213 struct TALER_Amount total; 214 215 /** 216 * Public key of the merchant, only valid if @e is_valid 217 * is #GNUNET_YES. 218 */ 219 struct TALER_MerchantPublicKeyP merchant_pub; 220 221 /** 222 * Wire fees applicable at @e exec_time. 223 */ 224 struct TALER_WireFeeSet fees; 225 226 /** 227 * Execution time of the wire transfer 228 */ 229 struct GNUNET_TIME_Timestamp exec_time; 230 231 /** 232 * Head of DLL with deposit details for transfers GET response. 233 */ 234 struct AggregatedDepositDetail *wdd_head; 235 236 /** 237 * Tail of DLL with deposit details for transfers GET response. 238 */ 239 struct AggregatedDepositDetail *wdd_tail; 240 241 /** 242 * Where were the funds wired? 243 */ 244 struct TALER_FullPayto payto_uri; 245 246 /** 247 * JSON array with details about the individual deposits. 248 */ 249 json_t *deposits; 250 251 /** 252 * Initially #GNUNET_NO, if we found no deposits so far. Set to 253 * #GNUNET_YES if we got transaction data, and the database replies 254 * remained consistent with respect to @e merchant_pub and @e h_wire 255 * (as they should). Set to #GNUNET_SYSERR if we encountered an 256 * internal error. 257 */ 258 enum GNUNET_GenericReturnValue is_valid; 259 260 }; 261 262 263 /** 264 * Callback that totals up the applicable refunds. 265 * 266 * @param cls a `struct TALER_Amount` where we keep the total 267 * @param amount_with_fee amount being refunded 268 */ 269 static enum GNUNET_GenericReturnValue 270 add_refunds (void *cls, 271 const struct TALER_Amount *amount_with_fee) 272 273 { 274 struct TALER_Amount *total = cls; 275 276 if (0 > 277 TALER_amount_add (total, 278 total, 279 amount_with_fee)) 280 { 281 GNUNET_break (0); 282 return GNUNET_SYSERR; 283 } 284 return GNUNET_OK; 285 } 286 287 288 /** 289 * Function called with the results of the lookup of the individual deposits 290 * that were aggregated for the given wire transfer. 291 * 292 * @param cls our context for transmission 293 * @param rowid which row in the DB is the information from (for diagnostics), ignored 294 * @param merchant_pub public key of the merchant (should be same for all callbacks with the same @e cls) 295 * @param account_payto_uri where the funds were sent 296 * @param h_payto hash over @a account_payto_uri as it is in the DB 297 * @param exec_time execution time of the wire transfer (should be same for all callbacks with the same @e cls) 298 * @param h_contract_terms which proposal was this payment about 299 * @param denom_pub denomination public key of the @a coin_pub (ignored) 300 * @param coin_pub which public key was this payment about 301 * @param deposit_value amount contributed by this coin in total (including fee) 302 * @param deposit_fee deposit fee charged by exchange for this coin 303 */ 304 static void 305 handle_deposit_data ( 306 void *cls, 307 uint64_t rowid, 308 const struct TALER_MerchantPublicKeyP *merchant_pub, 309 const struct TALER_FullPayto account_payto_uri, 310 const struct TALER_FullPaytoHashP *h_payto, 311 struct GNUNET_TIME_Timestamp exec_time, 312 const struct TALER_PrivateContractHashP *h_contract_terms, 313 const struct TALER_DenominationPublicKey *denom_pub, 314 const struct TALER_CoinSpendPublicKeyP *coin_pub, 315 const struct TALER_Amount *deposit_value, 316 const struct TALER_Amount *deposit_fee) 317 { 318 struct WtidTransactionContext *ctx = cls; 319 struct TALER_Amount total_refunds; 320 struct TALER_Amount dval; 321 struct TALER_Amount dfee; 322 enum GNUNET_DB_QueryStatus qs; 323 324 (void) rowid; 325 (void) denom_pub; 326 (void) h_payto; 327 if (GNUNET_SYSERR == ctx->is_valid) 328 return; 329 GNUNET_assert (GNUNET_OK == 330 TALER_amount_set_zero (deposit_value->currency, 331 &total_refunds)); 332 qs = TEH_plugin->select_refunds_by_coin (TEH_plugin->cls, 333 coin_pub, 334 merchant_pub, 335 h_contract_terms, 336 &add_refunds, 337 &total_refunds); 338 if (qs < 0) 339 { 340 GNUNET_break (0); 341 ctx->is_valid = GNUNET_SYSERR; 342 return; 343 } 344 if (1 == 345 TALER_amount_cmp (&total_refunds, 346 deposit_value)) 347 { 348 /* Refunds exceeded total deposit? not OK! */ 349 GNUNET_break (0); 350 ctx->is_valid = GNUNET_SYSERR; 351 return; 352 } 353 if (0 == 354 TALER_amount_cmp (&total_refunds, 355 deposit_value)) 356 { 357 /* total_refunds == deposit_value; 358 in this case, the total contributed to the 359 wire transfer is zero (as are fees) */ 360 GNUNET_assert (GNUNET_OK == 361 TALER_amount_set_zero (deposit_value->currency, 362 &dval)); 363 GNUNET_assert (GNUNET_OK == 364 TALER_amount_set_zero (deposit_value->currency, 365 &dfee)); 366 367 } 368 else 369 { 370 /* Compute deposit value by subtracting refunds */ 371 GNUNET_assert (0 < 372 TALER_amount_subtract (&dval, 373 deposit_value, 374 &total_refunds)); 375 if (-1 == 376 TALER_amount_cmp (&dval, 377 deposit_fee)) 378 { 379 /* dval < deposit_fee, so after refunds less than 380 the deposit fee remains; reduce deposit fee to 381 the remaining value of the coin */ 382 dfee = dval; 383 } 384 else 385 { 386 /* Partial refund, deposit fee remains */ 387 dfee = *deposit_fee; 388 } 389 } 390 391 if (GNUNET_NO == ctx->is_valid) 392 { 393 /* First one we encounter, setup general information in 'ctx' */ 394 ctx->merchant_pub = *merchant_pub; 395 ctx->payto_uri.full_payto = GNUNET_strdup (account_payto_uri.full_payto); 396 ctx->exec_time = exec_time; 397 ctx->is_valid = GNUNET_YES; 398 if (0 > 399 TALER_amount_subtract (&ctx->total, 400 &dval, 401 &dfee)) 402 { 403 GNUNET_break (0); 404 ctx->is_valid = GNUNET_SYSERR; 405 return; 406 } 407 } 408 else 409 { 410 struct TALER_Amount delta; 411 412 /* Subsequent data, check general information matches that in 'ctx'; 413 (it should, otherwise the deposits should not have been aggregated) */ 414 if ( (0 != GNUNET_memcmp (&ctx->merchant_pub, 415 merchant_pub)) || 416 (0 != TALER_full_payto_cmp (account_payto_uri, 417 ctx->payto_uri)) ) 418 { 419 GNUNET_break (0); 420 ctx->is_valid = GNUNET_SYSERR; 421 return; 422 } 423 if (0 > 424 TALER_amount_subtract (&delta, 425 &dval, 426 &dfee)) 427 { 428 GNUNET_break (0); 429 ctx->is_valid = GNUNET_SYSERR; 430 return; 431 } 432 if (0 > 433 TALER_amount_add (&ctx->total, 434 &ctx->total, 435 &delta)) 436 { 437 GNUNET_break (0); 438 ctx->is_valid = GNUNET_SYSERR; 439 return; 440 } 441 } 442 443 { 444 struct AggregatedDepositDetail *wdd; 445 446 wdd = GNUNET_new (struct AggregatedDepositDetail); 447 wdd->deposit_value = dval; 448 wdd->deposit_fee = dfee; 449 wdd->refund_total = total_refunds; 450 wdd->h_contract_terms = *h_contract_terms; 451 wdd->coin_pub = *coin_pub; 452 GNUNET_CONTAINER_DLL_insert (ctx->wdd_head, 453 ctx->wdd_tail, 454 wdd); 455 } 456 } 457 458 459 /** 460 * Free data structure reachable from @a ctx, but not @a ctx itself. 461 * 462 * @param ctx context to free 463 */ 464 static void 465 free_ctx (struct WtidTransactionContext *ctx) 466 { 467 struct AggregatedDepositDetail *wdd; 468 469 while (NULL != (wdd = ctx->wdd_head)) 470 { 471 GNUNET_CONTAINER_DLL_remove (ctx->wdd_head, 472 ctx->wdd_tail, 473 wdd); 474 GNUNET_free (wdd); 475 } 476 GNUNET_free (ctx->payto_uri.full_payto); 477 } 478 479 480 /** 481 * Execute a "/transfers" GET operation. Returns the deposit details of the 482 * deposits that were aggregated to create the given wire transfer. 483 * 484 * If it returns a non-error code, the transaction logic MUST 485 * NOT queue a MHD response. IF it returns an hard error, the 486 * transaction logic MUST queue a MHD response and set @a mhd_ret. IF 487 * it returns the soft error code, the function MAY be called again to 488 * retry and MUST not queue a MHD response. 489 * 490 * @param cls closure 491 * @param connection MHD request which triggered the transaction 492 * @param[out] mhd_ret set to MHD response status for @a connection, 493 * if transaction failed (!) 494 * @return transaction status 495 */ 496 static enum GNUNET_DB_QueryStatus 497 get_transfer_deposits (void *cls, 498 struct MHD_Connection *connection, 499 MHD_RESULT *mhd_ret) 500 { 501 struct WtidTransactionContext *ctx = cls; 502 enum GNUNET_DB_QueryStatus qs; 503 struct GNUNET_TIME_Timestamp wire_fee_start_date; 504 struct GNUNET_TIME_Timestamp wire_fee_end_date; 505 struct TALER_MasterSignatureP wire_fee_master_sig; 506 507 /* resetting to NULL/0 in case transaction was repeated after 508 serialization failure */ 509 free_ctx (ctx); 510 qs = TEH_plugin->lookup_wire_transfer (TEH_plugin->cls, 511 &ctx->wtid, 512 &handle_deposit_data, 513 ctx); 514 if (0 > qs) 515 { 516 if (GNUNET_DB_STATUS_HARD_ERROR == qs) 517 { 518 GNUNET_break (0); 519 *mhd_ret = TALER_MHD_reply_with_error (connection, 520 MHD_HTTP_INTERNAL_SERVER_ERROR, 521 TALER_EC_GENERIC_DB_FETCH_FAILED, 522 "wire transfer"); 523 } 524 return qs; 525 } 526 if (GNUNET_SYSERR == ctx->is_valid) 527 { 528 GNUNET_break (0); 529 *mhd_ret = TALER_MHD_reply_with_error (connection, 530 MHD_HTTP_INTERNAL_SERVER_ERROR, 531 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 532 "wire history malformed"); 533 return GNUNET_DB_STATUS_HARD_ERROR; 534 } 535 if (GNUNET_NO == ctx->is_valid) 536 { 537 *mhd_ret = TALER_MHD_reply_with_error (connection, 538 MHD_HTTP_NOT_FOUND, 539 TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_NOT_FOUND, 540 NULL); 541 return GNUNET_DB_STATUS_HARD_ERROR; 542 } 543 { 544 char *wire_method; 545 uint64_t rowid; 546 547 wire_method = TALER_payto_get_method (ctx->payto_uri.full_payto); 548 if (NULL == wire_method) 549 { 550 GNUNET_break (0); 551 *mhd_ret = TALER_MHD_reply_with_error (connection, 552 MHD_HTTP_INTERNAL_SERVER_ERROR, 553 TALER_EC_GENERIC_DB_INVARIANT_FAILURE, 554 "payto:// without wire method encountered"); 555 return GNUNET_DB_STATUS_HARD_ERROR; 556 } 557 qs = TEH_plugin->get_wire_fee (TEH_plugin->cls, 558 wire_method, 559 ctx->exec_time, 560 &rowid, 561 &wire_fee_start_date, 562 &wire_fee_end_date, 563 &ctx->fees, 564 &wire_fee_master_sig); 565 GNUNET_free (wire_method); 566 } 567 if (0 >= qs) 568 { 569 if ( (GNUNET_DB_STATUS_HARD_ERROR == qs) || 570 (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) ) 571 { 572 GNUNET_break (0); 573 *mhd_ret = TALER_MHD_reply_with_error (connection, 574 MHD_HTTP_INTERNAL_SERVER_ERROR, 575 TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_NOT_FOUND, 576 NULL); 577 } 578 return qs; 579 } 580 if (0 > 581 TALER_amount_subtract (&ctx->total, 582 &ctx->total, 583 &ctx->fees.wire)) 584 { 585 GNUNET_break (0); 586 *mhd_ret = TALER_MHD_reply_with_error (connection, 587 MHD_HTTP_INTERNAL_SERVER_ERROR, 588 TALER_EC_EXCHANGE_TRANSFERS_GET_WIRE_FEE_INCONSISTENT, 589 NULL); 590 return GNUNET_DB_STATUS_HARD_ERROR; 591 } 592 return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT; 593 } 594 595 596 MHD_RESULT 597 TEH_handler_transfers_get (struct TEH_RequestContext *rc, 598 const char *const args[1]) 599 { 600 struct WtidTransactionContext ctx; 601 MHD_RESULT mhd_ret; 602 603 memset (&ctx, 604 0, 605 sizeof (ctx)); 606 if (GNUNET_OK != 607 GNUNET_STRINGS_string_to_data (args[0], 608 strlen (args[0]), 609 &ctx.wtid, 610 sizeof (ctx.wtid))) 611 { 612 GNUNET_break_op (0); 613 return TALER_MHD_reply_with_error (rc->connection, 614 MHD_HTTP_BAD_REQUEST, 615 TALER_EC_EXCHANGE_TRANSFERS_GET_WTID_MALFORMED, 616 args[0]); 617 } 618 if (GNUNET_OK != 619 TEH_DB_run_transaction (rc->connection, 620 "run transfers GET", 621 TEH_MT_REQUEST_OTHER, 622 &mhd_ret, 623 &get_transfer_deposits, 624 &ctx)) 625 { 626 free_ctx (&ctx); 627 return mhd_ret; 628 } 629 mhd_ret = reply_transfer_details (rc->connection, 630 &ctx.total, 631 &ctx.merchant_pub, 632 ctx.payto_uri, 633 &ctx.fees.wire, 634 ctx.exec_time, 635 ctx.wdd_head); 636 free_ctx (&ctx); 637 return mhd_ret; 638 } 639 640 641 /* end of taler-exchange-httpd_transfers_get.c */