exchange_api_post-reserves-RESERVE_PUB-purse.c (20905B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2022-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 15 <http://www.gnu.org/licenses/> 16 */ 17 /** 18 * @file lib/exchange_api_post-reserves-RESERVE_PUB-purse.c 19 * @brief Implementation of the client to create a 20 * purse for an account 21 * @author Christian Grothoff 22 */ 23 #include <jansson.h> 24 #include <microhttpd.h> /* just for HTTP status codes */ 25 #include <gnunet/gnunet_util_lib.h> 26 #include <gnunet/gnunet_json_lib.h> 27 #include <gnunet/gnunet_curl_lib.h> 28 #include "taler/taler_json_lib.h" 29 #include "exchange_api_handle.h" 30 #include "exchange_api_common.h" 31 #include "taler/taler_signatures.h" 32 #include "exchange_api_curl_defaults.h" 33 34 35 /** 36 * @brief A POST /reserves/$RESERVE_PUB/purse handle 37 */ 38 struct TALER_EXCHANGE_PostReservesPurseHandle 39 { 40 41 /** 42 * The keys of the exchange this request handle will use 43 */ 44 struct TALER_EXCHANGE_Keys *keys; 45 46 /** 47 * Context for #TEH_curl_easy_post(). Keeps the data that must 48 * persist for Curl to make the upload. 49 */ 50 struct TALER_CURL_PostContext ctx; 51 52 /** 53 * The base URL for this request. 54 */ 55 char *base_url; 56 57 /** 58 * The full URL for this request, set during _start. 59 */ 60 char *url; 61 62 /** 63 * The exchange base URL (same as base_url, kept for conflict checks). 64 */ 65 char *exchange_url; 66 67 /** 68 * Reference to the execution context. 69 */ 70 struct GNUNET_CURL_Context *curl_ctx; 71 72 /** 73 * Handle for the request. 74 */ 75 struct GNUNET_CURL_Job *job; 76 77 /** 78 * Function to call with the result. 79 */ 80 TALER_EXCHANGE_PostReservesPurseCallback cb; 81 82 /** 83 * Closure for @a cb. 84 */ 85 TALER_EXCHANGE_POST_RESERVES_PURSE_RESULT_CLOSURE *cb_cls; 86 87 /** 88 * Private key for the contract. 89 */ 90 struct TALER_ContractDiffiePrivateP contract_priv; 91 92 /** 93 * Private key for the purse. 94 */ 95 struct TALER_PurseContractPrivateKeyP purse_priv; 96 97 /** 98 * Private key of the reserve. 99 */ 100 struct TALER_ReservePrivateKeyP reserve_priv; 101 102 /** 103 * The encrypted contract (if any). 104 */ 105 struct TALER_EncryptedContract econtract; 106 107 /** 108 * Expected value in the purse after fees. 109 */ 110 struct TALER_Amount purse_value_after_fees; 111 112 /** 113 * Public key of the reserve public key. 114 */ 115 struct TALER_ReservePublicKeyP reserve_pub; 116 117 /** 118 * Reserve signature affirming our merge. 119 */ 120 struct TALER_ReserveSignatureP reserve_sig; 121 122 /** 123 * Merge capability key. 124 */ 125 struct TALER_PurseMergePublicKeyP merge_pub; 126 127 /** 128 * Our merge signature (if any). 129 */ 130 struct TALER_PurseMergeSignatureP merge_sig; 131 132 /** 133 * Public key of the purse. 134 */ 135 struct TALER_PurseContractPublicKeyP purse_pub; 136 137 /** 138 * Request data we signed over. 139 */ 140 struct TALER_PurseContractSignatureP purse_sig; 141 142 /** 143 * Hash over the purse's contract terms. 144 */ 145 struct TALER_PrivateContractHashP h_contract_terms; 146 147 /** 148 * When does the purse expire. 149 */ 150 struct GNUNET_TIME_Timestamp purse_expiration; 151 152 /** 153 * When does the purse get merged/created. 154 */ 155 struct GNUNET_TIME_Timestamp merge_timestamp; 156 157 /** 158 * Our contract terms. 159 */ 160 json_t *contract_terms; 161 162 /** 163 * Minimum age for the coins as per @e contract_terms. 164 */ 165 uint32_t min_age; 166 167 struct 168 { 169 170 /** 171 * Are we paying for purse creation? Not yet a "real" option. 172 */ 173 bool pay_for_purse; 174 175 /** 176 * Should we upload the contract? 177 */ 178 bool upload_contract; 179 } options; 180 181 }; 182 183 184 /** 185 * Function called when we're done processing the 186 * HTTP /reserves/$RID/purse request. 187 * 188 * @param cls the `struct TALER_EXCHANGE_PostReservesPurseHandle` 189 * @param response_code HTTP response code, 0 on error 190 * @param response parsed JSON result, NULL on error 191 */ 192 static void 193 handle_purse_create_with_merge_finished (void *cls, 194 long response_code, 195 const void *response) 196 { 197 struct TALER_EXCHANGE_PostReservesPurseHandle *prph = cls; 198 const json_t *j = response; 199 struct TALER_EXCHANGE_PostReservesPurseResponse dr = { 200 .hr.reply = j, 201 .hr.http_status = (unsigned int) response_code, 202 .reserve_sig = &prph->reserve_sig 203 }; 204 205 prph->job = NULL; 206 switch (response_code) 207 { 208 case 0: 209 dr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; 210 break; 211 case MHD_HTTP_OK: 212 { 213 struct GNUNET_JSON_Specification spec[] = { 214 TALER_JSON_spec_amount_any ("total_deposited", 215 &dr.details.ok.total_deposited), 216 GNUNET_JSON_spec_fixed_auto ("exchange_sig", 217 &dr.details.ok.exchange_sig), 218 GNUNET_JSON_spec_fixed_auto ("exchange_pub", 219 &dr.details.ok.exchange_pub), 220 GNUNET_JSON_spec_timestamp ("exchange_timestamp", 221 &dr.details.ok.exchange_timestamp), 222 GNUNET_JSON_spec_end () 223 }; 224 225 if (GNUNET_OK != 226 GNUNET_JSON_parse (j, 227 spec, 228 NULL, NULL)) 229 { 230 GNUNET_break_op (0); 231 dr.hr.http_status = 0; 232 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 233 break; 234 } 235 if (GNUNET_OK != 236 TALER_EXCHANGE_test_signing_key (prph->keys, 237 &dr.details.ok.exchange_pub)) 238 { 239 GNUNET_break_op (0); 240 dr.hr.http_status = 0; 241 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 242 break; 243 } 244 if (GNUNET_OK != 245 TALER_exchange_online_purse_created_verify ( 246 dr.details.ok.exchange_timestamp, 247 prph->purse_expiration, 248 &prph->purse_value_after_fees, 249 &dr.details.ok.total_deposited, 250 &prph->purse_pub, 251 &prph->h_contract_terms, 252 &dr.details.ok.exchange_pub, 253 &dr.details.ok.exchange_sig)) 254 { 255 GNUNET_break_op (0); 256 dr.hr.http_status = 0; 257 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 258 break; 259 } 260 } 261 break; 262 case MHD_HTTP_BAD_REQUEST: 263 /* This should never happen, either us or the exchange is buggy 264 (or API version conflict); just pass JSON reply to the application */ 265 dr.hr.ec = TALER_JSON_get_error_code (j); 266 dr.hr.hint = TALER_JSON_get_error_hint (j); 267 break; 268 case MHD_HTTP_FORBIDDEN: 269 dr.hr.ec = TALER_JSON_get_error_code (j); 270 dr.hr.hint = TALER_JSON_get_error_hint (j); 271 /* Nothing really to verify, exchange says one of the signatures is 272 invalid; as we checked them, this should never happen, we 273 should pass the JSON reply to the application */ 274 break; 275 case MHD_HTTP_NOT_FOUND: 276 dr.hr.ec = TALER_JSON_get_error_code (j); 277 dr.hr.hint = TALER_JSON_get_error_hint (j); 278 /* Nothing really to verify, this should never 279 happen, we should pass the JSON reply to the application */ 280 break; 281 case MHD_HTTP_CONFLICT: 282 dr.hr.ec = TALER_JSON_get_error_code (j); 283 switch (dr.hr.ec) 284 { 285 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_CONFLICTING_META_DATA: 286 if (GNUNET_OK != 287 TALER_EXCHANGE_check_purse_create_conflict_ ( 288 &prph->purse_sig, 289 &prph->purse_pub, 290 j)) 291 { 292 dr.hr.http_status = 0; 293 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 294 break; 295 } 296 break; 297 case TALER_EC_EXCHANGE_RESERVES_PURSE_MERGE_CONFLICTING_META_DATA: 298 if (GNUNET_OK != 299 TALER_EXCHANGE_check_purse_merge_conflict_ ( 300 &prph->merge_sig, 301 &prph->merge_pub, 302 &prph->purse_pub, 303 prph->exchange_url, 304 j)) 305 { 306 GNUNET_break_op (0); 307 dr.hr.http_status = 0; 308 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 309 break; 310 } 311 break; 312 case TALER_EC_EXCHANGE_RESERVES_PURSE_CREATE_INSUFFICIENT_FUNDS: 313 /* nothing to verify */ 314 break; 315 case TALER_EC_EXCHANGE_PURSE_ECONTRACT_CONFLICTING_META_DATA: 316 if (GNUNET_OK != 317 TALER_EXCHANGE_check_purse_econtract_conflict_ ( 318 &prph->econtract.econtract_sig, 319 &prph->purse_pub, 320 j)) 321 { 322 GNUNET_break_op (0); 323 dr.hr.http_status = 0; 324 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 325 break; 326 } 327 break; 328 default: 329 /* unexpected EC! */ 330 GNUNET_break_op (0); 331 dr.hr.http_status = 0; 332 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 333 break; 334 } /* end inner (EC) switch */ 335 break; 336 case MHD_HTTP_GONE: 337 /* could happen if denomination was revoked */ 338 /* Note: one might want to check /keys for revocation 339 signature here, alas tricky in case our /keys 340 is outdated => left to clients */ 341 dr.hr.ec = TALER_JSON_get_error_code (j); 342 dr.hr.hint = TALER_JSON_get_error_hint (j); 343 break; 344 case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS: 345 dr.hr.ec = TALER_JSON_get_error_code (j); 346 dr.hr.hint = TALER_JSON_get_error_hint (j); 347 if (GNUNET_OK != 348 TALER_EXCHANGE_parse_451 (&dr.details.unavailable_for_legal_reasons, 349 j)) 350 { 351 GNUNET_break_op (0); 352 dr.hr.http_status = 0; 353 dr.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED; 354 break; 355 } 356 break; 357 case MHD_HTTP_INTERNAL_SERVER_ERROR: 358 dr.hr.ec = TALER_JSON_get_error_code (j); 359 dr.hr.hint = TALER_JSON_get_error_hint (j); 360 /* Server had an internal issue; we should retry, but this API 361 leaves this to the application */ 362 break; 363 default: 364 /* unexpected response code */ 365 dr.hr.ec = TALER_JSON_get_error_code (j); 366 dr.hr.hint = TALER_JSON_get_error_hint (j); 367 GNUNET_log (GNUNET_ERROR_TYPE_ERROR, 368 "Unexpected response code %u/%d for exchange deposit\n", 369 (unsigned int) response_code, 370 dr.hr.ec); 371 GNUNET_break_op (0); 372 break; 373 } 374 if (NULL != prph->cb) 375 { 376 prph->cb (prph->cb_cls, 377 &dr); 378 prph->cb = NULL; 379 } 380 TALER_EXCHANGE_post_reserves_purse_cancel (prph); 381 } 382 383 384 struct TALER_EXCHANGE_PostReservesPurseHandle * 385 TALER_EXCHANGE_post_reserves_purse_create ( 386 struct GNUNET_CURL_Context *ctx, 387 const char *url, 388 struct TALER_EXCHANGE_Keys *keys, 389 const struct TALER_ReservePrivateKeyP *reserve_priv, 390 const struct TALER_PurseContractPrivateKeyP *purse_priv, 391 const struct TALER_PurseMergePrivateKeyP *merge_priv, 392 const struct TALER_ContractDiffiePrivateP *contract_priv, 393 const json_t *contract_terms, 394 bool pay_for_purse, // FIXME: turn into option? 395 struct GNUNET_TIME_Timestamp merge_timestamp) 396 { 397 struct TALER_EXCHANGE_PostReservesPurseHandle *prph; 398 399 prph = GNUNET_new (struct TALER_EXCHANGE_PostReservesPurseHandle); 400 prph->curl_ctx = ctx; 401 prph->keys = TALER_EXCHANGE_keys_incref (keys); 402 prph->base_url = GNUNET_strdup (url); 403 prph->contract_terms = json_incref ((json_t *) contract_terms); 404 prph->exchange_url = GNUNET_strdup (url); 405 prph->contract_priv = *contract_priv; 406 prph->reserve_priv = *reserve_priv; 407 prph->purse_priv = *purse_priv; 408 prph->options.pay_for_purse = pay_for_purse; 409 410 if (GNUNET_OK != 411 TALER_JSON_contract_hash (contract_terms, 412 &prph->h_contract_terms)) 413 { 414 GNUNET_break (0); 415 TALER_EXCHANGE_keys_decref (prph->keys); 416 json_decref (prph->contract_terms); 417 GNUNET_free (prph->base_url); 418 GNUNET_free (prph->exchange_url); 419 GNUNET_free (prph); 420 return NULL; 421 } 422 prph->merge_timestamp = merge_timestamp; 423 GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv, 424 &prph->purse_pub.eddsa_pub); 425 GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv, 426 &prph->reserve_pub.eddsa_pub); 427 GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv, 428 &prph->merge_pub.eddsa_pub); 429 430 { 431 struct GNUNET_JSON_Specification spec[] = { 432 TALER_JSON_spec_amount_any ("amount", 433 &prph->purse_value_after_fees), 434 GNUNET_JSON_spec_mark_optional ( 435 GNUNET_JSON_spec_uint32 ("minimum_age", 436 &prph->min_age), 437 NULL), 438 GNUNET_JSON_spec_timestamp ("pay_deadline", 439 &prph->purse_expiration), 440 GNUNET_JSON_spec_end () 441 }; 442 443 if (GNUNET_OK != 444 GNUNET_JSON_parse (contract_terms, 445 spec, 446 NULL, NULL)) 447 { 448 GNUNET_break (0); 449 TALER_EXCHANGE_keys_decref (prph->keys); 450 json_decref (prph->contract_terms); 451 GNUNET_free (prph->base_url); 452 GNUNET_free (prph->exchange_url); 453 GNUNET_free (prph); 454 return NULL; 455 } 456 } 457 458 TALER_wallet_purse_create_sign (prph->purse_expiration, 459 &prph->h_contract_terms, 460 &prph->merge_pub, 461 prph->min_age, 462 &prph->purse_value_after_fees, 463 purse_priv, 464 &prph->purse_sig); 465 { 466 struct TALER_NormalizedPayto payto_uri; 467 468 payto_uri = TALER_reserve_make_payto (url, 469 &prph->reserve_pub); 470 if (NULL == payto_uri.normalized_payto) 471 { 472 GNUNET_break (0); 473 TALER_EXCHANGE_keys_decref (prph->keys); 474 json_decref (prph->contract_terms); 475 GNUNET_free (prph->base_url); 476 GNUNET_free (prph->exchange_url); 477 GNUNET_free (prph); 478 return NULL; 479 } 480 TALER_wallet_purse_merge_sign (payto_uri, 481 prph->merge_timestamp, 482 &prph->purse_pub, 483 merge_priv, 484 &prph->merge_sig); 485 GNUNET_free (payto_uri.normalized_payto); 486 } 487 return prph; 488 } 489 490 491 enum GNUNET_GenericReturnValue 492 TALER_EXCHANGE_post_reserves_purse_set_options_ ( 493 struct TALER_EXCHANGE_PostReservesPurseHandle *prph, 494 unsigned int num_options, 495 const struct TALER_EXCHANGE_PostReservesPurseOptionValue options[]) 496 { 497 for (unsigned int i = 0; i < num_options; i++) 498 { 499 const struct TALER_EXCHANGE_PostReservesPurseOptionValue *opt = &options[i]; 500 501 switch (opt->option) 502 { 503 case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_END: 504 return GNUNET_OK; 505 case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_UPLOAD_CONTRACT: 506 prph->options.upload_contract = true; 507 break; 508 } 509 } 510 return GNUNET_OK; 511 } 512 513 514 enum TALER_ErrorCode 515 TALER_EXCHANGE_post_reserves_purse_start ( 516 struct TALER_EXCHANGE_PostReservesPurseHandle *prph, 517 TALER_EXCHANGE_PostReservesPurseCallback cb, 518 TALER_EXCHANGE_POST_RESERVES_PURSE_RESULT_CLOSURE *cb_cls) 519 { 520 char arg_str[sizeof (prph->reserve_pub) * 2 + 32]; 521 CURL *eh; 522 json_t *body; 523 struct TALER_Amount purse_fee; 524 enum TALER_WalletAccountMergeFlags flags; 525 526 prph->cb = cb; 527 prph->cb_cls = cb_cls; 528 if (prph->options.pay_for_purse) 529 { 530 const struct TALER_EXCHANGE_GlobalFee *gf; 531 532 flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE; 533 gf = TALER_EXCHANGE_get_global_fee ( 534 prph->keys, 535 GNUNET_TIME_timestamp_get ()); 536 if (NULL == gf) 537 { 538 GNUNET_break_op (0); 539 return TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING; 540 } 541 purse_fee = gf->fees.purse; 542 } 543 else 544 { 545 flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA; 546 GNUNET_assert (GNUNET_OK == 547 TALER_amount_set_zero (prph->purse_value_after_fees.currency, 548 &purse_fee)); 549 } 550 551 TALER_wallet_account_merge_sign (prph->merge_timestamp, 552 &prph->purse_pub, 553 prph->purse_expiration, 554 &prph->h_contract_terms, 555 &prph->purse_value_after_fees, 556 &purse_fee, 557 prph->min_age, 558 flags, 559 &prph->reserve_priv, 560 &prph->reserve_sig); 561 562 563 if (prph->options.upload_contract) 564 { 565 TALER_CRYPTO_contract_encrypt_for_deposit ( 566 &prph->purse_pub, 567 &prph->contract_priv, 568 prph->contract_terms, 569 &prph->econtract.econtract, 570 &prph->econtract.econtract_size); 571 GNUNET_CRYPTO_ecdhe_key_get_public ( 572 &prph->contract_priv.ecdhe_priv, 573 &prph->econtract.contract_pub.ecdhe_pub); 574 TALER_wallet_econtract_upload_sign ( 575 prph->econtract.econtract, 576 prph->econtract.econtract_size, 577 &prph->econtract.contract_pub, 578 &prph->purse_priv, 579 &prph->econtract.econtract_sig); 580 } 581 582 body = GNUNET_JSON_PACK ( 583 TALER_JSON_pack_amount ("purse_value", 584 &prph->purse_value_after_fees), 585 GNUNET_JSON_pack_uint64 ("min_age", 586 prph->min_age), 587 GNUNET_JSON_pack_allow_null ( 588 TALER_JSON_pack_econtract ("econtract", 589 prph->options.upload_contract 590 ? &prph->econtract 591 : NULL)), 592 GNUNET_JSON_pack_allow_null ( 593 prph->options.pay_for_purse 594 ? TALER_JSON_pack_amount ("purse_fee", 595 &purse_fee) 596 : GNUNET_JSON_pack_string ("dummy2", 597 NULL)), 598 GNUNET_JSON_pack_data_auto ("merge_pub", 599 &prph->merge_pub), 600 GNUNET_JSON_pack_data_auto ("merge_sig", 601 &prph->merge_sig), 602 GNUNET_JSON_pack_data_auto ("reserve_sig", 603 &prph->reserve_sig), 604 GNUNET_JSON_pack_data_auto ("purse_pub", 605 &prph->purse_pub), 606 GNUNET_JSON_pack_data_auto ("purse_sig", 607 &prph->purse_sig), 608 GNUNET_JSON_pack_data_auto ("h_contract_terms", 609 &prph->h_contract_terms), 610 GNUNET_JSON_pack_timestamp ("merge_timestamp", 611 prph->merge_timestamp), 612 GNUNET_JSON_pack_timestamp ("purse_expiration", 613 prph->purse_expiration)); 614 if (NULL == body) 615 return TALER_EC_GENERIC_ALLOCATION_FAILURE; 616 617 { 618 char pub_str[sizeof (prph->reserve_pub) * 2]; 619 char *end; 620 621 end = GNUNET_STRINGS_data_to_string ( 622 &prph->reserve_pub, 623 sizeof (prph->reserve_pub), 624 pub_str, 625 sizeof (pub_str)); 626 *end = '\0'; 627 GNUNET_snprintf (arg_str, 628 sizeof (arg_str), 629 "reserves/%s/purse", 630 pub_str); 631 } 632 prph->url = TALER_url_join (prph->base_url, 633 arg_str, 634 NULL); 635 if (NULL == prph->url) 636 { 637 GNUNET_break (0); 638 json_decref (body); 639 return TALER_EC_GENERIC_CONFIGURATION_INVALID; 640 } 641 GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, 642 "URL for purse create_with_merge: `%s'\n", 643 prph->url); 644 eh = TALER_EXCHANGE_curl_easy_get_ (prph->url); 645 if ( (NULL == eh) || 646 (GNUNET_OK != 647 TALER_curl_easy_post (&prph->ctx, 648 eh, 649 body)) ) 650 { 651 GNUNET_break (0); 652 json_decref (body); 653 if (NULL != eh) 654 curl_easy_cleanup (eh); 655 return TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE; 656 } 657 json_decref (body); 658 prph->job = GNUNET_CURL_job_add2 (prph->curl_ctx, 659 eh, 660 prph->ctx.headers, 661 &handle_purse_create_with_merge_finished, 662 prph); 663 if (NULL == prph->job) 664 return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE; 665 return TALER_EC_NONE; 666 } 667 668 669 void 670 TALER_EXCHANGE_post_reserves_purse_cancel ( 671 struct TALER_EXCHANGE_PostReservesPurseHandle *prph) 672 { 673 if (NULL != prph->job) 674 { 675 GNUNET_CURL_job_cancel (prph->job); 676 prph->job = NULL; 677 } 678 GNUNET_free (prph->url); 679 GNUNET_free (prph->base_url); 680 GNUNET_free (prph->exchange_url); 681 TALER_curl_easy_post_finished (&prph->ctx); 682 TALER_EXCHANGE_keys_decref (prph->keys); 683 GNUNET_free (prph->econtract.econtract); 684 json_decref (prph->contract_terms); 685 GNUNET_free (prph); 686 } 687 688 689 /* end of exchange_api_post-reserves-RESERVE_PUB-purse.c */