LCOV - code coverage report
Current view: top level - lib - exchange_api_reserves_open.c (source / functions) Coverage Total Hit
Test: coverage.info Lines: 46.8 % 188 88
Test Date: 2025-12-28 14:06:02 Functions: 83.3 % 6 5

            Line data    Source code
       1              : /*
       2              :   This file is part of TALER
       3              :   Copyright (C) 2014-2023 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_reserves_open.c
      19              :  * @brief Implementation of the POST /reserves/$RESERVE_PUB/open requests
      20              :  * @author Christian Grothoff
      21              :  */
      22              : #include "taler/platform.h"
      23              : #include <jansson.h>
      24              : #include <microhttpd.h> /* just for HTTP open 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_common.h"
      31              : #include "exchange_api_handle.h"
      32              : #include "taler/taler_signatures.h"
      33              : #include "exchange_api_curl_defaults.h"
      34              : 
      35              : 
      36              : /**
      37              :  * Information we keep per coin to validate the reply.
      38              :  */
      39              : struct CoinData
      40              : {
      41              :   /**
      42              :    * Public key of the coin.
      43              :    */
      44              :   struct TALER_CoinSpendPublicKeyP coin_pub;
      45              : 
      46              :   /**
      47              :    * Signature by the coin.
      48              :    */
      49              :   struct TALER_CoinSpendSignatureP coin_sig;
      50              : 
      51              :   /**
      52              :    * The hash of the denomination's public key
      53              :    */
      54              :   struct TALER_DenominationHashP h_denom_pub;
      55              : 
      56              :   /**
      57              :    * How much did this coin contribute.
      58              :    */
      59              :   struct TALER_Amount contribution;
      60              : };
      61              : 
      62              : 
      63              : /**
      64              :  * @brief A /reserves/$RID/open Handle
      65              :  */
      66              : struct TALER_EXCHANGE_ReservesOpenHandle
      67              : {
      68              : 
      69              :   /**
      70              :    * The keys of the exchange this request handle will use
      71              :    */
      72              :   struct TALER_EXCHANGE_Keys *keys;
      73              : 
      74              :   /**
      75              :    * The url for this request.
      76              :    */
      77              :   char *url;
      78              : 
      79              :   /**
      80              :    * Handle for the request.
      81              :    */
      82              :   struct GNUNET_CURL_Job *job;
      83              : 
      84              :   /**
      85              :    * Context for #TEH_curl_easy_post(). Keeps the data that must
      86              :    * persist for Curl to make the upload.
      87              :    */
      88              :   struct TALER_CURL_PostContext post_ctx;
      89              : 
      90              :   /**
      91              :    * Function to call with the result.
      92              :    */
      93              :   TALER_EXCHANGE_ReservesOpenCallback cb;
      94              : 
      95              :   /**
      96              :    * Closure for @a cb.
      97              :    */
      98              :   void *cb_cls;
      99              : 
     100              :   /**
     101              :    * Information we keep per coin to validate the reply.
     102              :    */
     103              :   struct CoinData *coins;
     104              : 
     105              :   /**
     106              :    * Length of the @e coins array.
     107              :    */
     108              :   unsigned int num_coins;
     109              : 
     110              :   /**
     111              :    * Public key of the reserve we are querying.
     112              :    */
     113              :   struct TALER_ReservePublicKeyP reserve_pub;
     114              : 
     115              :   /**
     116              :    * Our signature.
     117              :    */
     118              :   struct TALER_ReserveSignatureP reserve_sig;
     119              : 
     120              :   /**
     121              :    * When did we make the request.
     122              :    */
     123              :   struct GNUNET_TIME_Timestamp ts;
     124              : 
     125              : };
     126              : 
     127              : 
     128              : /**
     129              :  * We received an #MHD_HTTP_OK open code. Handle the JSON
     130              :  * response.
     131              :  *
     132              :  * @param roh handle of the request
     133              :  * @param j JSON response
     134              :  * @return #GNUNET_OK on success
     135              :  */
     136              : static enum GNUNET_GenericReturnValue
     137            4 : handle_reserves_open_ok (struct TALER_EXCHANGE_ReservesOpenHandle *roh,
     138              :                          const json_t *j)
     139              : {
     140            4 :   struct TALER_EXCHANGE_ReserveOpenResult rs = {
     141              :     .hr.reply = j,
     142              :     .hr.http_status = MHD_HTTP_OK,
     143              :   };
     144              :   struct GNUNET_JSON_Specification spec[] = {
     145            4 :     TALER_JSON_spec_amount_any ("open_cost",
     146              :                                 &rs.details.ok.open_cost),
     147            4 :     GNUNET_JSON_spec_timestamp ("reserve_expiration",
     148              :                                 &rs.details.ok.expiration_time),
     149            4 :     GNUNET_JSON_spec_end ()
     150              :   };
     151              : 
     152            4 :   if (GNUNET_OK !=
     153            4 :       GNUNET_JSON_parse (j,
     154              :                          spec,
     155              :                          NULL,
     156              :                          NULL))
     157              :   {
     158            0 :     GNUNET_break_op (0);
     159            0 :     return GNUNET_SYSERR;
     160              :   }
     161            4 :   roh->cb (roh->cb_cls,
     162              :            &rs);
     163            4 :   roh->cb = NULL;
     164            4 :   GNUNET_JSON_parse_free (spec);
     165            4 :   return GNUNET_OK;
     166              : }
     167              : 
     168              : 
     169              : /**
     170              :  * We received an #MHD_HTTP_PAYMENT_REQUIRED open code. Handle the JSON
     171              :  * response.
     172              :  *
     173              :  * @param roh handle of the request
     174              :  * @param j JSON response
     175              :  * @return #GNUNET_OK on success
     176              :  */
     177              : static enum GNUNET_GenericReturnValue
     178            2 : handle_reserves_open_pr (struct TALER_EXCHANGE_ReservesOpenHandle *roh,
     179              :                          const json_t *j)
     180              : {
     181            2 :   struct TALER_EXCHANGE_ReserveOpenResult rs = {
     182              :     .hr.reply = j,
     183              :     .hr.http_status = MHD_HTTP_PAYMENT_REQUIRED,
     184              :   };
     185              :   struct GNUNET_JSON_Specification spec[] = {
     186            2 :     TALER_JSON_spec_amount_any ("open_cost",
     187              :                                 &rs.details.payment_required.open_cost),
     188            2 :     GNUNET_JSON_spec_timestamp ("reserve_expiration",
     189              :                                 &rs.details.payment_required.expiration_time),
     190            2 :     GNUNET_JSON_spec_end ()
     191              :   };
     192              : 
     193            2 :   if (GNUNET_OK !=
     194            2 :       GNUNET_JSON_parse (j,
     195              :                          spec,
     196              :                          NULL,
     197              :                          NULL))
     198              :   {
     199            0 :     GNUNET_break_op (0);
     200            0 :     return GNUNET_SYSERR;
     201              :   }
     202            2 :   roh->cb (roh->cb_cls,
     203              :            &rs);
     204            2 :   roh->cb = NULL;
     205            2 :   GNUNET_JSON_parse_free (spec);
     206            2 :   return GNUNET_OK;
     207              : }
     208              : 
     209              : 
     210              : /**
     211              :  * We received an #MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS open code. Handle the JSON
     212              :  * response.
     213              :  *
     214              :  * @param roh handle of the request
     215              :  * @param j JSON response
     216              :  * @return #GNUNET_OK on success
     217              :  */
     218              : static enum GNUNET_GenericReturnValue
     219            0 : handle_reserves_open_kyc (struct TALER_EXCHANGE_ReservesOpenHandle *roh,
     220              :                           const json_t *j)
     221              : {
     222            0 :   struct TALER_EXCHANGE_ReserveOpenResult rs = {
     223              :     .hr.reply = j,
     224              :     .hr.http_status = MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
     225              :   };
     226              :   struct GNUNET_JSON_Specification spec[] = {
     227            0 :     GNUNET_JSON_spec_fixed_auto (
     228              :       "h_payto",
     229              :       &rs.details.unavailable_for_legal_reasons.h_payto),
     230            0 :     GNUNET_JSON_spec_uint64 (
     231              :       "requirement_row",
     232              :       &rs.details.unavailable_for_legal_reasons.requirement_row),
     233            0 :     GNUNET_JSON_spec_end ()
     234              :   };
     235              : 
     236            0 :   if (GNUNET_OK !=
     237            0 :       GNUNET_JSON_parse (j,
     238              :                          spec,
     239              :                          NULL,
     240              :                          NULL))
     241              :   {
     242            0 :     GNUNET_break_op (0);
     243            0 :     return GNUNET_SYSERR;
     244              :   }
     245            0 :   roh->cb (roh->cb_cls,
     246              :            &rs);
     247            0 :   roh->cb = NULL;
     248            0 :   GNUNET_JSON_parse_free (spec);
     249            0 :   return GNUNET_OK;
     250              : }
     251              : 
     252              : 
     253              : /**
     254              :  * Function called when we're done processing the
     255              :  * HTTP /reserves/$RID/open request.
     256              :  *
     257              :  * @param cls the `struct TALER_EXCHANGE_ReservesOpenHandle`
     258              :  * @param response_code HTTP response code, 0 on error
     259              :  * @param response parsed JSON result, NULL on error
     260              :  */
     261              : static void
     262            6 : handle_reserves_open_finished (void *cls,
     263              :                                long response_code,
     264              :                                const void *response)
     265              : {
     266            6 :   struct TALER_EXCHANGE_ReservesOpenHandle *roh = cls;
     267            6 :   const json_t *j = response;
     268            6 :   struct TALER_EXCHANGE_ReserveOpenResult rs = {
     269              :     .hr.reply = j,
     270            6 :     .hr.http_status = (unsigned int) response_code
     271              :   };
     272              : 
     273            6 :   roh->job = NULL;
     274            6 :   switch (response_code)
     275              :   {
     276            0 :   case 0:
     277            0 :     rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
     278            0 :     break;
     279            4 :   case MHD_HTTP_OK:
     280            4 :     if (GNUNET_OK !=
     281            4 :         handle_reserves_open_ok (roh,
     282              :                                  j))
     283              :     {
     284            0 :       GNUNET_break_op (0);
     285            0 :       rs.hr.http_status = 0;
     286            0 :       rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     287              :     }
     288            4 :     break;
     289            0 :   case MHD_HTTP_BAD_REQUEST:
     290              :     /* This should never happen, either us or the exchange is buggy
     291              :        (or API version conflict); just pass JSON reply to the application */
     292            0 :     GNUNET_break (0);
     293            0 :     json_dumpf (j,
     294              :                 stderr,
     295              :                 JSON_INDENT (2));
     296            0 :     rs.hr.ec = TALER_JSON_get_error_code (j);
     297            0 :     rs.hr.hint = TALER_JSON_get_error_hint (j);
     298            0 :     break;
     299            2 :   case MHD_HTTP_PAYMENT_REQUIRED:
     300            2 :     if (GNUNET_OK !=
     301            2 :         handle_reserves_open_pr (roh,
     302              :                                  j))
     303              :     {
     304            0 :       GNUNET_break_op (0);
     305            0 :       rs.hr.http_status = 0;
     306            0 :       rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     307              :     }
     308            2 :     break;
     309            0 :   case MHD_HTTP_FORBIDDEN:
     310              :     /* This should never happen, either us or the exchange is buggy
     311              :        (or API version conflict); just pass JSON reply to the application */
     312            0 :     GNUNET_break (0);
     313            0 :     rs.hr.ec = TALER_JSON_get_error_code (j);
     314            0 :     rs.hr.hint = TALER_JSON_get_error_hint (j);
     315            0 :     break;
     316            0 :   case MHD_HTTP_NOT_FOUND:
     317              :     /* Nothing really to verify, this should never
     318              :        happen, we should pass the JSON reply to the application */
     319            0 :     rs.hr.ec = TALER_JSON_get_error_code (j);
     320            0 :     rs.hr.hint = TALER_JSON_get_error_hint (j);
     321            0 :     break;
     322            0 :   case MHD_HTTP_CONFLICT:
     323              :     {
     324            0 :       const struct CoinData *cd = NULL;
     325              :       struct GNUNET_JSON_Specification spec[] = {
     326            0 :         GNUNET_JSON_spec_fixed_auto ("coin_pub",
     327              :                                      &rs.details.conflict.coin_pub),
     328            0 :         GNUNET_JSON_spec_end ()
     329              :       };
     330              : 
     331            0 :       if (GNUNET_OK !=
     332            0 :           GNUNET_JSON_parse (j,
     333              :                              spec,
     334              :                              NULL,
     335              :                              NULL))
     336              :       {
     337            0 :         GNUNET_break_op (0);
     338            0 :         rs.hr.http_status = 0;
     339            0 :         rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     340            0 :         break;
     341              :       }
     342            0 :       for (unsigned int i = 0; i<roh->num_coins; i++)
     343              :       {
     344            0 :         const struct CoinData *cdi = &roh->coins[i];
     345              : 
     346            0 :         if (0 == GNUNET_memcmp (&rs.details.conflict.coin_pub,
     347              :                                 &cdi->coin_pub))
     348              :         {
     349            0 :           cd = cdi;
     350            0 :           break;
     351              :         }
     352              :       }
     353            0 :       if (NULL == cd)
     354              :       {
     355            0 :         GNUNET_break_op (0);
     356            0 :         rs.hr.http_status = 0;
     357            0 :         rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     358            0 :         break;
     359              :       }
     360            0 :       rs.hr.ec = TALER_JSON_get_error_code (j);
     361            0 :       rs.hr.hint = TALER_JSON_get_error_hint (j);
     362            0 :       break;
     363              :     }
     364            0 :   case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
     365            0 :     if (GNUNET_OK !=
     366            0 :         handle_reserves_open_kyc (roh,
     367              :                                   j))
     368              :     {
     369            0 :       GNUNET_break_op (0);
     370            0 :       rs.hr.http_status = 0;
     371            0 :       rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
     372              :     }
     373            0 :     break;
     374            0 :   case MHD_HTTP_INTERNAL_SERVER_ERROR:
     375              :     /* Server had an internal issue; we should retry, but this API
     376              :        leaves this to the application */
     377            0 :     rs.hr.ec = TALER_JSON_get_error_code (j);
     378            0 :     rs.hr.hint = TALER_JSON_get_error_hint (j);
     379            0 :     break;
     380            0 :   default:
     381              :     /* unexpected response code */
     382            0 :     GNUNET_break_op (0);
     383            0 :     rs.hr.ec = TALER_JSON_get_error_code (j);
     384            0 :     rs.hr.hint = TALER_JSON_get_error_hint (j);
     385            0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     386              :                 "Unexpected response code %u/%d for reserves open\n",
     387              :                 (unsigned int) response_code,
     388              :                 (int) rs.hr.ec);
     389            0 :     break;
     390              :   }
     391            6 :   if (NULL != roh->cb)
     392              :   {
     393            0 :     roh->cb (roh->cb_cls,
     394              :              &rs);
     395            0 :     roh->cb = NULL;
     396              :   }
     397            6 :   TALER_EXCHANGE_reserves_open_cancel (roh);
     398            6 : }
     399              : 
     400              : 
     401              : struct TALER_EXCHANGE_ReservesOpenHandle *
     402            6 : TALER_EXCHANGE_reserves_open (
     403              :   struct GNUNET_CURL_Context *ctx,
     404              :   const char *url,
     405              :   struct TALER_EXCHANGE_Keys *keys,
     406              :   const struct TALER_ReservePrivateKeyP *reserve_priv,
     407              :   const struct TALER_Amount *reserve_contribution,
     408              :   unsigned int coin_payments_length,
     409              :   const struct TALER_EXCHANGE_PurseDeposit coin_payments[
     410              :     static coin_payments_length],
     411              :   struct GNUNET_TIME_Timestamp expiration_time,
     412              :   uint32_t min_purses,
     413              :   TALER_EXCHANGE_ReservesOpenCallback cb,
     414              :   void *cb_cls)
     415            6 : {
     416              :   struct TALER_EXCHANGE_ReservesOpenHandle *roh;
     417              :   CURL *eh;
     418              :   char arg_str[sizeof (struct TALER_ReservePublicKeyP) * 2 + 32];
     419              :   json_t *cpa;
     420              : 
     421            6 :   roh = GNUNET_new (struct TALER_EXCHANGE_ReservesOpenHandle);
     422            6 :   roh->cb = cb;
     423            6 :   roh->cb_cls = cb_cls;
     424            6 :   roh->ts = GNUNET_TIME_timestamp_get ();
     425            6 :   GNUNET_CRYPTO_eddsa_key_get_public (&reserve_priv->eddsa_priv,
     426              :                                       &roh->reserve_pub.eddsa_pub);
     427              :   {
     428              :     char pub_str[sizeof (struct TALER_ReservePublicKeyP) * 2];
     429              :     char *end;
     430              : 
     431            6 :     end = GNUNET_STRINGS_data_to_string (
     432            6 :       &roh->reserve_pub,
     433              :       sizeof (roh->reserve_pub),
     434              :       pub_str,
     435              :       sizeof (pub_str));
     436            6 :     *end = '\0';
     437            6 :     GNUNET_snprintf (arg_str,
     438              :                      sizeof (arg_str),
     439              :                      "reserves/%s/open",
     440              :                      pub_str);
     441              :   }
     442            6 :   roh->url = TALER_url_join (url,
     443              :                              arg_str,
     444              :                              NULL);
     445            6 :   if (NULL == roh->url)
     446              :   {
     447            0 :     GNUNET_free (roh);
     448            0 :     return NULL;
     449              :   }
     450            6 :   eh = TALER_EXCHANGE_curl_easy_get_ (roh->url);
     451            6 :   if (NULL == eh)
     452              :   {
     453            0 :     GNUNET_break (0);
     454            0 :     GNUNET_free (roh->url);
     455            0 :     GNUNET_free (roh);
     456            0 :     return NULL;
     457              :   }
     458            6 :   TALER_wallet_reserve_open_sign (reserve_contribution,
     459              :                                   roh->ts,
     460              :                                   expiration_time,
     461              :                                   min_purses,
     462              :                                   reserve_priv,
     463              :                                   &roh->reserve_sig);
     464            6 :   roh->coins = GNUNET_new_array (coin_payments_length,
     465              :                                  struct CoinData);
     466            6 :   cpa = json_array ();
     467            6 :   GNUNET_assert (NULL != cpa);
     468            8 :   for (unsigned int i = 0; i<coin_payments_length; i++)
     469              :   {
     470            2 :     const struct TALER_EXCHANGE_PurseDeposit *pd = &coin_payments[i];
     471            2 :     const struct TALER_AgeCommitmentProof *acp = pd->age_commitment_proof;
     472              :     struct TALER_AgeCommitmentHashP ahac;
     473            2 :     struct TALER_AgeCommitmentHashP *achp = NULL;
     474            2 :     struct CoinData *cd = &roh->coins[i];
     475              :     json_t *cp;
     476              : 
     477            2 :     cd->contribution = pd->amount;
     478            2 :     cd->h_denom_pub = pd->h_denom_pub;
     479            2 :     if (NULL != acp)
     480              :     {
     481            0 :       TALER_age_commitment_hash (&acp->commitment,
     482              :                                  &ahac);
     483            0 :       achp = &ahac;
     484              :     }
     485            2 :     TALER_wallet_reserve_open_deposit_sign (&pd->amount,
     486            2 :                                             &roh->reserve_sig,
     487              :                                             &pd->coin_priv,
     488              :                                             &cd->coin_sig);
     489            2 :     GNUNET_CRYPTO_eddsa_key_get_public (&pd->coin_priv.eddsa_priv,
     490              :                                         &cd->coin_pub.eddsa_pub);
     491              : 
     492            2 :     cp = GNUNET_JSON_PACK (
     493              :       GNUNET_JSON_pack_allow_null (
     494              :         GNUNET_JSON_pack_data_auto ("h_age_commitment",
     495              :                                     achp)),
     496              :       TALER_JSON_pack_amount ("amount",
     497              :                               &pd->amount),
     498              :       GNUNET_JSON_pack_data_auto ("denom_pub_hash",
     499              :                                   &pd->h_denom_pub),
     500              :       TALER_JSON_pack_denom_sig ("ub_sig",
     501              :                                  &pd->denom_sig),
     502              :       GNUNET_JSON_pack_data_auto ("coin_pub",
     503              :                                   &cd->coin_pub),
     504              :       GNUNET_JSON_pack_data_auto ("coin_sig",
     505              :                                   &cd->coin_sig));
     506            2 :     GNUNET_assert (0 ==
     507              :                    json_array_append_new (cpa,
     508              :                                           cp));
     509              :   }
     510              :   {
     511            6 :     json_t *open_obj = GNUNET_JSON_PACK (
     512              :       GNUNET_JSON_pack_timestamp ("request_timestamp",
     513              :                                   roh->ts),
     514              :       GNUNET_JSON_pack_timestamp ("reserve_expiration",
     515              :                                   expiration_time),
     516              :       GNUNET_JSON_pack_array_steal ("payments",
     517              :                                     cpa),
     518              :       TALER_JSON_pack_amount ("reserve_payment",
     519              :                               reserve_contribution),
     520              :       GNUNET_JSON_pack_uint64 ("purse_limit",
     521              :                                min_purses),
     522              :       GNUNET_JSON_pack_data_auto ("reserve_sig",
     523              :                                   &roh->reserve_sig));
     524              : 
     525            6 :     if (GNUNET_OK !=
     526            6 :         TALER_curl_easy_post (&roh->post_ctx,
     527              :                               eh,
     528              :                               open_obj))
     529              :     {
     530            0 :       GNUNET_break (0);
     531            0 :       curl_easy_cleanup (eh);
     532            0 :       json_decref (open_obj);
     533            0 :       GNUNET_free (roh->coins);
     534            0 :       GNUNET_free (roh->url);
     535            0 :       GNUNET_free (roh);
     536            0 :       return NULL;
     537              :     }
     538            6 :     json_decref (open_obj);
     539              :   }
     540            6 :   roh->keys = TALER_EXCHANGE_keys_incref (keys);
     541           12 :   roh->job = GNUNET_CURL_job_add2 (ctx,
     542              :                                    eh,
     543            6 :                                    roh->post_ctx.headers,
     544              :                                    &handle_reserves_open_finished,
     545              :                                    roh);
     546            6 :   return roh;
     547              : }
     548              : 
     549              : 
     550              : void
     551            6 : TALER_EXCHANGE_reserves_open_cancel (
     552              :   struct TALER_EXCHANGE_ReservesOpenHandle *roh)
     553              : {
     554            6 :   if (NULL != roh->job)
     555              :   {
     556            0 :     GNUNET_CURL_job_cancel (roh->job);
     557            0 :     roh->job = NULL;
     558              :   }
     559            6 :   TALER_curl_easy_post_finished (&roh->post_ctx);
     560            6 :   GNUNET_free (roh->coins);
     561            6 :   GNUNET_free (roh->url);
     562            6 :   TALER_EXCHANGE_keys_decref (roh->keys);
     563            6 :   GNUNET_free (roh);
     564            6 : }
     565              : 
     566              : 
     567              : /* end of exchange_api_reserves_open.c */
        

Generated by: LCOV version 2.0-1