/*
  This file is part of TALER
  Copyright (C) 2014-2026 Taler Systems SA

  TALER is free software; you can redistribute it and/or modify it under the
  terms of the GNU General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  TALER is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
  A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

  You should have received a copy of the GNU General Public License along with
  TALER; see the file COPYING.  If not, see
  <http://www.gnu.org/licenses/>
*/
/**
 * @file lib/exchange_api_get-coins-COIN_PUB-history.c
 * @brief Implementation of the GET /coins/$COIN_PUB/history request
 * @author Christian Grothoff
 */
#include <jansson.h>
#include <microhttpd.h> /* just for HTTP status codes */
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_json_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include "taler/taler_json_lib.h"
#include "exchange_api_handle.h"
#include "taler/taler_signatures.h"
#include "exchange_api_curl_defaults.h"


/**
 * @brief A GET /coins/$COIN_PUB/history Handle
 */
struct TALER_EXCHANGE_GetCoinsHistoryHandle
{

  /**
   * Base URL of the exchange.
   */
  char *base_url;

  /**
   * The url for this request.
   */
  char *url;

  /**
   * Handle for the request.
   */
  struct GNUNET_CURL_Job *job;

  /**
   * Function to call with the result.
   */
  TALER_EXCHANGE_GetCoinsHistoryCallback cb;

  /**
   * Closure for @e cb.
   */
  TALER_EXCHANGE_GET_COINS_HISTORY_RESULT_CLOSURE *cb_cls;

  /**
   * CURL context to use.
   */
  struct GNUNET_CURL_Context *ctx;

  /**
   * Private key of the coin (for signing in _start).
   */
  struct TALER_CoinSpendPrivateKeyP coin_priv;

  /**
   * Public key of the coin we are querying.
   */
  struct TALER_CoinSpendPublicKeyP coin_pub;

  /**
   * Options set for this request.
   */
  struct
  {
    /**
     * Only return entries with history_offset > this value.
     * Default: 0 (return all entries).
     */
    uint64_t start_off;
  } options;

};


/**
 * Context for coin helpers.
 */
struct CoinHistoryParseContext
{

  /**
   * Keys of the exchange.
   */
  const struct TALER_EXCHANGE_Keys *keys;

  /**
   * Denomination of the coin.
   */
  const struct TALER_EXCHANGE_DenomPublicKey *dk;

  /**
   * Our coin public key.
   */
  const struct TALER_CoinSpendPublicKeyP *coin_pub;

  /**
   * Where to sum up total refunds.
   */
  struct TALER_Amount *total_in;

  /**
   * Total amount encountered.
   */
  struct TALER_Amount *total_out;

};


/**
 * Signature of functions that operate on one of
 * the coin's history entries.
 *
 * @param[in,out] pc overall context
 * @param[out] rh where to write the history entry
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
typedef enum GNUNET_GenericReturnValue
(*CoinCheckHelper)(struct CoinHistoryParseContext *pc,
                   struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                   const struct TALER_Amount *amount,
                   json_t *transaction);


/**
 * Handle deposit entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_deposit (struct CoinHistoryParseContext *pc,
              struct TALER_EXCHANGE_CoinHistoryEntry *rh,
              const struct TALER_Amount *amount,
              json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any ("deposit_fee",
                                &rh->details.deposit.deposit_fee),
    GNUNET_JSON_spec_fixed_auto ("merchant_pub",
                                 &rh->details.deposit.merchant_pub),
    GNUNET_JSON_spec_timestamp ("timestamp",
                                &rh->details.deposit.wallet_timestamp),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_timestamp ("refund_deadline",
                                  &rh->details.deposit.refund_deadline),
      NULL),
    GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
                                 &rh->details.deposit.h_contract_terms),
    GNUNET_JSON_spec_fixed_auto ("h_wire",
                                 &rh->details.deposit.h_wire),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("h_policy",
                                   &rh->details.deposit.h_policy),
      &rh->details.deposit.no_h_policy),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("wallet_data_hash",
                                   &rh->details.deposit.wallet_data_hash),
      &rh->details.deposit.no_wallet_data_hash),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
                                   &rh->details.deposit.hac),
      &rh->details.deposit.no_hac),
    GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                 &rh->details.deposit.sig),
    GNUNET_JSON_spec_end ()
  };

  rh->details.deposit.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS;
  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_deposit_verify (
        amount,
        &rh->details.deposit.deposit_fee,
        &rh->details.deposit.h_wire,
        &rh->details.deposit.h_contract_terms,
        rh->details.deposit.no_wallet_data_hash
        ? NULL
        : &rh->details.deposit.wallet_data_hash,
        rh->details.deposit.no_hac
        ? NULL
        : &rh->details.deposit.hac,
        rh->details.deposit.no_h_policy
        ? NULL
        : &rh->details.deposit.h_policy,
        &pc->dk->h_key,
        rh->details.deposit.wallet_timestamp,
        &rh->details.deposit.merchant_pub,
        rh->details.deposit.refund_deadline,
        pc->coin_pub,
        &rh->details.deposit.sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  /* check that deposit fee matches our expectations from /keys! */
  if ( (GNUNET_YES !=
        TALER_amount_cmp_currency (&rh->details.deposit.deposit_fee,
                                   &pc->dk->fees.deposit)) ||
       (0 !=
        TALER_amount_cmp (&rh->details.deposit.deposit_fee,
                          &pc->dk->fees.deposit)) )
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_YES;
}


/**
 * Handle melt entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_melt (struct CoinHistoryParseContext *pc,
           struct TALER_EXCHANGE_CoinHistoryEntry *rh,
           const struct TALER_Amount *amount,
           json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any ("melt_fee",
                                &rh->details.melt.melt_fee),
    GNUNET_JSON_spec_fixed_auto ("rc",
                                 &rh->details.melt.rc),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
                                   &rh->details.melt.h_age_commitment),
      &rh->details.melt.no_hac),
    GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                 &rh->details.melt.sig),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }

  /* check that melt fee matches our expectations from /keys! */
  if ( (GNUNET_YES !=
        TALER_amount_cmp_currency (&rh->details.melt.melt_fee,
                                   &pc->dk->fees.refresh)) ||
       (0 !=
        TALER_amount_cmp (&rh->details.melt.melt_fee,
                          &pc->dk->fees.refresh)) )
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_melt_verify (
        amount,
        &rh->details.melt.melt_fee,
        &rh->details.melt.rc,
        &pc->dk->h_key,
        rh->details.melt.no_hac
        ? NULL
        : &rh->details.melt.h_age_commitment,
        pc->coin_pub,
        &rh->details.melt.sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_YES;
}


/**
 * Handle refund entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_refund (struct CoinHistoryParseContext *pc,
             struct TALER_EXCHANGE_CoinHistoryEntry *rh,
             const struct TALER_Amount *amount,
             json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any ("refund_fee",
                                &rh->details.refund.refund_fee),
    GNUNET_JSON_spec_fixed_auto ("h_contract_terms",
                                 &rh->details.refund.h_contract_terms),
    GNUNET_JSON_spec_fixed_auto ("merchant_pub",
                                 &rh->details.refund.merchant_pub),
    GNUNET_JSON_spec_uint64 ("rtransaction_id",
                             &rh->details.refund.rtransaction_id),
    GNUNET_JSON_spec_fixed_auto ("merchant_sig",
                                 &rh->details.refund.sig),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (0 >
      TALER_amount_add (&rh->details.refund.sig_amount,
                        &rh->details.refund.refund_fee,
                        amount))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_merchant_refund_verify (pc->coin_pub,
                                    &rh->details.refund.h_contract_terms,
                                    rh->details.refund.rtransaction_id,
                                    &rh->details.refund.sig_amount,
                                    &rh->details.refund.merchant_pub,
                                    &rh->details.refund.sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  /* check that refund fee matches our expectations from /keys! */
  if ( (GNUNET_YES !=
        TALER_amount_cmp_currency (&rh->details.refund.refund_fee,
                                   &pc->dk->fees.refund)) ||
       (0 !=
        TALER_amount_cmp (&rh->details.refund.refund_fee,
                          &pc->dk->fees.refund)) )
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_NO;
}


/**
 * Handle recoup entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_recoup (struct CoinHistoryParseContext *pc,
             struct TALER_EXCHANGE_CoinHistoryEntry *rh,
             const struct TALER_Amount *amount,
             json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
                                 &rh->details.recoup.exchange_sig),
    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
                                 &rh->details.recoup.exchange_pub),
    GNUNET_JSON_spec_fixed_auto ("reserve_pub",
                                 &rh->details.recoup.reserve_pub),
    GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                 &rh->details.recoup.coin_sig),
    GNUNET_JSON_spec_fixed_auto ("coin_blind",
                                 &rh->details.recoup.coin_bks),
    GNUNET_JSON_spec_timestamp ("timestamp",
                                &rh->details.recoup.timestamp),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_EXCHANGE_test_signing_key (pc->keys,
                                       &rh->details.recoup.exchange_pub))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_exchange_online_confirm_recoup_verify (
        rh->details.recoup.timestamp,
        amount,
        pc->coin_pub,
        &rh->details.recoup.reserve_pub,
        &rh->details.recoup.exchange_pub,
        &rh->details.recoup.exchange_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_recoup_verify (&pc->dk->h_key,
                                  &rh->details.recoup.coin_bks,
                                  pc->coin_pub,
                                  &rh->details.recoup.coin_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_YES;
}


/**
 * Handle recoup-refresh entry in the coin's history.
 * This is the coin that was subjected to a recoup,
 * the value being credited to the old coin.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_recoup_refresh (struct CoinHistoryParseContext *pc,
                     struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                     const struct TALER_Amount *amount,
                     json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
                                 &rh->details.recoup_refresh.exchange_sig),
    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
                                 &rh->details.recoup_refresh.exchange_pub),
    GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                 &rh->details.recoup_refresh.coin_sig),
    GNUNET_JSON_spec_fixed_auto ("old_coin_pub",
                                 &rh->details.recoup_refresh.old_coin_pub),
    GNUNET_JSON_spec_fixed_auto ("coin_blind",
                                 &rh->details.recoup_refresh.coin_bks),
    GNUNET_JSON_spec_timestamp ("timestamp",
                                &rh->details.recoup_refresh.timestamp),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_EXCHANGE_test_signing_key (
        pc->keys,
        &rh->details.recoup_refresh.exchange_pub))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_exchange_online_confirm_recoup_refresh_verify (
        rh->details.recoup_refresh.timestamp,
        amount,
        pc->coin_pub,
        &rh->details.recoup_refresh.old_coin_pub,
        &rh->details.recoup_refresh.exchange_pub,
        &rh->details.recoup_refresh.exchange_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_recoup_verify (&pc->dk->h_key,
                                  &rh->details.recoup_refresh.coin_bks,
                                  pc->coin_pub,
                                  &rh->details.recoup_refresh.coin_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_YES;
}


/**
 * Handle old coin recoup entry in the coin's history.
 * This is the coin that was credited in a recoup,
 * the value being credited to this coin.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_old_coin_recoup (struct CoinHistoryParseContext *pc,
                      struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                      const struct TALER_Amount *amount,
                      json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
                                 &rh->details.old_coin_recoup.exchange_sig),
    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
                                 &rh->details.old_coin_recoup.exchange_pub),
    GNUNET_JSON_spec_fixed_auto ("coin_pub",
                                 &rh->details.old_coin_recoup.new_coin_pub),
    GNUNET_JSON_spec_timestamp ("timestamp",
                                &rh->details.old_coin_recoup.timestamp),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_EXCHANGE_test_signing_key (
        pc->keys,
        &rh->details.old_coin_recoup.exchange_pub))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_exchange_online_confirm_recoup_refresh_verify (
        rh->details.old_coin_recoup.timestamp,
        amount,
        &rh->details.old_coin_recoup.new_coin_pub,
        pc->coin_pub,
        &rh->details.old_coin_recoup.exchange_pub,
        &rh->details.old_coin_recoup.exchange_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_NO;
}


/**
 * Handle purse deposit entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_purse_deposit (struct CoinHistoryParseContext *pc,
                    struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                    const struct TALER_Amount *amount,
                    json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_web_url ("exchange_base_url",
                             &rh->details.purse_deposit.exchange_base_url),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
                                   &rh->details.purse_deposit.phac),
      NULL),
    GNUNET_JSON_spec_fixed_auto ("purse_pub",
                                 &rh->details.purse_deposit.purse_pub),
    GNUNET_JSON_spec_bool ("refunded",
                           &rh->details.purse_deposit.refunded),
    GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                 &rh->details.purse_deposit.coin_sig),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_purse_deposit_verify (
        rh->details.purse_deposit.exchange_base_url,
        &rh->details.purse_deposit.purse_pub,
        amount,
        &pc->dk->h_key,
        &rh->details.purse_deposit.phac,
        pc->coin_pub,
        &rh->details.purse_deposit.coin_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (rh->details.purse_deposit.refunded)
  {
    /* We wave the deposit fee. */
    if (0 >
        TALER_amount_add (pc->total_in,
                          pc->total_in,
                          &pc->dk->fees.deposit))
    {
      /* overflow in refund history? inconceivable! Bad exchange! */
      GNUNET_break_op (0);
      return GNUNET_SYSERR;
    }
  }
  return GNUNET_YES;
}


/**
 * Handle purse refund entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_purse_refund (struct CoinHistoryParseContext *pc,
                   struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                   const struct TALER_Amount *amount,
                   json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any ("refund_fee",
                                &rh->details.purse_refund.refund_fee),
    GNUNET_JSON_spec_fixed_auto ("exchange_sig",
                                 &rh->details.purse_refund.exchange_sig),
    GNUNET_JSON_spec_fixed_auto ("exchange_pub",
                                 &rh->details.purse_refund.exchange_pub),
    GNUNET_JSON_spec_fixed_auto ("purse_pub",
                                 &rh->details.purse_refund.purse_pub),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_EXCHANGE_test_signing_key (
        pc->keys,
        &rh->details.purse_refund.exchange_pub))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_exchange_online_purse_refund_verify (
        amount,
        &rh->details.purse_refund.refund_fee,
        pc->coin_pub,
        &rh->details.purse_refund.purse_pub,
        &rh->details.purse_refund.exchange_pub,
        &rh->details.purse_refund.exchange_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if ( (GNUNET_YES !=
        TALER_amount_cmp_currency (&rh->details.purse_refund.refund_fee,
                                   &pc->dk->fees.refund)) ||
       (0 !=
        TALER_amount_cmp (&rh->details.purse_refund.refund_fee,
                          &pc->dk->fees.refund)) )
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_NO;
}


/**
 * Handle reserve deposit entry in the coin's history.
 *
 * @param[in,out] pc overall context
 * @param[out] rh history entry to initialize
 * @param amount main amount of this operation
 * @param transaction JSON details for the operation
 * @return #GNUNET_SYSERR on error,
 *         #GNUNET_OK to add, #GNUNET_NO to subtract
 */
static enum GNUNET_GenericReturnValue
help_reserve_open_deposit (struct CoinHistoryParseContext *pc,
                           struct TALER_EXCHANGE_CoinHistoryEntry *rh,
                           const struct TALER_Amount *amount,
                           json_t *transaction)
{
  struct GNUNET_JSON_Specification spec[] = {
    GNUNET_JSON_spec_fixed_auto (
      "reserve_sig",
      &rh->details.reserve_open_deposit.reserve_sig),
    GNUNET_JSON_spec_fixed_auto (
      "coin_sig",
      &rh->details.reserve_open_deposit.coin_sig),
    TALER_JSON_spec_amount_any (
      "coin_contribution",
      &rh->details.reserve_open_deposit.coin_contribution),
    GNUNET_JSON_spec_mark_optional (
      GNUNET_JSON_spec_fixed_auto (
        "h_age_commitment",
        &rh->details.reserve_open_deposit.h_age_commitment),
      &rh->details.reserve_open_deposit.no_hac),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (transaction,
                         spec,
                         NULL, NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (GNUNET_OK !=
      TALER_wallet_reserve_open_deposit_verify (
        amount,
        &pc->dk->h_key,
        rh->details.reserve_open_deposit.no_hac
        ? NULL
        : &rh->details.reserve_open_deposit.h_age_commitment,
        &rh->details.reserve_open_deposit.reserve_sig,
        pc->coin_pub,
        &rh->details.reserve_open_deposit.coin_sig))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_YES;
}


enum GNUNET_GenericReturnValue
TALER_EXCHANGE_parse_coin_history (
  const struct TALER_EXCHANGE_Keys *keys,
  const struct TALER_EXCHANGE_DenomPublicKey *dk,
  const json_t *history,
  const struct TALER_CoinSpendPublicKeyP *coin_pub,
  struct TALER_Amount *total_in,
  struct TALER_Amount *total_out,
  unsigned int rlen,
  struct TALER_EXCHANGE_CoinHistoryEntry rhistory[static rlen])
{
  const struct
  {
    const char *type;
    CoinCheckHelper helper;
    enum TALER_EXCHANGE_CoinTransactionType ctt;
  } map[] = {
    { "DEPOSIT",
      &help_deposit,
      TALER_EXCHANGE_CTT_DEPOSIT },
    { "MELT",
      &help_melt,
      TALER_EXCHANGE_CTT_MELT },
    { "REFUND",
      &help_refund,
      TALER_EXCHANGE_CTT_REFUND },
    { "RECOUP-WITHDRAW",
      &help_recoup,
      TALER_EXCHANGE_CTT_RECOUP },
    { "RECOUP-REFRESH",
      &help_recoup_refresh,
      TALER_EXCHANGE_CTT_RECOUP_REFRESH },
    { "RECOUP-REFRESH-RECEIVER",
      &help_old_coin_recoup,
      TALER_EXCHANGE_CTT_OLD_COIN_RECOUP },
    { "PURSE-DEPOSIT",
      &help_purse_deposit,
      TALER_EXCHANGE_CTT_PURSE_DEPOSIT },
    { "PURSE-REFUND",
      &help_purse_refund,
      TALER_EXCHANGE_CTT_PURSE_REFUND },
    { "RESERVE-OPEN-DEPOSIT",
      &help_reserve_open_deposit,
      TALER_EXCHANGE_CTT_RESERVE_OPEN_DEPOSIT },
    { NULL, NULL, TALER_EXCHANGE_CTT_NONE }
  };
  struct CoinHistoryParseContext pc = {
    .keys = keys,
    .dk = dk,
    .coin_pub = coin_pub,
    .total_out = total_out,
    .total_in = total_in
  };
  size_t len;

  if (NULL == history)
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  len = json_array_size (history);
  if (0 == len)
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  *total_in = dk->value;
  GNUNET_assert (GNUNET_OK ==
                 TALER_amount_set_zero (total_in->currency,
                                        total_out));
  for (size_t off = 0; off < len; off++)
  {
    struct TALER_EXCHANGE_CoinHistoryEntry *rh = &rhistory[off];
    json_t *transaction = json_array_get (history,
                                          off);
    enum GNUNET_GenericReturnValue add;
    const char *type;
    struct GNUNET_JSON_Specification spec_glob[] = {
      TALER_JSON_spec_amount_any ("amount",
                                  &rh->amount),
      GNUNET_JSON_spec_string ("type",
                               &type),
      GNUNET_JSON_spec_uint64 ("history_offset",
                               &rh->history_offset),
      GNUNET_JSON_spec_end ()
    };

    if (GNUNET_OK !=
        GNUNET_JSON_parse (transaction,
                           spec_glob,
                           NULL, NULL))
    {
      GNUNET_break_op (0);
      return GNUNET_SYSERR;
    }
    if (GNUNET_YES !=
        TALER_amount_cmp_currency (&rh->amount,
                                   total_in))
    {
      GNUNET_break_op (0);
      return GNUNET_SYSERR;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Operation of type %s with amount %s\n",
                type,
                TALER_amount2s (&rh->amount));
    add = GNUNET_SYSERR;
    for (unsigned int i = 0; NULL != map[i].type; i++)
    {
      if (0 == strcasecmp (type,
                           map[i].type))
      {
        rh->type = map[i].ctt;
        add = map[i].helper (&pc,
                             rh,
                             &rh->amount,
                             transaction);
        break;
      }
    }
    switch (add)
    {
    case GNUNET_SYSERR:
      /* entry type not supported, new version on server? */
      rh->type = TALER_EXCHANGE_CTT_NONE;
      GNUNET_break_op (0);
      GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                  "Unexpected type `%s' in response\n",
                  type);
      return GNUNET_SYSERR;
    case GNUNET_YES:
      /* This amount should be debited from the coin */
      if (0 >
          TALER_amount_add (total_out,
                            total_out,
                            &rh->amount))
      {
        /* overflow in history already!? inconceivable! Bad exchange! */
        GNUNET_break_op (0);
        return GNUNET_SYSERR;
      }
      break;
    case GNUNET_NO:
      /* This amount should be credited to the coin. */
      if (0 >
          TALER_amount_add (total_in,
                            total_in,
                            &rh->amount))
      {
        /* overflow in refund history? inconceivable! Bad exchange! */
        GNUNET_break_op (0);
        return GNUNET_SYSERR;
      }
      break;
    } /* end of switch(add) */
  }
  return GNUNET_OK;
}


/**
 * We received an #MHD_HTTP_OK response. Handle the JSON response.
 *
 * @param gcsh handle of the request
 * @param j JSON response
 * @return #GNUNET_OK on success
 */
static enum GNUNET_GenericReturnValue
handle_coins_history_ok (struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh,
                         const json_t *j)
{
  struct TALER_EXCHANGE_GetCoinsHistoryResponse rs = {
    .hr.reply = j,
    .hr.http_status = MHD_HTTP_OK
  };
  struct GNUNET_JSON_Specification spec[] = {
    TALER_JSON_spec_amount_any ("balance",
                                &rs.details.ok.balance),
    GNUNET_JSON_spec_fixed_auto ("h_denom_pub",
                                 &rs.details.ok.h_denom_pub),
    GNUNET_JSON_spec_array_const ("history",
                                  &rs.details.ok.history),
    GNUNET_JSON_spec_end ()
  };

  if (GNUNET_OK !=
      GNUNET_JSON_parse (j,
                         spec,
                         NULL,
                         NULL))
  {
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  if (NULL != gcsh->cb)
  {
    gcsh->cb (gcsh->cb_cls,
              &rs);
    gcsh->cb = NULL;
  }
  GNUNET_JSON_parse_free (spec);
  return GNUNET_OK;
}


/**
 * Function called when we're done processing the
 * HTTP GET /coins/$COIN_PUB/history request.
 *
 * @param cls the `struct TALER_EXCHANGE_GetCoinsHistoryHandle`
 * @param response_code HTTP response code, 0 on error
 * @param response parsed JSON result, NULL on error
 */
static void
handle_coins_history_finished (void *cls,
                               long response_code,
                               const void *response)
{
  struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh = cls;
  const json_t *j = response;
  struct TALER_EXCHANGE_GetCoinsHistoryResponse rs = {
    .hr.reply = j,
    .hr.http_status = (unsigned int) response_code
  };

  gcsh->job = NULL;
  switch (response_code)
  {
  case 0:
    rs.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE;
    break;
  case MHD_HTTP_OK:
    if (GNUNET_OK !=
        handle_coins_history_ok (gcsh,
                                 j))
    {
      rs.hr.http_status = 0;
      rs.hr.ec = TALER_EC_GENERIC_REPLY_MALFORMED;
    }
    break;
  case MHD_HTTP_NO_CONTENT:
    break;
  case MHD_HTTP_NOT_MODIFIED:
    break;
  case MHD_HTTP_BAD_REQUEST:
    /* This should never happen, either us or the exchange is buggy
       (or API version conflict); just pass JSON reply to the application */
    GNUNET_break (0);
    rs.hr.ec = TALER_JSON_get_error_code (j);
    rs.hr.hint = TALER_JSON_get_error_hint (j);
    break;
  case MHD_HTTP_FORBIDDEN:
    /* This should never happen, either us or the exchange is buggy
       (or API version conflict); just pass JSON reply to the application */
    GNUNET_break (0);
    rs.hr.ec = TALER_JSON_get_error_code (j);
    rs.hr.hint = TALER_JSON_get_error_hint (j);
    break;
  case MHD_HTTP_NOT_FOUND:
    rs.hr.ec = TALER_JSON_get_error_code (j);
    rs.hr.hint = TALER_JSON_get_error_hint (j);
    break;
  case MHD_HTTP_INTERNAL_SERVER_ERROR:
    /* Server had an internal issue; we should retry, but this API
       leaves this to the application */
    rs.hr.ec = TALER_JSON_get_error_code (j);
    rs.hr.hint = TALER_JSON_get_error_hint (j);
    break;
  default:
    /* unexpected response code */
    GNUNET_break_op (0);
    rs.hr.ec = TALER_JSON_get_error_code (j);
    rs.hr.hint = TALER_JSON_get_error_hint (j);
    GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
                "Unexpected response code %u/%d for coins history\n",
                (unsigned int) response_code,
                (int) rs.hr.ec);
    break;
  }
  if (NULL != gcsh->cb)
  {
    gcsh->cb (gcsh->cb_cls,
              &rs);
    gcsh->cb = NULL;
  }
  TALER_EXCHANGE_get_coins_history_cancel (gcsh);
}


struct TALER_EXCHANGE_GetCoinsHistoryHandle *
TALER_EXCHANGE_get_coins_history_create (
  struct GNUNET_CURL_Context *ctx,
  const char *url,
  const struct TALER_CoinSpendPrivateKeyP *coin_priv)
{
  struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh;

  gcsh = GNUNET_new (struct TALER_EXCHANGE_GetCoinsHistoryHandle);
  gcsh->ctx = ctx;
  gcsh->base_url = GNUNET_strdup (url);
  gcsh->coin_priv = *coin_priv;
  GNUNET_CRYPTO_eddsa_key_get_public (&coin_priv->eddsa_priv,
                                      &gcsh->coin_pub.eddsa_pub);
  gcsh->options.start_off = 0;
  return gcsh;
}


enum GNUNET_GenericReturnValue
TALER_EXCHANGE_get_coins_history_set_options_ (
  struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh,
  unsigned int num_options,
  const struct TALER_EXCHANGE_GetCoinsHistoryOptionValue *options)
{
  for (unsigned int i = 0; i < num_options; i++)
  {
    const struct TALER_EXCHANGE_GetCoinsHistoryOptionValue *opt = &options[i];

    switch (opt->option)
    {
    case TALER_EXCHANGE_GET_COINS_HISTORY_OPTION_END:
      return GNUNET_OK;
    case TALER_EXCHANGE_GET_COINS_HISTORY_OPTION_START_OFF:
      gcsh->options.start_off = opt->details.start_off;
      break;
    }
  }
  return GNUNET_OK;
}


enum TALER_ErrorCode
TALER_EXCHANGE_get_coins_history_start (
  struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh,
  TALER_EXCHANGE_GetCoinsHistoryCallback cb,
  TALER_EXCHANGE_GET_COINS_HISTORY_RESULT_CLOSURE *cb_cls)
{
  char arg_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2 + 64];
  struct curl_slist *job_headers;
  CURL *eh;

  if (NULL != gcsh->job)
  {
    GNUNET_break (0);
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  }
  gcsh->cb = cb;
  gcsh->cb_cls = cb_cls;
  {
    char pub_str[sizeof (struct TALER_CoinSpendPublicKeyP) * 2];
    char *end;

    end = GNUNET_STRINGS_data_to_string (
      &gcsh->coin_pub,
      sizeof (gcsh->coin_pub),
      pub_str,
      sizeof (pub_str));
    *end = '\0';
    if (0 != gcsh->options.start_off)
      GNUNET_snprintf (arg_str,
                       sizeof (arg_str),
                       "coins/%s/history?start=%llu",
                       pub_str,
                       (unsigned long long) gcsh->options.start_off);
    else
      GNUNET_snprintf (arg_str,
                       sizeof (arg_str),
                       "coins/%s/history",
                       pub_str);
  }
  gcsh->url = TALER_url_join (gcsh->base_url,
                              arg_str,
                              NULL);
  if (NULL == gcsh->url)
    return TALER_EC_GENERIC_CONFIGURATION_INVALID;
  eh = TALER_EXCHANGE_curl_easy_get_ (gcsh->url);
  if (NULL == eh)
    return TALER_EC_GENERIC_CONFIGURATION_INVALID;
  {
    struct TALER_CoinSpendSignatureP coin_sig;
    char *sig_hdr;
    char *hdr;

    TALER_wallet_coin_history_sign (gcsh->options.start_off,
                                    &gcsh->coin_priv,
                                    &coin_sig);
    sig_hdr = GNUNET_STRINGS_data_to_string_alloc (
      &coin_sig,
      sizeof (coin_sig));
    GNUNET_asprintf (&hdr,
                     "%s: %s",
                     TALER_COIN_HISTORY_SIGNATURE_HEADER,
                     sig_hdr);
    GNUNET_free (sig_hdr);
    job_headers = curl_slist_append (NULL,
                                     hdr);
    GNUNET_free (hdr);
    if (NULL == job_headers)
    {
      GNUNET_break (0);
      curl_easy_cleanup (eh);
      GNUNET_free (gcsh->url);
      return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
    }
  }
  gcsh->job = GNUNET_CURL_job_add2 (gcsh->ctx,
                                    eh,
                                    job_headers,
                                    &handle_coins_history_finished,
                                    gcsh);
  curl_slist_free_all (job_headers);
  if (NULL == gcsh->job)
    return TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
  return TALER_EC_NONE;
}


void
TALER_EXCHANGE_get_coins_history_cancel (
  struct TALER_EXCHANGE_GetCoinsHistoryHandle *gcsh)
{
  if (NULL != gcsh->job)
  {
    GNUNET_CURL_job_cancel (gcsh->job);
    gcsh->job = NULL;
  }
  GNUNET_free (gcsh->url);
  GNUNET_free (gcsh->base_url);
  GNUNET_free (gcsh);
}


enum GNUNET_GenericReturnValue
TALER_EXCHANGE_check_coin_signature_conflict (
  const json_t *history,
  const struct TALER_CoinSpendSignatureP *coin_sig)
{
  size_t off;
  json_t *entry;

  json_array_foreach (history, off, entry)
  {
    struct TALER_CoinSpendSignatureP cs;
    struct GNUNET_JSON_Specification spec[] = {
      GNUNET_JSON_spec_fixed_auto ("coin_sig",
                                   &cs),
      GNUNET_JSON_spec_end ()
    };

    if (GNUNET_OK !=
        GNUNET_JSON_parse (entry,
                           spec,
                           NULL, NULL))
      continue; /* entry without coin signature */
    if (0 ==
        GNUNET_memcmp (&cs,
                       coin_sig))
    {
      GNUNET_break_op (0);
      return GNUNET_SYSERR;
    }
  }
  return GNUNET_OK;
}


#if FIXME_IMPLEMENT /* #9422 */
/**
 * FIXME-Oec-#9422: we need some specific routines that show
 * that certain coin operations are indeed in conflict,
 * for example that the coin is of a different denomination
 * or different age restrictions.
 * This relates to unimplemented error handling for
 * coins in the exchange!
 *
 * Check that the provided @a proof indeeds indicates
 * a conflict for @a coin_pub.
 *
 * @param keys exchange keys
 * @param proof provided conflict proof
 * @param dk denomination of @a coin_pub that the client
 *           used
 * @param coin_pub public key of the coin
 * @param required balance required on the coin for the operation
 * @return #GNUNET_OK if @a proof holds
 */
// FIXME-#9422: should be properly defined and implemented!
enum GNUNET_GenericReturnValue
TALER_EXCHANGE_check_coin_conflict_ (
  const struct TALER_EXCHANGE_Keys *keys,
  const json_t *proof,
  const struct TALER_EXCHANGE_DenomPublicKey *dk,
  const struct TALER_CoinSpendPublicKeyP *coin_pub,
  const struct TALER_Amount *required)
{
  enum TALER_ErrorCode ec;

  ec = TALER_JSON_get_error_code (proof);
  switch (ec)
  {
  case TALER_EC_EXCHANGE_GENERIC_INSUFFICIENT_FUNDS:
    /* Nothing to check anymore here, proof needs to be
       checked in the GET /coins/$COIN_PUB handler */
    break;
  case TALER_EC_EXCHANGE_GENERIC_COIN_CONFLICTING_DENOMINATION_KEY:
    // FIXME-#9422: write check!
    break;
  default:
    GNUNET_break_op (0);
    return GNUNET_SYSERR;
  }
  return GNUNET_OK;
}


#endif


/* end of exchange_api_get-coins-COIN_PUB-history.c */
