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


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