exchange

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

exchange_api_get-reserves-RESERVE_PUB-history.c (38885B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2014-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_get-reserves-RESERVE_PUB-history.c
     19  * @brief Implementation of the GET /reserves/$RESERVE_PUB/history requests
     20  * @author Christian Grothoff
     21  */
     22 #include <jansson.h>
     23 #include <microhttpd.h> /* just for HTTP history codes */
     24 #include <gnunet/gnunet_util_lib.h>
     25 #include <gnunet/gnunet_json_lib.h>
     26 #include <gnunet/gnunet_curl_lib.h>
     27 #include "taler/taler_json_lib.h"
     28 #include "exchange_api_handle.h"
     29 #include "taler/taler_signatures.h"
     30 #include "exchange_api_curl_defaults.h"
     31 
     32 
     33 /**
     34  * @brief A GET /reserves/$RESERVE_PUB/history Handle
     35  */
     36 struct TALER_EXCHANGE_GetReservesHistoryHandle
     37 {
     38 
     39   /**
     40    * Base URL of the exchange.
     41    */
     42   char *base_url;
     43 
     44   /**
     45    * The url for this request.
     46    */
     47   char *url;
     48 
     49   /**
     50    * The keys of the exchange this request handle will use.
     51    */
     52   struct TALER_EXCHANGE_Keys *keys;
     53 
     54   /**
     55    * Handle for the request.
     56    */
     57   struct GNUNET_CURL_Job *job;
     58 
     59   /**
     60    * Function to call with the result.
     61    */
     62   TALER_EXCHANGE_GetReservesHistoryCallback cb;
     63 
     64   /**
     65    * Closure for @e cb.
     66    */
     67   TALER_EXCHANGE_GET_RESERVES_HISTORY_RESULT_CLOSURE *cb_cls;
     68 
     69   /**
     70    * CURL context to use.
     71    */
     72   struct GNUNET_CURL_Context *ctx;
     73 
     74   /**
     75    * Private key of the reserve we are querying.
     76    */
     77   struct TALER_ReservePrivateKeyP reserve_priv;
     78 
     79   /**
     80    * Public key of the reserve we are querying.
     81    */
     82   struct TALER_ReservePublicKeyP reserve_pub;
     83 
     84   /**
     85    * Where to store the etag (if any).
     86    */
     87   uint64_t etag;
     88 
     89   /**
     90    * Options for the request.
     91    */
     92   struct
     93   {
     94     /**
     95      * Only return entries with offset strictly greater than this value.
     96      */
     97     uint64_t start_off;
     98   } options;
     99 
    100 };
    101 
    102 
    103 /**
    104  * Context for history entry helpers.
    105  */
    106 struct HistoryParseContext
    107 {
    108 
    109   /**
    110    * Keys of the exchange we use.
    111    */
    112   const struct TALER_EXCHANGE_Keys *keys;
    113 
    114   /**
    115    * Our reserve public key.
    116    */
    117   const struct TALER_ReservePublicKeyP *reserve_pub;
    118 
    119   /**
    120    * Array of UUIDs.
    121    */
    122   struct GNUNET_HashCode *uuids;
    123 
    124   /**
    125    * Where to sum up total inbound amounts.
    126    */
    127   struct TALER_Amount *total_in;
    128 
    129   /**
    130    * Where to sum up total outbound amounts.
    131    */
    132   struct TALER_Amount *total_out;
    133 
    134   /**
    135    * Number of entries already used in @e uuids.
    136    */
    137   unsigned int uuid_off;
    138 };
    139 
    140 
    141 /**
    142  * Type of a function called to parse a reserve history
    143  * entry @a rh.
    144  *
    145  * @param[in,out] rh where to write the result
    146  * @param[in,out] uc UUID context for duplicate detection
    147  * @param transaction the transaction to parse
    148  * @return #GNUNET_OK on success
    149  */
    150 typedef enum GNUNET_GenericReturnValue
    151 (*ParseHelper)(struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    152                struct HistoryParseContext *uc,
    153                const json_t *transaction);
    154 
    155 
    156 /**
    157  * Parse "credit" reserve history entry.
    158  *
    159  * @param[in,out] rh entry to parse
    160  * @param uc our context
    161  * @param transaction the transaction to parse
    162  * @return #GNUNET_OK on success
    163  */
    164 static enum GNUNET_GenericReturnValue
    165 parse_credit (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    166               struct HistoryParseContext *uc,
    167               const json_t *transaction)
    168 {
    169   struct TALER_FullPayto wire_uri;
    170   uint64_t wire_reference;
    171   struct GNUNET_TIME_Timestamp timestamp;
    172   struct GNUNET_JSON_Specification withdraw_spec[] = {
    173     GNUNET_JSON_spec_uint64 ("wire_reference",
    174                              &wire_reference),
    175     GNUNET_JSON_spec_timestamp ("timestamp",
    176                                 &timestamp),
    177     TALER_JSON_spec_full_payto_uri ("sender_account_url",
    178                                     &wire_uri),
    179     GNUNET_JSON_spec_end ()
    180   };
    181 
    182   rh->type = TALER_EXCHANGE_RTT_CREDIT;
    183   if (0 >
    184       TALER_amount_add (uc->total_in,
    185                         uc->total_in,
    186                         &rh->amount))
    187   {
    188     /* overflow in history already!? inconceivable! Bad exchange! */
    189     GNUNET_break_op (0);
    190     return GNUNET_SYSERR;
    191   }
    192   if (GNUNET_OK !=
    193       GNUNET_JSON_parse (transaction,
    194                          withdraw_spec,
    195                          NULL, NULL))
    196   {
    197     GNUNET_break_op (0);
    198     return GNUNET_SYSERR;
    199   }
    200   rh->details.in_details.sender_url.full_payto
    201     = GNUNET_strdup (wire_uri.full_payto);
    202   rh->details.in_details.wire_reference = wire_reference;
    203   rh->details.in_details.timestamp = timestamp;
    204   return GNUNET_OK;
    205 }
    206 
    207 
    208 /**
    209  * Parse "withdraw" reserve history entry.
    210  *
    211  * @param[in,out] rh entry to parse
    212  * @param uc our context
    213  * @param transaction the transaction to parse
    214  * @return #GNUNET_OK on success
    215  */
    216 static enum GNUNET_GenericReturnValue
    217 parse_withdraw (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    218                 struct HistoryParseContext *uc,
    219                 const json_t *transaction)
    220 {
    221   uint16_t num_coins;
    222   struct TALER_Amount withdraw_fee;
    223   struct TALER_Amount withdraw_amount;
    224   struct TALER_Amount amount_without_fee;
    225   uint8_t max_age = 0;
    226   uint8_t noreveal_index = 0;
    227   struct TALER_HashBlindedPlanchetsP planchets_h;
    228   struct TALER_HashBlindedPlanchetsP selected_h;
    229   struct TALER_ReserveSignatureP reserve_sig;
    230   struct TALER_BlindingMasterSeedP blinding_seed;
    231   struct TALER_DenominationHashP *denom_pub_hashes;
    232   size_t num_denom_pub_hashes;
    233   bool no_max_age;
    234   bool no_noreveal_index;
    235   bool no_blinding_seed;
    236   bool no_selected_h;
    237   struct GNUNET_JSON_Specification withdraw_spec[] = {
    238     GNUNET_JSON_spec_fixed_auto ("reserve_sig",
    239                                  &reserve_sig),
    240     GNUNET_JSON_spec_uint16 ("num_coins",
    241                              &num_coins),
    242     GNUNET_JSON_spec_fixed_auto ("planchets_h",
    243                                  &planchets_h),
    244     TALER_JSON_spec_amount_any ("amount",
    245                                 &withdraw_amount),
    246     TALER_JSON_spec_amount_any ("withdraw_fee",
    247                                 &withdraw_fee),
    248     TALER_JSON_spec_array_of_denom_pub_h ("denom_pub_hashes",
    249                                           &num_denom_pub_hashes,
    250                                           &denom_pub_hashes),
    251     GNUNET_JSON_spec_mark_optional (
    252       GNUNET_JSON_spec_fixed_auto ("selected_h",
    253                                    &selected_h),
    254       &no_selected_h),
    255     GNUNET_JSON_spec_mark_optional (
    256       GNUNET_JSON_spec_uint8 ("max_age",
    257                               &max_age),
    258       &no_max_age),
    259     GNUNET_JSON_spec_mark_optional (
    260       GNUNET_JSON_spec_fixed_auto ("blinding_seed",
    261                                    &blinding_seed),
    262       &no_blinding_seed),
    263     GNUNET_JSON_spec_mark_optional (
    264       GNUNET_JSON_spec_uint8 ("noreveal_index",
    265                               &noreveal_index),
    266       &no_noreveal_index),
    267     GNUNET_JSON_spec_end ()
    268   };
    269 
    270   rh->type = TALER_EXCHANGE_RTT_WITHDRAWAL;
    271   if (GNUNET_OK !=
    272       GNUNET_JSON_parse (transaction,
    273                          withdraw_spec,
    274                          NULL, NULL))
    275   {
    276     GNUNET_break_op (0);
    277     return GNUNET_SYSERR;
    278   }
    279 
    280   if ((no_max_age != no_noreveal_index) ||
    281       (no_max_age != no_selected_h))
    282   {
    283     GNUNET_break_op (0);
    284     GNUNET_JSON_parse_free (withdraw_spec);
    285     return GNUNET_SYSERR;
    286   }
    287   rh->details.withdraw.age_restricted = ! no_max_age;
    288 
    289   if (num_coins != num_denom_pub_hashes)
    290   {
    291     GNUNET_break_op (0);
    292     GNUNET_JSON_parse_free (withdraw_spec);
    293     return GNUNET_SYSERR;
    294   }
    295 
    296   /* Check that the signature is a valid withdraw request */
    297   if (0>TALER_amount_subtract (
    298         &amount_without_fee,
    299         &withdraw_amount,
    300         &withdraw_fee))
    301   {
    302     GNUNET_break_op (0);
    303     GNUNET_JSON_parse_free (withdraw_spec);
    304     return GNUNET_SYSERR;
    305   }
    306 
    307   if (GNUNET_OK !=
    308       TALER_wallet_withdraw_verify (
    309         &amount_without_fee,
    310         &withdraw_fee,
    311         &planchets_h,
    312         no_blinding_seed ? NULL : &blinding_seed,
    313         no_max_age ? NULL : &uc->keys->age_mask,
    314         no_max_age ? 0 : max_age,
    315         uc->reserve_pub,
    316         &reserve_sig))
    317   {
    318     GNUNET_break_op (0);
    319     GNUNET_JSON_parse_free (withdraw_spec);
    320     return GNUNET_SYSERR;
    321   }
    322 
    323   rh->details.withdraw.num_coins = num_coins;
    324   rh->details.withdraw.age_restricted = ! no_max_age;
    325   rh->details.withdraw.max_age = max_age;
    326   rh->details.withdraw.planchets_h = planchets_h;
    327   rh->details.withdraw.selected_h = selected_h;
    328   rh->details.withdraw.noreveal_index = noreveal_index;
    329   rh->details.withdraw.no_blinding_seed = no_blinding_seed;
    330   if (! no_blinding_seed)
    331     rh->details.withdraw.blinding_seed = blinding_seed;
    332 
    333   /* check that withdraw fee matches expectations! */
    334   {
    335     const struct TALER_EXCHANGE_Keys *key_state;
    336     struct TALER_Amount fee_acc;
    337     struct TALER_Amount amount_acc;
    338 
    339     GNUNET_assert (GNUNET_OK ==
    340                    TALER_amount_set_zero (withdraw_amount.currency,
    341                                           &fee_acc));
    342     GNUNET_assert (GNUNET_OK ==
    343                    TALER_amount_set_zero (withdraw_amount.currency,
    344                                           &amount_acc));
    345 
    346     key_state = uc->keys;
    347 
    348     /* accumulate the withdraw fees */
    349     for (size_t i=0; i < num_coins; i++)
    350     {
    351       const struct TALER_EXCHANGE_DenomPublicKey *dki;
    352 
    353       dki = TALER_EXCHANGE_get_denomination_key_by_hash (key_state,
    354                                                          &denom_pub_hashes[i]);
    355       if (NULL == dki)
    356       {
    357         GNUNET_break_op (0);
    358         GNUNET_JSON_parse_free (withdraw_spec);
    359         return GNUNET_SYSERR;
    360       }
    361       GNUNET_assert (0 <=
    362                      TALER_amount_add (&fee_acc,
    363                                        &fee_acc,
    364                                        &dki->fees.withdraw));
    365       GNUNET_assert (0 <=
    366                      TALER_amount_add (&amount_acc,
    367                                        &amount_acc,
    368                                        &dki->value));
    369     }
    370 
    371     if ( (GNUNET_YES !=
    372           TALER_amount_cmp_currency (&fee_acc,
    373                                      &withdraw_fee)) ||
    374          (0 !=
    375           TALER_amount_cmp (&amount_acc,
    376                             &amount_without_fee)) )
    377     {
    378       GNUNET_break_op (0);
    379       GNUNET_JSON_parse_free (withdraw_spec);
    380       return GNUNET_SYSERR;
    381     }
    382     rh->details.withdraw.fee = withdraw_fee;
    383   }
    384 
    385   #pragma message "is out_authorization_sig still needed? Not set anywhere"
    386   rh->details.withdraw.out_authorization_sig
    387     = json_object_get (transaction,
    388                        "signature");
    389   /* Check check that the same withdraw transaction
    390        isn't listed twice by the exchange. We use the
    391        "uuid" array to remember the hashes of all
    392        signatures, and compare the hashes to find
    393        duplicates. */
    394   GNUNET_CRYPTO_hash (&reserve_sig,
    395                       sizeof (reserve_sig),
    396                       &uc->uuids[uc->uuid_off]);
    397   for (unsigned int i = 0; i<uc->uuid_off; i++)
    398   {
    399     if (0 == GNUNET_memcmp (&uc->uuids[uc->uuid_off],
    400                             &uc->uuids[i]))
    401     {
    402       GNUNET_break_op (0);
    403       GNUNET_JSON_parse_free (withdraw_spec);
    404       return GNUNET_SYSERR;
    405     }
    406   }
    407   uc->uuid_off++;
    408 
    409   if (0 >
    410       TALER_amount_add (uc->total_out,
    411                         uc->total_out,
    412                         &rh->amount))
    413   {
    414     /* overflow in history already!? inconceivable! Bad exchange! */
    415     GNUNET_break_op (0);
    416     GNUNET_JSON_parse_free (withdraw_spec);
    417     return GNUNET_SYSERR;
    418   }
    419   GNUNET_JSON_parse_free (withdraw_spec);
    420   return GNUNET_OK;
    421 }
    422 
    423 
    424 /**
    425  * Parse "recoup" reserve history entry.
    426  *
    427  * @param[in,out] rh entry to parse
    428  * @param uc our context
    429  * @param transaction the transaction to parse
    430  * @return #GNUNET_OK on success
    431  */
    432 static enum GNUNET_GenericReturnValue
    433 parse_recoup (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    434               struct HistoryParseContext *uc,
    435               const json_t *transaction)
    436 {
    437   const struct TALER_EXCHANGE_Keys *key_state;
    438   struct GNUNET_JSON_Specification recoup_spec[] = {
    439     GNUNET_JSON_spec_fixed_auto ("coin_pub",
    440                                  &rh->details.recoup_details.coin_pub),
    441     GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    442                                  &rh->details.recoup_details.exchange_sig),
    443     GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    444                                  &rh->details.recoup_details.exchange_pub),
    445     GNUNET_JSON_spec_timestamp ("timestamp",
    446                                 &rh->details.recoup_details.timestamp),
    447     GNUNET_JSON_spec_end ()
    448   };
    449 
    450   rh->type = TALER_EXCHANGE_RTT_RECOUP;
    451   if (GNUNET_OK !=
    452       GNUNET_JSON_parse (transaction,
    453                          recoup_spec,
    454                          NULL, NULL))
    455   {
    456     GNUNET_break_op (0);
    457     return GNUNET_SYSERR;
    458   }
    459   key_state = uc->keys;
    460   if (GNUNET_OK !=
    461       TALER_EXCHANGE_test_signing_key (key_state,
    462                                        &rh->details.
    463                                        recoup_details.exchange_pub))
    464   {
    465     GNUNET_break_op (0);
    466     return GNUNET_SYSERR;
    467   }
    468   if (GNUNET_OK !=
    469       TALER_exchange_online_confirm_recoup_verify (
    470         rh->details.recoup_details.timestamp,
    471         &rh->amount,
    472         &rh->details.recoup_details.coin_pub,
    473         uc->reserve_pub,
    474         &rh->details.recoup_details.exchange_pub,
    475         &rh->details.recoup_details.exchange_sig))
    476   {
    477     GNUNET_break_op (0);
    478     return GNUNET_SYSERR;
    479   }
    480   if (0 >
    481       TALER_amount_add (uc->total_in,
    482                         uc->total_in,
    483                         &rh->amount))
    484   {
    485     /* overflow in history already!? inconceivable! Bad exchange! */
    486     GNUNET_break_op (0);
    487     return GNUNET_SYSERR;
    488   }
    489   return GNUNET_OK;
    490 }
    491 
    492 
    493 /**
    494  * Parse "closing" reserve history entry.
    495  *
    496  * @param[in,out] rh entry to parse
    497  * @param uc our context
    498  * @param transaction the transaction to parse
    499  * @return #GNUNET_OK on success
    500  */
    501 static enum GNUNET_GenericReturnValue
    502 parse_closing (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    503                struct HistoryParseContext *uc,
    504                const json_t *transaction)
    505 {
    506   const struct TALER_EXCHANGE_Keys *key_state;
    507   struct TALER_FullPayto receiver_uri;
    508   struct GNUNET_JSON_Specification closing_spec[] = {
    509     TALER_JSON_spec_full_payto_uri (
    510       "receiver_account_details",
    511       &receiver_uri),
    512     GNUNET_JSON_spec_fixed_auto ("wtid",
    513                                  &rh->details.close_details.wtid),
    514     GNUNET_JSON_spec_fixed_auto ("exchange_sig",
    515                                  &rh->details.close_details.exchange_sig),
    516     GNUNET_JSON_spec_fixed_auto ("exchange_pub",
    517                                  &rh->details.close_details.exchange_pub),
    518     TALER_JSON_spec_amount_any ("closing_fee",
    519                                 &rh->details.close_details.fee),
    520     GNUNET_JSON_spec_timestamp ("timestamp",
    521                                 &rh->details.close_details.timestamp),
    522     GNUNET_JSON_spec_end ()
    523   };
    524 
    525   rh->type = TALER_EXCHANGE_RTT_CLOSING;
    526   if (GNUNET_OK !=
    527       GNUNET_JSON_parse (transaction,
    528                          closing_spec,
    529                          NULL, NULL))
    530   {
    531     GNUNET_break_op (0);
    532     return GNUNET_SYSERR;
    533   }
    534   key_state = uc->keys;
    535   if (GNUNET_OK !=
    536       TALER_EXCHANGE_test_signing_key (
    537         key_state,
    538         &rh->details.close_details.exchange_pub))
    539   {
    540     GNUNET_break_op (0);
    541     return GNUNET_SYSERR;
    542   }
    543   if (GNUNET_OK !=
    544       TALER_exchange_online_reserve_closed_verify (
    545         rh->details.close_details.timestamp,
    546         &rh->amount,
    547         &rh->details.close_details.fee,
    548         receiver_uri,
    549         &rh->details.close_details.wtid,
    550         uc->reserve_pub,
    551         &rh->details.close_details.exchange_pub,
    552         &rh->details.close_details.exchange_sig))
    553   {
    554     GNUNET_break_op (0);
    555     return GNUNET_SYSERR;
    556   }
    557   if (0 >
    558       TALER_amount_add (uc->total_out,
    559                         uc->total_out,
    560                         &rh->amount))
    561   {
    562     /* overflow in history already!? inconceivable! Bad exchange! */
    563     GNUNET_break_op (0);
    564     return GNUNET_SYSERR;
    565   }
    566   rh->details.close_details.receiver_account_details.full_payto
    567     = GNUNET_strdup (receiver_uri.full_payto);
    568   return GNUNET_OK;
    569 }
    570 
    571 
    572 /**
    573  * Parse "merge" reserve history entry.
    574  *
    575  * @param[in,out] rh entry to parse
    576  * @param uc our context
    577  * @param transaction the transaction to parse
    578  * @return #GNUNET_OK on success
    579  */
    580 static enum GNUNET_GenericReturnValue
    581 parse_merge (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    582              struct HistoryParseContext *uc,
    583              const json_t *transaction)
    584 {
    585   uint32_t flags32;
    586   struct GNUNET_JSON_Specification merge_spec[] = {
    587     GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
    588                                  &rh->details.merge_details.h_contract_terms),
    589     GNUNET_JSON_spec_fixed_auto ("merge_pub",
    590                                  &rh->details.merge_details.merge_pub),
    591     GNUNET_JSON_spec_fixed_auto ("purse_pub",
    592                                  &rh->details.merge_details.purse_pub),
    593     GNUNET_JSON_spec_uint32 ("min_age",
    594                              &rh->details.merge_details.min_age),
    595     GNUNET_JSON_spec_uint32 ("flags",
    596                              &flags32),
    597     GNUNET_JSON_spec_fixed_auto ("reserve_sig",
    598                                  &rh->details.merge_details.reserve_sig),
    599     TALER_JSON_spec_amount_any ("purse_fee",
    600                                 &rh->details.merge_details.purse_fee),
    601     GNUNET_JSON_spec_timestamp ("merge_timestamp",
    602                                 &rh->details.merge_details.merge_timestamp),
    603     GNUNET_JSON_spec_timestamp ("purse_expiration",
    604                                 &rh->details.merge_details.purse_expiration),
    605     GNUNET_JSON_spec_bool ("merged",
    606                            &rh->details.merge_details.merged),
    607     GNUNET_JSON_spec_end ()
    608   };
    609 
    610   rh->type = TALER_EXCHANGE_RTT_MERGE;
    611   if (GNUNET_OK !=
    612       GNUNET_JSON_parse (transaction,
    613                          merge_spec,
    614                          NULL, NULL))
    615   {
    616     GNUNET_break_op (0);
    617     return GNUNET_SYSERR;
    618   }
    619   rh->details.merge_details.flags =
    620     (enum TALER_WalletAccountMergeFlags) flags32;
    621   if (GNUNET_OK !=
    622       TALER_wallet_account_merge_verify (
    623         rh->details.merge_details.merge_timestamp,
    624         &rh->details.merge_details.purse_pub,
    625         rh->details.merge_details.purse_expiration,
    626         &rh->details.merge_details.h_contract_terms,
    627         &rh->amount,
    628         &rh->details.merge_details.purse_fee,
    629         rh->details.merge_details.min_age,
    630         rh->details.merge_details.flags,
    631         uc->reserve_pub,
    632         &rh->details.merge_details.reserve_sig))
    633   {
    634     GNUNET_break_op (0);
    635     return GNUNET_SYSERR;
    636   }
    637   if (rh->details.merge_details.merged)
    638   {
    639     if (0 >
    640         TALER_amount_add (uc->total_in,
    641                           uc->total_in,
    642                           &rh->amount))
    643     {
    644       /* overflow in history already!? inconceivable! Bad exchange! */
    645       GNUNET_break_op (0);
    646       return GNUNET_SYSERR;
    647     }
    648   }
    649   else
    650   {
    651     if (0 >
    652         TALER_amount_add (uc->total_out,
    653                           uc->total_out,
    654                           &rh->details.merge_details.purse_fee))
    655     {
    656       /* overflow in history already!? inconceivable! Bad exchange! */
    657       GNUNET_break_op (0);
    658       return GNUNET_SYSERR;
    659     }
    660   }
    661   return GNUNET_OK;
    662 }
    663 
    664 
    665 /**
    666  * Parse "open" reserve open entry.
    667  *
    668  * @param[in,out] rh entry to parse
    669  * @param uc our context
    670  * @param transaction the transaction to parse
    671  * @return #GNUNET_OK on success
    672  */
    673 static enum GNUNET_GenericReturnValue
    674 parse_open (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    675             struct HistoryParseContext *uc,
    676             const json_t *transaction)
    677 {
    678   struct GNUNET_JSON_Specification open_spec[] = {
    679     GNUNET_JSON_spec_fixed_auto ("reserve_sig",
    680                                  &rh->details.open_request.reserve_sig),
    681     TALER_JSON_spec_amount_any ("open_fee",
    682                                 &rh->details.open_request.reserve_payment),
    683     GNUNET_JSON_spec_uint32 ("requested_min_purses",
    684                              &rh->details.open_request.purse_limit),
    685     GNUNET_JSON_spec_timestamp ("request_timestamp",
    686                                 &rh->details.open_request.request_timestamp),
    687     GNUNET_JSON_spec_timestamp ("requested_expiration",
    688                                 &rh->details.open_request.reserve_expiration),
    689     GNUNET_JSON_spec_end ()
    690   };
    691 
    692   rh->type = TALER_EXCHANGE_RTT_OPEN;
    693   if (GNUNET_OK !=
    694       GNUNET_JSON_parse (transaction,
    695                          open_spec,
    696                          NULL, NULL))
    697   {
    698     GNUNET_break_op (0);
    699     return GNUNET_SYSERR;
    700   }
    701   if (GNUNET_OK !=
    702       TALER_wallet_reserve_open_verify (
    703         &rh->amount,
    704         rh->details.open_request.request_timestamp,
    705         rh->details.open_request.reserve_expiration,
    706         rh->details.open_request.purse_limit,
    707         uc->reserve_pub,
    708         &rh->details.open_request.reserve_sig))
    709   {
    710     GNUNET_break_op (0);
    711     return GNUNET_SYSERR;
    712   }
    713   if (0 >
    714       TALER_amount_add (uc->total_out,
    715                         uc->total_out,
    716                         &rh->amount))
    717   {
    718     /* overflow in history already!? inconceivable! Bad exchange! */
    719     GNUNET_break_op (0);
    720     return GNUNET_SYSERR;
    721   }
    722   return GNUNET_OK;
    723 }
    724 
    725 
    726 /**
    727  * Parse "close" reserve close entry.
    728  *
    729  * @param[in,out] rh entry to parse
    730  * @param uc our context
    731  * @param transaction the transaction to parse
    732  * @return #GNUNET_OK on success
    733  */
    734 static enum GNUNET_GenericReturnValue
    735 parse_close (struct TALER_EXCHANGE_ReserveHistoryEntry *rh,
    736              struct HistoryParseContext *uc,
    737              const json_t *transaction)
    738 {
    739   struct GNUNET_JSON_Specification close_spec[] = {
    740     GNUNET_JSON_spec_fixed_auto ("reserve_sig",
    741                                  &rh->details.close_request.reserve_sig),
    742     GNUNET_JSON_spec_mark_optional (
    743       GNUNET_JSON_spec_fixed_auto ("h_payto",
    744                                    &rh->details.close_request.
    745                                    target_account_h_payto),
    746       NULL),
    747     GNUNET_JSON_spec_timestamp ("request_timestamp",
    748                                 &rh->details.close_request.request_timestamp),
    749     GNUNET_JSON_spec_end ()
    750   };
    751 
    752   rh->type = TALER_EXCHANGE_RTT_CLOSE;
    753   if (GNUNET_OK !=
    754       GNUNET_JSON_parse (transaction,
    755                          close_spec,
    756                          NULL, NULL))
    757   {
    758     GNUNET_break_op (0);
    759     return GNUNET_SYSERR;
    760   }
    761   /* force amount to invalid */
    762   memset (&rh->amount,
    763           0,
    764           sizeof (rh->amount));
    765   if (GNUNET_OK !=
    766       TALER_wallet_reserve_close_verify (
    767         rh->details.close_request.request_timestamp,
    768         &rh->details.close_request.target_account_h_payto,
    769         uc->reserve_pub,
    770         &rh->details.close_request.reserve_sig))
    771   {
    772     GNUNET_break_op (0);
    773     return GNUNET_SYSERR;
    774   }
    775   return GNUNET_OK;
    776 }
    777 
    778 
    779 static void
    780 free_reserve_history (
    781   unsigned int len,
    782   struct TALER_EXCHANGE_ReserveHistoryEntry rhistory[static len])
    783 {
    784   for (unsigned int i = 0; i<len; i++)
    785   {
    786     struct TALER_EXCHANGE_ReserveHistoryEntry *rhi = &rhistory[i];
    787 
    788     switch (rhi->type)
    789     {
    790     case TALER_EXCHANGE_RTT_CREDIT:
    791       GNUNET_free (rhi->details.in_details.sender_url.full_payto);
    792       break;
    793     case TALER_EXCHANGE_RTT_WITHDRAWAL:
    794       break;
    795     case TALER_EXCHANGE_RTT_RECOUP:
    796       break;
    797     case TALER_EXCHANGE_RTT_CLOSING:
    798       break;
    799     case TALER_EXCHANGE_RTT_MERGE:
    800       break;
    801     case TALER_EXCHANGE_RTT_OPEN:
    802       break;
    803     case TALER_EXCHANGE_RTT_CLOSE:
    804       GNUNET_free (rhi->details.close_details
    805                    .receiver_account_details.full_payto);
    806       break;
    807     }
    808   }
    809   GNUNET_free (rhistory);
    810 }
    811 
    812 
    813 /**
    814  * Parse history given in JSON format and return it in binary format.
    815  *
    816  * @param keys exchange keys
    817  * @param history JSON array with the history
    818  * @param reserve_pub public key of the reserve to inspect
    819  * @param currency currency we expect the balance to be in
    820  * @param[out] total_in set to value of credits to reserve
    821  * @param[out] total_out set to value of debits from reserve
    822  * @param history_length number of entries in @a history
    823  * @param[out] rhistory array of length @a history_length, set to the
    824  *             parsed history entries
    825  * @return #GNUNET_OK if history was valid and @a rhistory and @a balance
    826  *         were set,
    827  *         #GNUNET_SYSERR if there was a protocol violation in @a history
    828  */
    829 static enum GNUNET_GenericReturnValue
    830 parse_reserve_history (
    831   const struct TALER_EXCHANGE_Keys *keys,
    832   const json_t *history,
    833   const struct TALER_ReservePublicKeyP *reserve_pub,
    834   const char *currency,
    835   struct TALER_Amount *total_in,
    836   struct TALER_Amount *total_out,
    837   unsigned int history_length,
    838   struct TALER_EXCHANGE_ReserveHistoryEntry rhistory[static history_length])
    839 {
    840   const struct
    841   {
    842     const char *type;
    843     ParseHelper helper;
    844   } map[] = {
    845     { "CREDIT", &parse_credit },
    846     { "WITHDRAW", &parse_withdraw },
    847     { "RECOUP", &parse_recoup },
    848     { "MERGE", &parse_merge },
    849     { "CLOSING", &parse_closing },
    850     { "OPEN", &parse_open },
    851     { "CLOSE", &parse_close },
    852     { NULL, NULL }
    853   };
    854   struct GNUNET_HashCode uuid[history_length];
    855   struct HistoryParseContext uc = {
    856     .keys = keys,
    857     .reserve_pub = reserve_pub,
    858     .uuids = uuid,
    859     .total_in = total_in,
    860     .total_out = total_out
    861   };
    862 
    863   GNUNET_assert (GNUNET_OK ==
    864                  TALER_amount_set_zero (currency,
    865                                         total_in));
    866   GNUNET_assert (GNUNET_OK ==
    867                  TALER_amount_set_zero (currency,
    868                                         total_out));
    869   for (unsigned int off = 0; off<history_length; off++)
    870   {
    871     struct TALER_EXCHANGE_ReserveHistoryEntry *rh = &rhistory[off];
    872     json_t *transaction;
    873     const char *type;
    874     struct GNUNET_JSON_Specification hist_spec[] = {
    875       GNUNET_JSON_spec_string ("type",
    876                                &type),
    877       TALER_JSON_spec_amount_any ("amount",
    878                                   &rh->amount),
    879       GNUNET_JSON_spec_uint64 ("history_offset",
    880                                &rh->history_offset),
    881       /* 'wire' and 'signature' are optional depending on 'type'! */
    882       GNUNET_JSON_spec_end ()
    883     };
    884     bool found = false;
    885 
    886     transaction = json_array_get (history,
    887                                   off);
    888     if (GNUNET_OK !=
    889         GNUNET_JSON_parse (transaction,
    890                            hist_spec,
    891                            NULL, NULL))
    892     {
    893       GNUNET_break_op (0);
    894       json_dumpf (transaction,
    895                   stderr,
    896                   JSON_INDENT (2));
    897       return GNUNET_SYSERR;
    898     }
    899     if (GNUNET_YES !=
    900         TALER_amount_cmp_currency (&rh->amount,
    901                                    total_in))
    902     {
    903       GNUNET_break_op (0);
    904       return GNUNET_SYSERR;
    905     }
    906     for (unsigned int i = 0; NULL != map[i].type; i++)
    907     {
    908       if (0 == strcasecmp (map[i].type,
    909                            type))
    910       {
    911         found = true;
    912         if (GNUNET_OK !=
    913             map[i].helper (rh,
    914                            &uc,
    915                            transaction))
    916         {
    917           GNUNET_break_op (0);
    918           return GNUNET_SYSERR;
    919         }
    920         break;
    921       }
    922     }
    923     if (! found)
    924     {
    925       /* unexpected 'type', protocol incompatibility, complain! */
    926       GNUNET_break_op (0);
    927       return GNUNET_SYSERR;
    928     }
    929   }
    930   return GNUNET_OK;
    931 }
    932 
    933 
    934 /**
    935  * Handle HTTP header received by curl.
    936  *
    937  * @param buffer one line of HTTP header data
    938  * @param size size of an item
    939  * @param nitems number of items passed
    940  * @param userdata our `struct TALER_EXCHANGE_GetReservesHistoryHandle *`
    941  * @return `size * nitems`
    942  */
    943 static size_t
    944 handle_header (char *buffer,
    945                size_t size,
    946                size_t nitems,
    947                void *userdata)
    948 {
    949   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh = userdata;
    950   size_t total = size * nitems;
    951   char *ndup;
    952   const char *hdr_type;
    953   char *hdr_val;
    954   char *sp;
    955 
    956   ndup = GNUNET_strndup (buffer,
    957                          total);
    958   hdr_type = strtok_r (ndup,
    959                        ":",
    960                        &sp);
    961   if (NULL == hdr_type)
    962   {
    963     GNUNET_free (ndup);
    964     return total;
    965   }
    966   hdr_val = strtok_r (NULL,
    967                       "\n\r",
    968                       &sp);
    969   if (NULL == hdr_val)
    970   {
    971     GNUNET_free (ndup);
    972     return total;
    973   }
    974   if (' ' == *hdr_val)
    975     hdr_val++;
    976   if (0 == strcasecmp (hdr_type,
    977                        MHD_HTTP_HEADER_ETAG))
    978   {
    979     unsigned long long tval;
    980     char dummy;
    981 
    982     if (1 !=
    983         sscanf (hdr_val,
    984                 "\"%llu\"%c",
    985                 &tval,
    986                 &dummy))
    987     {
    988       GNUNET_break_op (0);
    989       GNUNET_free (ndup);
    990       return 0;
    991     }
    992     grhh->etag = (uint64_t) tval;
    993   }
    994   GNUNET_free (ndup);
    995   return total;
    996 }
    997 
    998 
    999 /**
   1000  * We received an #MHD_HTTP_OK status code. Handle the JSON response.
   1001  *
   1002  * @param grhh handle of the request
   1003  * @param j JSON response
   1004  * @return #GNUNET_OK on success
   1005  */
   1006 static enum GNUNET_GenericReturnValue
   1007 handle_reserves_history_ok (
   1008   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh,
   1009   const json_t *j)
   1010 {
   1011   const json_t *history;
   1012   unsigned int len;
   1013   struct TALER_EXCHANGE_GetReservesHistoryResponse rs = {
   1014     .hr.reply = j,
   1015     .hr.http_status = MHD_HTTP_OK,
   1016     .details.ok.etag = grhh->etag
   1017   };
   1018   struct GNUNET_JSON_Specification spec[] = {
   1019     TALER_JSON_spec_amount_any ("balance",
   1020                                 &rs.details.ok.balance),
   1021     GNUNET_JSON_spec_array_const ("history",
   1022                                   &history),
   1023     GNUNET_JSON_spec_end ()
   1024   };
   1025 
   1026   if (GNUNET_OK !=
   1027       GNUNET_JSON_parse (j,
   1028                          spec,
   1029                          NULL,
   1030                          NULL))
   1031   {
   1032     GNUNET_break_op (0);
   1033     return GNUNET_SYSERR;
   1034   }
   1035   len = json_array_size (history);
   1036   {
   1037     struct TALER_EXCHANGE_ReserveHistoryEntry *rhistory;
   1038 
   1039     rhistory = GNUNET_new_array (len,
   1040                                  struct TALER_EXCHANGE_ReserveHistoryEntry);
   1041     if (GNUNET_OK !=
   1042         parse_reserve_history (grhh->keys,
   1043                                history,
   1044                                &grhh->reserve_pub,
   1045                                rs.details.ok.balance.currency,
   1046                                &rs.details.ok.total_in,
   1047                                &rs.details.ok.total_out,
   1048                                len,
   1049                                rhistory))
   1050     {
   1051       GNUNET_break_op (0);
   1052       free_reserve_history (len,
   1053                             rhistory);
   1054       GNUNET_JSON_parse_free (spec);
   1055       return GNUNET_SYSERR;
   1056     }
   1057     if (NULL != grhh->cb)
   1058     {
   1059       rs.details.ok.history = rhistory;
   1060       rs.details.ok.history_len = len;
   1061       grhh->cb (grhh->cb_cls,
   1062                 &rs);
   1063       grhh->cb = NULL;
   1064     }
   1065     free_reserve_history (len,
   1066                           rhistory);
   1067   }
   1068   return GNUNET_OK;
   1069 }
   1070 
   1071 
   1072 /**
   1073  * Function called when we're done processing the
   1074  * HTTP GET /reserves/$RESERVE_PUB/history request.
   1075  *
   1076  * @param cls the `struct TALER_EXCHANGE_GetReservesHistoryHandle`
   1077  * @param response_code HTTP response code, 0 on error
   1078  * @param response parsed JSON result, NULL on error
   1079  */
   1080 static void
   1081 handle_reserves_history_finished (void *cls,
   1082                                   long response_code,
   1083                                   const void *response)
   1084 {
   1085   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh = cls;
   1086   const json_t *j = response;
   1087   struct TALER_EXCHANGE_GetReservesHistoryResponse rs = {
   1088     .hr.reply = j,
   1089     .hr.http_status = (unsigned int) response_code
   1090   };
   1091 
   1092   grhh->job = NULL;
   1093   switch (response_code)
   1094   {
   1095   case 0:
   1096     rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
   1097     break;
   1098   case MHD_HTTP_OK:
   1099     if (GNUNET_OK !=
   1100         handle_reserves_history_ok (grhh,
   1101                                     j))
   1102     {
   1103       rs.hr.http_status = 0;
   1104       rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
   1105     }
   1106     break;
   1107   case MHD_HTTP_BAD_REQUEST:
   1108     /* This should never happen, either us or the exchange is buggy
   1109        (or API version conflict); just pass JSON reply to the application */
   1110     GNUNET_break (0);
   1111     rs.hr.ec = TALER_JSON_get_error_code (j);
   1112     rs.hr.hint = TALER_JSON_get_error_hint (j);
   1113     break;
   1114   case MHD_HTTP_FORBIDDEN:
   1115     /* This should never happen, either us or the exchange is buggy
   1116        (or API version conflict); just pass JSON reply to the application */
   1117     GNUNET_break (0);
   1118     rs.hr.ec = TALER_JSON_get_error_code (j);
   1119     rs.hr.hint = TALER_JSON_get_error_hint (j);
   1120     break;
   1121   case MHD_HTTP_NOT_FOUND:
   1122     /* Nothing really to verify, this should never
   1123        happen, we should pass the JSON reply to the application */
   1124     rs.hr.ec = TALER_JSON_get_error_code (j);
   1125     rs.hr.hint = TALER_JSON_get_error_hint (j);
   1126     break;
   1127   case MHD_HTTP_INTERNAL_SERVER_ERROR:
   1128     /* Server had an internal issue; we should retry, but this API
   1129        leaves this to the application */
   1130     rs.hr.ec = TALER_JSON_get_error_code (j);
   1131     rs.hr.hint = TALER_JSON_get_error_hint (j);
   1132     break;
   1133   default:
   1134     /* unexpected response code */
   1135     GNUNET_break_op (0);
   1136     rs.hr.ec = TALER_JSON_get_error_code (j);
   1137     rs.hr.hint = TALER_JSON_get_error_hint (j);
   1138     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
   1139                 "Unexpected response code %u/%d for reserves history\n",
   1140                 (unsigned int) response_code,
   1141                 (int) rs.hr.ec);
   1142     break;
   1143   }
   1144   if (NULL != grhh->cb)
   1145   {
   1146     grhh->cb (grhh->cb_cls,
   1147               &rs);
   1148     grhh->cb = NULL;
   1149   }
   1150   TALER_EXCHANGE_get_reserves_history_cancel (grhh);
   1151 }
   1152 
   1153 
   1154 struct TALER_EXCHANGE_GetReservesHistoryHandle *
   1155 TALER_EXCHANGE_get_reserves_history_create (
   1156   struct GNUNET_CURL_Context *ctx,
   1157   const char *url,
   1158   struct TALER_EXCHANGE_Keys *keys,
   1159   const struct TALER_ReservePrivateKeyP *reserve_priv)
   1160 {
   1161   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh;
   1162 
   1163   grhh = GNUNET_new (struct TALER_EXCHANGE_GetReservesHistoryHandle);
   1164   grhh->ctx = ctx;
   1165   grhh->base_url = GNUNET_strdup (url);
   1166   grhh->keys = TALER_EXCHANGE_keys_incref (keys);
   1167   grhh->reserve_priv = *reserve_priv;
   1168   GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
   1169                                       &grhh->reserve_pub.eddsa_pub);
   1170   return grhh;
   1171 }
   1172 
   1173 
   1174 enum GNUNET_GenericReturnValue
   1175 TALER_EXCHANGE_get_reserves_history_set_options_ (
   1176   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh,
   1177   unsigned int num_options,
   1178   const struct TALER_EXCHANGE_GetReservesHistoryOptionValue *options)
   1179 {
   1180   for (unsigned int i = 0; i < num_options; i++)
   1181   {
   1182     switch (options[i].option)
   1183     {
   1184     case TALER_EXCHANGE_GET_RESERVES_HISTORY_OPTION_END:
   1185       return GNUNET_OK;
   1186     case TALER_EXCHANGE_GET_RESERVES_HISTORY_OPTION_START_OFF:
   1187       grhh->options.start_off = options[i].details.start_off;
   1188       break;
   1189     default:
   1190       GNUNET_break (0);
   1191       return GNUNET_SYSERR;
   1192     }
   1193   }
   1194   return GNUNET_OK;
   1195 }
   1196 
   1197 
   1198 enum TALER_ErrorCode
   1199 TALER_EXCHANGE_get_reserves_history_start (
   1200   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh,
   1201   TALER_EXCHANGE_GetReservesHistoryCallback cb,
   1202   TALER_EXCHANGE_GET_RESERVES_HISTORY_RESULT_CLOSURE *cb_cls)
   1203 {
   1204   char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
   1205   struct curl_slist *job_headers;
   1206   CURL *eh;
   1207 
   1208   if (NULL != grhh->job)
   1209   {
   1210     GNUNET_break (0);
   1211     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
   1212   }
   1213   grhh->cb = cb;
   1214   grhh->cb_cls = cb_cls;
   1215   {
   1216     char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
   1217     char *end;
   1218     char start_off_str[32];
   1219 
   1220     end = GNUNET_STRINGS_data_to_string (
   1221       &grhh->reserve_pub,
   1222       sizeof (grhh->reserve_pub),
   1223       pub_str,
   1224       sizeof (pub_str));
   1225     *end = '\0';
   1226     GNUNET_snprintf (arg_str,
   1227                      sizeof (arg_str),
   1228                      "reserves/%s/history",
   1229                      pub_str);
   1230     GNUNET_snprintf (start_off_str,
   1231                      sizeof (start_off_str),
   1232                      "%llu",
   1233                      (unsigned long long) grhh->options.start_off);
   1234     grhh->url = TALER_url_join (grhh->base_url,
   1235                                 arg_str,
   1236                                 "start",
   1237                                 (0 != grhh->options.start_off)
   1238                                  ? start_off_str
   1239                                  : NULL,
   1240                                 NULL);
   1241   }
   1242   if (NULL == grhh->url)
   1243     return TALER_EC_GENERIC_CONFIGURATION_INVALID;
   1244   eh = TALER_EXCHANGE_curl_easy_get_ (grhh->url);
   1245   if (NULL == eh)
   1246   {
   1247     GNUNET_free (grhh->url);
   1248     return TALER_EC_GENERIC_CONFIGURATION_INVALID;
   1249   }
   1250   GNUNET_assert (CURLE_OK ==
   1251                  curl_easy_setopt (eh,
   1252                                    CURLOPT_HEADERFUNCTION,
   1253                                    &handle_header));
   1254   GNUNET_assert (CURLE_OK ==
   1255                  curl_easy_setopt (eh,
   1256                                    CURLOPT_HEADERDATA,
   1257                                    grhh));
   1258   {
   1259     struct TALER_ReserveSignatureP reserve_sig;
   1260     char *sig_hdr;
   1261     char *hdr;
   1262 
   1263     TALER_wallet_reserve_history_sign (grhh->options.start_off,
   1264                                        &grhh->reserve_priv,
   1265                                        &reserve_sig);
   1266     sig_hdr = GNUNET_STRINGS_data_to_string_alloc (
   1267       &reserve_sig,
   1268       sizeof (reserve_sig));
   1269     GNUNET_asprintf (&hdr,
   1270                      "%s: %s",
   1271                      TALER_RESERVE_HISTORY_SIGNATURE_HEADER,
   1272                      sig_hdr);
   1273     GNUNET_free (sig_hdr);
   1274     job_headers = curl_slist_append (NULL,
   1275                                      hdr);
   1276     GNUNET_free (hdr);
   1277     if (NULL == job_headers)
   1278     {
   1279       GNUNET_break (0);
   1280       curl_easy_cleanup (eh);
   1281       return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
   1282     }
   1283   }
   1284   grhh->job = GNUNET_CURL_job_add2 (grhh->ctx,
   1285                                     eh,
   1286                                     job_headers,
   1287                                     &handle_reserves_history_finished,
   1288                                     grhh);
   1289   curl_slist_free_all (job_headers);
   1290   if (NULL == grhh->job)
   1291     return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
   1292   return TALER_EC_NONE;
   1293 }
   1294 
   1295 
   1296 void
   1297 TALER_EXCHANGE_get_reserves_history_cancel (
   1298   struct TALER_EXCHANGE_GetReservesHistoryHandle *grhh)
   1299 {
   1300   if (NULL != grhh->job)
   1301   {
   1302     GNUNET_CURL_job_cancel (grhh->job);
   1303     grhh->job = NULL;
   1304   }
   1305   GNUNET_free (grhh->url);
   1306   GNUNET_free (grhh->base_url);
   1307   TALER_EXCHANGE_keys_decref (grhh->keys);
   1308   GNUNET_free (grhh);
   1309 }
   1310 
   1311 
   1312 /* end of exchange_api_get-reserves-RESERVE_PUB-history.c */