exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

exchange_api_post-reserves-RESERVE_PUB-purse.c (20652B)


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