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


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