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 (20366B)


      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     GNUNET_free (prph->base_url);
    417     GNUNET_free (prph->exchange_url);
    418     GNUNET_free (prph);
    419     return NULL;
    420   }
    421   prph->merge_timestamp = merge_timestamp;
    422   GNUNET_CRYPTO_eddsa_key_get_public (&purse_priv->eddsa_priv,
    423                                       &prph->purse_pub.eddsa_pub);
    424   GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
    425                                       &prph->reserve_pub.eddsa_pub);
    426   GNUNET_CRYPTO_eddsa_key_get_public (&merge_priv->eddsa_priv,
    427                                       &prph->merge_pub.eddsa_pub);
    428 
    429   {
    430     struct GNUNET_JSON_Specification spec[] = {
    431       TALER_JSON_spec_amount_any ("amount",
    432                                   &prph->purse_value_after_fees),
    433       GNUNET_JSON_spec_mark_optional (
    434         GNUNET_JSON_spec_uint32 ("minimum_age",
    435                                  &prph->min_age),
    436         NULL),
    437       GNUNET_JSON_spec_timestamp ("pay_deadline",
    438                                   &prph->purse_expiration),
    439       GNUNET_JSON_spec_end ()
    440     };
    441 
    442     if (GNUNET_OK !=
    443         GNUNET_JSON_parse (contract_terms,
    444                            spec,
    445                            NULL, NULL))
    446     {
    447       GNUNET_break (0);
    448       TALER_EXCHANGE_keys_decref (prph->keys);
    449       GNUNET_free (prph->base_url);
    450       GNUNET_free (prph->exchange_url);
    451       GNUNET_free (prph);
    452       return NULL;
    453     }
    454   }
    455 
    456   TALER_wallet_purse_create_sign (prph->purse_expiration,
    457                                   &prph->h_contract_terms,
    458                                   &prph->merge_pub,
    459                                   prph->min_age,
    460                                   &prph->purse_value_after_fees,
    461                                   purse_priv,
    462                                   &prph->purse_sig);
    463   {
    464     struct TALER_NormalizedPayto payto_uri;
    465 
    466     payto_uri = TALER_reserve_make_payto (url,
    467                                           &prph->reserve_pub);
    468     TALER_wallet_purse_merge_sign (payto_uri,
    469                                    prph->merge_timestamp,
    470                                    &prph->purse_pub,
    471                                    merge_priv,
    472                                    &prph->merge_sig);
    473     GNUNET_free (payto_uri.normalized_payto);
    474   }
    475   return prph;
    476 }
    477 
    478 
    479 enum GNUNET_GenericReturnValue
    480 TALER_EXCHANGE_post_reserves_purse_set_options_ (
    481   struct TALER_EXCHANGE_PostReservesPurseHandle *prph,
    482   unsigned int num_options,
    483   const struct TALER_EXCHANGE_PostReservesPurseOptionValue options[])
    484 {
    485   for (unsigned int i = 0; i < num_options; i++)
    486   {
    487     const struct TALER_EXCHANGE_PostReservesPurseOptionValue *opt = &options[i];
    488 
    489     switch (opt->option)
    490     {
    491     case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_END:
    492       return GNUNET_OK;
    493     case TALER_EXCHANGE_POST_RESERVES_PURSE_OPTION_UPLOAD_CONTRACT:
    494       prph->options.upload_contract = true;
    495       break;
    496     }
    497   }
    498   return GNUNET_OK;
    499 }
    500 
    501 
    502 enum TALER_ErrorCode
    503 TALER_EXCHANGE_post_reserves_purse_start (
    504   struct TALER_EXCHANGE_PostReservesPurseHandle *prph,
    505   TALER_EXCHANGE_PostReservesPurseCallback cb,
    506   TALER_EXCHANGE_POST_RESERVES_PURSE_RESULT_CLOSURE *cb_cls)
    507 {
    508   char arg_str[sizeof (prph->reserve_pub) * 2 + 32];
    509   CURL *eh;
    510   json_t *body;
    511   struct TALER_Amount purse_fee;
    512   enum TALER_WalletAccountMergeFlags flags;
    513 
    514   prph->cb = cb;
    515   prph->cb_cls = cb_cls;
    516   if (prph->options.pay_for_purse)
    517   {
    518     const struct TALER_EXCHANGE_GlobalFee *gf;
    519 
    520     flags = TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE;
    521     gf = TALER_EXCHANGE_get_global_fee (
    522       prph->keys,
    523       GNUNET_TIME_timestamp_get ());
    524     purse_fee = gf->fees.purse;
    525   }
    526   else
    527   {
    528     flags = TALER_WAMF_MODE_CREATE_FROM_PURSE_QUOTA;
    529     GNUNET_assert (GNUNET_OK ==
    530                    TALER_amount_set_zero (prph->purse_value_after_fees.currency,
    531                                           &purse_fee));
    532   }
    533 
    534   TALER_wallet_account_merge_sign (prph->merge_timestamp,
    535                                    &prph->purse_pub,
    536                                    prph->purse_expiration,
    537                                    &prph->h_contract_terms,
    538                                    &prph->purse_value_after_fees,
    539                                    &purse_fee,
    540                                    prph->min_age,
    541                                    flags,
    542                                    &prph->reserve_priv,
    543                                    &prph->reserve_sig);
    544 
    545 
    546   if (prph->options.upload_contract)
    547   {
    548     TALER_CRYPTO_contract_encrypt_for_deposit (
    549       &prph->purse_pub,
    550       &prph->contract_priv,
    551       prph->contract_terms,
    552       &prph->econtract.econtract,
    553       &prph->econtract.econtract_size);
    554     GNUNET_CRYPTO_ecdhe_key_get_public (
    555       &prph->contract_priv.ecdhe_priv,
    556       &prph->econtract.contract_pub.ecdhe_pub);
    557     TALER_wallet_econtract_upload_sign (
    558       prph->econtract.econtract,
    559       prph->econtract.econtract_size,
    560       &prph->econtract.contract_pub,
    561       &prph->purse_priv,
    562       &prph->econtract.econtract_sig);
    563   }
    564 
    565   body = GNUNET_JSON_PACK (
    566     TALER_JSON_pack_amount ("purse_value",
    567                             &prph->purse_value_after_fees),
    568     GNUNET_JSON_pack_uint64 ("min_age",
    569                              prph->min_age),
    570     GNUNET_JSON_pack_allow_null (
    571       TALER_JSON_pack_econtract ("econtract",
    572                                  prph->options.upload_contract
    573                                  ? &prph->econtract
    574                                  : NULL)),
    575     GNUNET_JSON_pack_allow_null (
    576       prph->options.pay_for_purse
    577       ? TALER_JSON_pack_amount ("purse_fee",
    578                                 &purse_fee)
    579       : GNUNET_JSON_pack_string ("dummy2",
    580                                  NULL)),
    581     GNUNET_JSON_pack_data_auto ("merge_pub",
    582                                 &prph->merge_pub),
    583     GNUNET_JSON_pack_data_auto ("merge_sig",
    584                                 &prph->merge_sig),
    585     GNUNET_JSON_pack_data_auto ("reserve_sig",
    586                                 &prph->reserve_sig),
    587     GNUNET_JSON_pack_data_auto ("purse_pub",
    588                                 &prph->purse_pub),
    589     GNUNET_JSON_pack_data_auto ("purse_sig",
    590                                 &prph->purse_sig),
    591     GNUNET_JSON_pack_data_auto ("h_contract_terms",
    592                                 &prph->h_contract_terms),
    593     GNUNET_JSON_pack_timestamp ("merge_timestamp",
    594                                 prph->merge_timestamp),
    595     GNUNET_JSON_pack_timestamp ("purse_expiration",
    596                                 prph->purse_expiration));
    597   if (NULL == body)
    598     return TALER_EC_GENERIC_ALLOCATION_FAILURE;
    599 
    600   {
    601     char pub_str[sizeof (prph->reserve_pub) * 2];
    602     char *end;
    603 
    604     end = GNUNET_STRINGS_data_to_string (
    605       &prph->reserve_pub,
    606       sizeof (prph->reserve_pub),
    607       pub_str,
    608       sizeof (pub_str));
    609     *end = '\0';
    610     GNUNET_snprintf (arg_str,
    611                      sizeof (arg_str),
    612                      "reserves/%s/purse",
    613                      pub_str);
    614   }
    615   prph->url = TALER_url_join (prph->base_url,
    616                               arg_str,
    617                               NULL);
    618   if (NULL == prph->url)
    619   {
    620     GNUNET_break (0);
    621     return TALER_EC_GENERIC_CONFIGURATION_INVALID;
    622   }
    623   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    624               "URL for purse create_with_merge: `%s'\n",
    625               prph->url);
    626   eh = TALER_EXCHANGE_curl_easy_get_ (prph->url);
    627   if ( (NULL == eh) ||
    628        (GNUNET_OK !=
    629         TALER_curl_easy_post (&prph->ctx,
    630                               eh,
    631                               body)) )
    632   {
    633     GNUNET_break (0);
    634     if (NULL != eh)
    635       curl_easy_cleanup (eh);
    636     return TALER_EC_GENERIC_CURL_ALLOCATION_FAILURE;
    637   }
    638   json_decref (body);
    639   prph->job = GNUNET_CURL_job_add2 (prph->curl_ctx,
    640                                     eh,
    641                                     prph->ctx.headers,
    642                                     &handle_purse_create_with_merge_finished,
    643                                     prph);
    644   if (NULL == prph->job)
    645     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    646   return TALER_EC_NONE;
    647 }
    648 
    649 
    650 void
    651 TALER_EXCHANGE_post_reserves_purse_cancel (
    652   struct TALER_EXCHANGE_PostReservesPurseHandle *prph)
    653 {
    654   if (NULL != prph->job)
    655   {
    656     GNUNET_CURL_job_cancel (prph->job);
    657     prph->job = NULL;
    658   }
    659   GNUNET_free (prph->url);
    660   GNUNET_free (prph->base_url);
    661   GNUNET_free (prph->exchange_url);
    662   TALER_curl_easy_post_finished (&prph->ctx);
    663   TALER_EXCHANGE_keys_decref (prph->keys);
    664   GNUNET_free (prph->econtract.econtract);
    665   json_decref (prph->contract_terms);
    666   GNUNET_free (prph);
    667 }
    668 
    669 
    670 /* end of exchange_api_post-reserves-RESERVE_PUB-purse.c */