LCOV - code coverage report
Current view: top level - exchange - taler-exchange-httpd_refund.c (source / functions) Hit Total Coverage
Test: GNU Taler exchange coverage report Lines: 125 158 79.1 %
Date: 2021-08-30 06:43:37 Functions: 4 4 100.0 %
Legend: Lines: hit not hit

          Line data    Source code
       1             : /*
       2             :   This file is part of TALER
       3             :   Copyright (C) 2014-2020 Taler Systems SA
       4             : 
       5             :   TALER is free software; you can redistribute it and/or modify it under the
       6             :   terms of the GNU Affero 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 Affero General Public License for more details.
      12             : 
      13             :   You should have received a copy of the GNU Affero General Public License along with
      14             :   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
      15             : */
      16             : /**
      17             :  * @file taler-exchange-httpd_refund.c
      18             :  * @brief Handle refund requests; parses the POST and JSON and
      19             :  *        verifies the coin signature before handing things off
      20             :  *        to the database.
      21             :  * @author Florian Dold
      22             :  * @author Benedikt Mueller
      23             :  * @author Christian Grothoff
      24             :  */
      25             : #include "platform.h"
      26             : #include <gnunet/gnunet_util_lib.h>
      27             : #include <gnunet/gnunet_json_lib.h>
      28             : #include <jansson.h>
      29             : #include <microhttpd.h>
      30             : #include <pthread.h>
      31             : #include "taler_json_lib.h"
      32             : #include "taler_mhd_lib.h"
      33             : #include "taler-exchange-httpd_refund.h"
      34             : #include "taler-exchange-httpd_responses.h"
      35             : #include "taler-exchange-httpd_keys.h"
      36             : 
      37             : 
      38             : /**
      39             :  * Generate successful refund confirmation message.
      40             :  *
      41             :  * @param connection connection to the client
      42             :  * @param coin_pub public key of the coin
      43             :  * @param refund details about the successful refund
      44             :  * @return MHD result code
      45             :  */
      46             : static MHD_RESULT
      47           5 : reply_refund_success (struct MHD_Connection *connection,
      48             :                       const struct TALER_CoinSpendPublicKeyP *coin_pub,
      49             :                       const struct TALER_EXCHANGEDB_RefundListEntry *refund)
      50             : {
      51             :   struct TALER_ExchangePublicKeyP pub;
      52             :   struct TALER_ExchangeSignatureP sig;
      53          10 :   struct TALER_RefundConfirmationPS rc = {
      54           5 :     .purpose.purpose = htonl (TALER_SIGNATURE_EXCHANGE_CONFIRM_REFUND),
      55           5 :     .purpose.size = htonl (sizeof (rc)),
      56             :     .h_contract_terms = refund->h_contract_terms,
      57             :     .coin_pub = *coin_pub,
      58             :     .merchant = refund->merchant_pub,
      59           5 :     .rtransaction_id = GNUNET_htonll (refund->rtransaction_id)
      60             :   };
      61             :   enum TALER_ErrorCode ec;
      62             : 
      63           5 :   TALER_amount_hton (&rc.refund_amount,
      64             :                      &refund->refund_amount);
      65           5 :   if (TALER_EC_NONE !=
      66           5 :       (ec = TEH_keys_exchange_sign (&rc,
      67             :                                     &pub,
      68             :                                     &sig)))
      69             :   {
      70           0 :     return TALER_MHD_reply_with_ec (connection,
      71             :                                     ec,
      72             :                                     NULL);
      73             :   }
      74           5 :   return TALER_MHD_REPLY_JSON_PACK (
      75             :     connection,
      76             :     MHD_HTTP_OK,
      77             :     GNUNET_JSON_pack_data_auto ("exchange_sig",
      78             :                                 &sig),
      79             :     GNUNET_JSON_pack_data_auto ("exchange_pub",
      80             :                                 &pub));
      81             : }
      82             : 
      83             : 
      84             : /**
      85             :  * Execute a "/refund" transaction.  Returns a confirmation that the
      86             :  * refund was successful, or a failure if we are not aware of a
      87             :  * matching /deposit or if it is too late to do the refund.
      88             :  *
      89             :  * IF it returns a non-error code, the transaction logic MUST
      90             :  * NOT queue a MHD response.  IF it returns an hard error, the
      91             :  * transaction logic MUST queue a MHD response and set @a mhd_ret.  IF
      92             :  * it returns the soft error code, the function MAY be called again to
      93             :  * retry and MUST not queue a MHD response.
      94             :  *
      95             :  * @param cls closure with a `const struct TALER_EXCHANGEDB_Refund *`
      96             :  * @param connection MHD request which triggered the transaction
      97             :  * @param[out] mhd_ret set to MHD response status for @a connection,
      98             :  *             if transaction failed (!)
      99             :  * @return transaction status
     100             :  */
     101             : static enum GNUNET_DB_QueryStatus
     102           9 : refund_transaction (void *cls,
     103             :                     struct MHD_Connection *connection,
     104             :                     MHD_RESULT *mhd_ret)
     105             : {
     106           9 :   const struct TALER_EXCHANGEDB_Refund *refund = cls;
     107             :   struct TALER_EXCHANGEDB_TransactionList *tl; /* head of original list */
     108             :   struct TALER_EXCHANGEDB_TransactionList *tlx; /* head of sublist that applies to merchant and contract */
     109             :   struct TALER_EXCHANGEDB_TransactionList *tln; /* next element, during iteration */
     110             :   struct TALER_EXCHANGEDB_TransactionList *tlp; /* previous element in 'tl' list, during iteration */
     111             :   enum GNUNET_DB_QueryStatus qs;
     112             :   bool deposit_found; /* deposit_total initialized? */
     113             :   bool refund_found; /* refund_total initialized? */
     114             :   struct TALER_Amount deposit_total;
     115             :   struct TALER_Amount refund_total;
     116             : 
     117           9 :   tl = NULL;
     118           9 :   qs = TEH_plugin->get_coin_transactions (TEH_plugin->cls,
     119             :                                           &refund->coin.coin_pub,
     120             :                                           GNUNET_NO,
     121             :                                           &tl);
     122           9 :   if (0 > qs)
     123             :   {
     124           0 :     if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     125           0 :       *mhd_ret = TALER_MHD_reply_with_error (connection,
     126             :                                              MHD_HTTP_INTERNAL_SERVER_ERROR,
     127             :                                              TALER_EC_GENERIC_DB_FETCH_FAILED,
     128             :                                              "coin transactions");
     129           0 :     return qs;
     130             :   }
     131           9 :   deposit_found = false;
     132           9 :   refund_found = false;
     133           9 :   tlx = NULL; /* relevant subset of transactions */
     134           9 :   tln = NULL;
     135           9 :   tlp = NULL;
     136          20 :   for (struct TALER_EXCHANGEDB_TransactionList *tli = tl;
     137             :        NULL != tli;
     138          11 :        tli = tln)
     139             :   {
     140          13 :     tln = tli->next;
     141          13 :     switch (tli->type)
     142             :     {
     143           8 :     case TALER_EXCHANGEDB_TT_DEPOSIT:
     144             :       {
     145             :         const struct TALER_EXCHANGEDB_DepositListEntry *dep;
     146             : 
     147           8 :         dep = tli->details.deposit;
     148           8 :         if ( (0 == GNUNET_memcmp (&dep->merchant_pub,
     149           7 :                                   &refund->details.merchant_pub)) &&
     150           7 :              (0 == GNUNET_memcmp (&dep->h_contract_terms,
     151             :                                   &refund->details.h_contract_terms)) )
     152             :         {
     153             :           /* check if we already send the money for this /deposit */
     154           7 :           if (dep->done)
     155             :           {
     156           1 :             TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     157             :                                                     tlx);
     158           1 :             TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     159             :                                                     tln);
     160             :             /* money was already transferred to merchant, can no longer refund */
     161           1 :             *mhd_ret = TALER_MHD_reply_with_error (connection,
     162             :                                                    MHD_HTTP_GONE,
     163             :                                                    TALER_EC_EXCHANGE_REFUND_MERCHANT_ALREADY_PAID,
     164             :                                                    NULL);
     165           1 :             return GNUNET_DB_STATUS_HARD_ERROR;
     166             :           }
     167             : 
     168             :           /* deposit applies and was not yet wired; add to total (it is NOT
     169             :              the case that multiple deposits of the same coin for the same
     170             :              contract are really allowed (see UNIQUE constraint on 'deposits'
     171             :              table), but in case this changes we tolerate it with this code
     172             :              anyway). *///
     173           6 :           if (deposit_found)
     174             :           {
     175           0 :             GNUNET_assert (0 <=
     176             :                            TALER_amount_add (&deposit_total,
     177             :                                              &deposit_total,
     178             :                                              &dep->amount_with_fee));
     179             :           }
     180             :           else
     181             :           {
     182           6 :             deposit_total = dep->amount_with_fee;
     183           6 :             deposit_found = true;
     184             :           }
     185             :           /* move 'tli' from 'tl' to 'tlx' list */
     186           6 :           if (NULL == tlp)
     187           6 :             tl = tln;
     188             :           else
     189           0 :             tlp->next = tln;
     190           6 :           tli->next = tlx;
     191           6 :           tlx = tli;
     192           6 :           break;
     193             :         }
     194             :         else
     195             :         {
     196           1 :           tlp = tli;
     197             :         }
     198           1 :         break;
     199             :       }
     200           0 :     case TALER_EXCHANGEDB_TT_MELT:
     201             :       /* Melts cannot be refunded, ignore here */
     202           0 :       break;
     203           5 :     case TALER_EXCHANGEDB_TT_REFUND:
     204             :       {
     205             :         const struct TALER_EXCHANGEDB_RefundListEntry *ref;
     206             : 
     207           5 :         ref = tli->details.refund;
     208           5 :         if ( (0 != GNUNET_memcmp (&ref->merchant_pub,
     209           3 :                                   &refund->details.merchant_pub)) ||
     210           3 :              (0 != GNUNET_memcmp (&ref->h_contract_terms,
     211             :                                   &refund->details.h_contract_terms)) )
     212             :         {
     213           2 :           tlp = tli;
     214           2 :           break; /* refund does not apply to our transaction */
     215             :         }
     216             :         /* Check if existing refund request matches in everything but the amount */
     217           3 :         if ( (ref->rtransaction_id ==
     218           4 :               refund->details.rtransaction_id) &&
     219           1 :              (0 != TALER_amount_cmp (&ref->refund_amount,
     220             :                                      &refund->details.refund_amount)) )
     221             :         {
     222             :           /* Generate precondition failed response, with ONLY the conflicting entry */
     223           0 :           TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     224             :                                                   tlx);
     225           0 :           TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     226             :                                                   tln);
     227           0 :           tli->next = NULL;
     228           0 :           *mhd_ret = TALER_MHD_REPLY_JSON_PACK (
     229             :             connection,
     230             :             MHD_HTTP_PRECONDITION_FAILED,
     231             :             TALER_JSON_pack_amount ("detail",
     232             :                                     &ref->refund_amount),
     233             :             TALER_JSON_pack_ec (TALER_EC_EXCHANGE_REFUND_INCONSISTENT_AMOUNT),
     234             :             GNUNET_JSON_pack_array_steal ("history",
     235             :                                           TEH_RESPONSE_compile_transaction_history (
     236             :                                             &refund->coin.coin_pub,
     237             :                                             tli)));
     238           0 :           TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     239             :                                                   tli);
     240           0 :           return GNUNET_DB_STATUS_HARD_ERROR;
     241             :         }
     242             :         /* Check if existing refund request matches in everything including the amount */
     243           3 :         if ( (ref->rtransaction_id ==
     244           4 :               refund->details.rtransaction_id) &&
     245           1 :              (0 == TALER_amount_cmp (&ref->refund_amount,
     246             :                                      &refund->details.refund_amount)) )
     247             :         {
     248             :           /* we can blanketly approve, as this request is identical to one
     249             :              we saw before */
     250           1 :           *mhd_ret = reply_refund_success (connection,
     251             :                                            &refund->coin.coin_pub,
     252             :                                            ref);
     253           1 :           TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     254             :                                                   tlx);
     255           1 :           TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     256             :                                                   tl);
     257             :           /* we still abort the transaction, as there is nothing to be
     258             :              committed! */
     259           1 :           return GNUNET_DB_STATUS_HARD_ERROR;
     260             :         }
     261             : 
     262             :         /* We have another refund, that relates, add to total */
     263           2 :         if (refund_found)
     264             :         {
     265           0 :           GNUNET_assert (0 <=
     266             :                          TALER_amount_add (&refund_total,
     267             :                                            &refund_total,
     268             :                                            &ref->refund_amount));
     269             :         }
     270             :         else
     271             :         {
     272           2 :           refund_total = ref->refund_amount;
     273           2 :           refund_found = true;
     274             :         }
     275             :         /* move 'tli' from 'tl' to 'tlx' list */
     276           2 :         if (NULL == tlp)
     277           2 :           tl = tln;
     278             :         else
     279           0 :           tlp->next = tln;
     280           2 :         tli->next = tlx;
     281           2 :         tlx = tli;
     282           2 :         break;
     283             :       }
     284           0 :     case TALER_EXCHANGEDB_TT_OLD_COIN_RECOUP:
     285             :       /* Recoups cannot be refunded, ignore here */
     286           0 :       break;
     287           0 :     case TALER_EXCHANGEDB_TT_RECOUP:
     288             :       /* Recoups cannot be refunded, ignore here */
     289           0 :       break;
     290           0 :     case TALER_EXCHANGEDB_TT_RECOUP_REFRESH:
     291             :       /* Recoups cannot be refunded, ignore here */
     292           0 :       break;
     293             :     }
     294          11 :   }
     295             :   /* no need for 'tl' anymore, everything we may still care about is in tlx now */
     296           7 :   TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     297             :                                           tl);
     298             :   /* handle if deposit was NOT found */
     299           7 :   if (! deposit_found)
     300             :   {
     301           1 :     TALER_LOG_WARNING ("Deposit to /refund was not found\n");
     302           1 :     TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     303             :                                             tlx);
     304           1 :     *mhd_ret = TALER_MHD_reply_with_error (connection,
     305             :                                            MHD_HTTP_NOT_FOUND,
     306             :                                            TALER_EC_EXCHANGE_REFUND_DEPOSIT_NOT_FOUND,
     307             :                                            NULL);
     308           1 :     return GNUNET_DB_STATUS_HARD_ERROR;
     309             :   }
     310             : 
     311             :   /* check total refund amount is sufficiently low */
     312           6 :   if (refund_found)
     313           2 :     GNUNET_break (0 <=
     314             :                   TALER_amount_add (&refund_total,
     315             :                                     &refund_total,
     316             :                                     &refund->details.refund_amount));
     317             :   else
     318           4 :     refund_total = refund->details.refund_amount;
     319             : 
     320           6 :   if (1 == TALER_amount_cmp (&refund_total,
     321             :                              &deposit_total) )
     322             :   {
     323           2 :     *mhd_ret = TALER_MHD_REPLY_JSON_PACK (
     324             :       connection,
     325             :       MHD_HTTP_CONFLICT,
     326             :       GNUNET_JSON_pack_string ("detail",
     327             :                                "total amount refunded exceeds total amount deposited for this coin"),
     328             :       TALER_JSON_pack_ec (
     329             :         TALER_EC_EXCHANGE_REFUND_CONFLICT_DEPOSIT_INSUFFICIENT),
     330             :       GNUNET_JSON_pack_array_steal ("history",
     331             :                                     TEH_RESPONSE_compile_transaction_history (
     332             :                                       &refund->coin.coin_pub,
     333             :                                       tlx)));
     334           2 :     TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     335             :                                             tlx);
     336           2 :     return GNUNET_DB_STATUS_HARD_ERROR;
     337             :   }
     338           4 :   TEH_plugin->free_coin_transaction_list (TEH_plugin->cls,
     339             :                                           tlx);
     340             : 
     341             : 
     342             :   /* Finally, store new refund data */
     343           4 :   qs = TEH_plugin->insert_refund (TEH_plugin->cls,
     344             :                                   refund);
     345           4 :   if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     346             :   {
     347           0 :     TALER_LOG_WARNING ("Failed to store /refund information in database\n");
     348           0 :     *mhd_ret = TALER_MHD_reply_with_error (connection,
     349             :                                            MHD_HTTP_INTERNAL_SERVER_ERROR,
     350             :                                            TALER_EC_GENERIC_DB_STORE_FAILED,
     351             :                                            "refund");
     352           0 :     return qs;
     353             :   }
     354             :   /* Success or soft failure */
     355           4 :   return qs;
     356             : }
     357             : 
     358             : 
     359             : /**
     360             :  * We have parsed the JSON information about the refund, do some basic
     361             :  * sanity checks (especially that the signature on the coin is valid)
     362             :  * and then execute the refund.  Note that we need the DB to check
     363             :  * the fee structure, so this is not done here.
     364             :  *
     365             :  * @param connection the MHD connection to handle
     366             :  * @param[in,out] refund information about the refund
     367             :  * @return MHD result code
     368             :  */
     369             : static MHD_RESULT
     370          10 : verify_and_execute_refund (struct MHD_Connection *connection,
     371             :                            struct TALER_EXCHANGEDB_Refund *refund)
     372             : {
     373             :   struct GNUNET_HashCode denom_hash;
     374             : 
     375             :   {
     376          20 :     struct TALER_RefundRequestPS rr = {
     377          10 :       .purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_REFUND),
     378          10 :       .purpose.size = htonl (sizeof (rr)),
     379             :       .h_contract_terms = refund->details.h_contract_terms,
     380             :       .coin_pub = refund->coin.coin_pub,
     381             :       .merchant = refund->details.merchant_pub,
     382          10 :       .rtransaction_id = GNUNET_htonll (refund->details.rtransaction_id)
     383             :     };
     384             : 
     385          10 :     TALER_amount_hton (&rr.refund_amount,
     386          10 :                        &refund->details.refund_amount);
     387          10 :     if (GNUNET_OK !=
     388          10 :         GNUNET_CRYPTO_eddsa_verify (TALER_SIGNATURE_MERCHANT_REFUND,
     389             :                                     &rr,
     390             :                                     &refund->details.merchant_sig.eddsa_sig,
     391             :                                     &refund->details.merchant_pub.eddsa_pub))
     392             :     {
     393           1 :       TALER_LOG_WARNING ("Invalid signature on refund request\n");
     394           1 :       return TALER_MHD_reply_with_error (connection,
     395             :                                          MHD_HTTP_FORBIDDEN,
     396             :                                          TALER_EC_EXCHANGE_REFUND_MERCHANT_SIGNATURE_INVALID,
     397             :                                          NULL);
     398             :     }
     399             :   }
     400             : 
     401             :   /* Fetch the coin's denomination (hash) */
     402             :   {
     403             :     enum GNUNET_DB_QueryStatus qs;
     404             : 
     405           9 :     qs = TEH_plugin->get_coin_denomination (TEH_plugin->cls,
     406           9 :                                             &refund->coin.coin_pub,
     407             :                                             &denom_hash);
     408           9 :     if (0 > qs)
     409             :     {
     410             :       MHD_RESULT res;
     411             :       char *dhs;
     412             : 
     413           0 :       GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
     414           0 :       dhs = GNUNET_STRINGS_data_to_string_alloc (&denom_hash,
     415             :                                                  sizeof (denom_hash));
     416           0 :       res = TALER_MHD_reply_with_error (connection,
     417             :                                         MHD_HTTP_NOT_FOUND,
     418             :                                         TALER_EC_EXCHANGE_REFUND_COIN_NOT_FOUND,
     419             :                                         dhs);
     420           0 :       GNUNET_free (dhs);
     421           0 :       return res;
     422             :     }
     423             :   }
     424             : 
     425             :   {
     426             :     /* Obtain information about the coin's denomination! */
     427             :     struct TEH_DenominationKey *dk;
     428             :     MHD_RESULT mret;
     429             : 
     430           9 :     dk = TEH_keys_denomination_by_hash (&denom_hash,
     431             :                                         connection,
     432             :                                         &mret);
     433           9 :     if (NULL == dk)
     434             :     {
     435             :       /* DKI not found, but we do have a coin with this DK in our database;
     436             :          not good... */
     437           0 :       GNUNET_break (0);
     438           0 :       return mret;
     439             :     }
     440           9 :     refund->details.refund_fee = dk->meta.fee_refund;
     441             :   }
     442             : 
     443             :   /* Finally run the actual transaction logic */
     444             :   {
     445             :     MHD_RESULT mhd_ret;
     446             : 
     447           9 :     if (GNUNET_OK !=
     448           9 :         TEH_DB_run_transaction (connection,
     449             :                                 "run refund",
     450             :                                 &mhd_ret,
     451             :                                 &refund_transaction,
     452             :                                 (void *) refund))
     453             :     {
     454           5 :       return mhd_ret;
     455             :     }
     456             :   }
     457           4 :   return reply_refund_success (connection,
     458           4 :                                &refund->coin.coin_pub,
     459           4 :                                &refund->details);
     460             : }
     461             : 
     462             : 
     463             : /**
     464             :  * Handle a "/coins/$COIN_PUB/refund" request.  Parses the JSON, and, if
     465             :  * successful, passes the JSON data to #verify_and_execute_refund() to further
     466             :  * check the details of the operation specified.  If everything checks out,
     467             :  * this will ultimately lead to the refund being executed, or rejected.
     468             :  *
     469             :  * @param connection the MHD connection to handle
     470             :  * @param coin_pub public key of the coin
     471             :  * @param root uploaded JSON data
     472             :  * @return MHD result code
     473             :   */
     474             : MHD_RESULT
     475          11 : TEH_handler_refund (struct MHD_Connection *connection,
     476             :                     const struct TALER_CoinSpendPublicKeyP *coin_pub,
     477             :                     const json_t *root)
     478             : {
     479          11 :   struct TALER_EXCHANGEDB_Refund refund = {
     480             :     .details.refund_fee.currency = {0}                                        /* set to invalid, just to be sure */
     481             :   };
     482             :   struct GNUNET_JSON_Specification spec[] = {
     483          11 :     TALER_JSON_spec_amount ("refund_amount",
     484             :                             TEH_currency,
     485             :                             &refund.details.refund_amount),
     486          11 :     GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
     487             :                                  &refund.details.h_contract_terms),
     488          11 :     GNUNET_JSON_spec_fixed_auto ("merchant_pub",
     489             :                                  &refund.details.merchant_pub),
     490          11 :     GNUNET_JSON_spec_uint64 ("rtransaction_id",
     491             :                              &refund.details.rtransaction_id),
     492          11 :     GNUNET_JSON_spec_fixed_auto ("merchant_sig",
     493             :                                  &refund.details.merchant_sig),
     494          11 :     GNUNET_JSON_spec_end ()
     495             :   };
     496             : 
     497          11 :   refund.coin.coin_pub = *coin_pub;
     498             :   {
     499             :     enum GNUNET_GenericReturnValue res;
     500             : 
     501          11 :     res = TALER_MHD_parse_json_data (connection,
     502             :                                      root,
     503             :                                      spec);
     504          11 :     if (GNUNET_SYSERR == res)
     505           0 :       return MHD_NO; /* hard failure */
     506          11 :     if (GNUNET_NO == res)
     507           1 :       return MHD_YES; /* failure */
     508             :   }
     509             :   {
     510             :     MHD_RESULT res;
     511             : 
     512          10 :     res = verify_and_execute_refund (connection,
     513             :                                      &refund);
     514          10 :     GNUNET_JSON_parse_free (spec);
     515          10 :     return res;
     516             :   }
     517             : }
     518             : 
     519             : 
     520             : /* end of taler-exchange-httpd_refund.c */

Generated by: LCOV version 1.14