exchange

Base system with REST service to issue digital coins, run by the payment service provider
Log | Files | Refs | Submodules | README | LICENSE

get_reserve_history.c (31061B)


      1 /*
      2    This file is part of TALER
      3    Copyright (C) 2022-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 <http://www.gnu.org/licenses/>
     15  */
     16 /**
     17  * @file get_reserve_history.c
     18  * @brief Obtain (parts of) the history of a reserve.
     19  * @author Christian Grothoff
     20  */
     21 #include "taler/taler_error_codes.h"
     22 #include "taler/taler_pq_lib.h"
     23 #include "exchange-database/get_reserve_history.h"
     24 #include "exchange-database/start_read_committed.h"
     25 #include "exchange-database/commit.h"
     26 #include "exchange-database/rollback.h"
     27 #include "helper.h"
     28 
     29 /**
     30  * How often do we re-try when encountering DB serialization issues?
     31  * (We are read-only, so can only happen due to concurrent insert,
     32  * which should be very rare.)
     33  */
     34 #define RETRIES 3
     35 
     36 
     37 /**
     38  * Closure for callbacks invoked via #TALER_EXCHANGEDB_get_reserve_history().
     39  */
     40 struct ReserveHistoryContext
     41 {
     42 
     43   /**
     44    * Which reserve are we building the history for?
     45    */
     46   const struct TALER_ReservePublicKeyP *reserve_pub;
     47 
     48   /**
     49    * Where we build the history.
     50    */
     51   struct TALER_EXCHANGEDB_ReserveHistory *rh;
     52 
     53   /**
     54    * Tail of @e rh list.
     55    */
     56   struct TALER_EXCHANGEDB_ReserveHistory *rh_tail;
     57 
     58   /**
     59    * Plugin context.
     60    */
     61   struct TALER_EXCHANGEDB_PostgresContext *pg;
     62 
     63   /**
     64    * Current reserve_history_serial_id being processed,
     65    * set before each sub-table callback.
     66    */
     67   uint64_t current_history_offset;
     68 
     69   /**
     70    * Set to true on serious internal errors during
     71    * the callbacks.
     72    */
     73   bool failed;
     74 };
     75 
     76 
     77 /**
     78  * Append and return a fresh element to the reserve
     79  * history kept in @a rhc.
     80  *
     81  * @param rhc where the history is kept
     82  * @return the fresh element that was added
     83  */
     84 static struct TALER_EXCHANGEDB_ReserveHistory *
     85 append_rh (struct ReserveHistoryContext *rhc)
     86 {
     87   struct TALER_EXCHANGEDB_ReserveHistory *tail;
     88 
     89   tail = GNUNET_new (struct TALER_EXCHANGEDB_ReserveHistory);
     90   tail->history_offset = rhc->current_history_offset;
     91   if (NULL != rhc->rh_tail)
     92   {
     93     rhc->rh_tail->next = tail;
     94     rhc->rh_tail = tail;
     95   }
     96   else
     97   {
     98     rhc->rh_tail = tail;
     99     rhc->rh = tail;
    100   }
    101   return tail;
    102 }
    103 
    104 
    105 /**
    106  * Add bank transfers to result set for #TALER_EXCHANGEDB_get_reserve_history.
    107  *
    108  * @param cls a `struct ReserveHistoryContext *`
    109  * @param result SQL result
    110  * @param num_results number of rows in @a result
    111  */
    112 static void
    113 add_bank_to_exchange (void *cls,
    114                       PGresult *result,
    115                       unsigned int num_results)
    116 {
    117   struct ReserveHistoryContext *rhc = cls;
    118   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    119 
    120   while (0 < num_results)
    121   {
    122     struct TALER_EXCHANGEDB_BankTransfer *bt;
    123     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    124 
    125     bt = GNUNET_new (struct TALER_EXCHANGEDB_BankTransfer);
    126     {
    127       struct GNUNET_PQ_ResultSpec rs[] = {
    128         GNUNET_PQ_result_spec_uint64 ("wire_reference",
    129                                       &bt->wire_reference),
    130         TALER_PQ_RESULT_SPEC_AMOUNT ("credit",
    131                                      &bt->amount),
    132         GNUNET_PQ_result_spec_timestamp ("execution_date",
    133                                          &bt->execution_date),
    134         GNUNET_PQ_result_spec_string ("sender_account_details",
    135                                       &bt->sender_account_details.full_payto),
    136         GNUNET_PQ_result_spec_end
    137       };
    138 
    139       if (GNUNET_OK !=
    140           GNUNET_PQ_extract_result (result,
    141                                     rs,
    142                                     --num_results))
    143       {
    144         GNUNET_break (0);
    145         GNUNET_free (bt);
    146         rhc->failed = true;
    147         return;
    148       }
    149     }
    150     bt->reserve_pub = *rhc->reserve_pub;
    151     tail = append_rh (rhc);
    152     tail->type = TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE;
    153     tail->details.bank = bt;
    154   } /* end of 'while (0 < rows)' */
    155 }
    156 
    157 
    158 /**
    159  * Add coin withdrawals to result set for #TALER_EXCHANGEDB_get_reserve_history.
    160  *
    161  * @param cls a `struct ReserveHistoryContext *`
    162  * @param result SQL result
    163  * @param num_results number of rows in @a result
    164  */
    165 static void
    166 add_withdraw (void *cls,
    167               PGresult *result,
    168               unsigned int num_results)
    169 {
    170   struct ReserveHistoryContext *rhc = cls;
    171   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    172 
    173   while (0 < num_results)
    174   {
    175     struct TALER_EXCHANGEDB_Withdraw *wd;
    176     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    177 
    178     wd = GNUNET_new (struct TALER_EXCHANGEDB_Withdraw);
    179     {
    180       bool no_noreveal_index;
    181       bool no_max_age;
    182       bool no_selected_h;
    183       size_t num_denom_hs;
    184       size_t num_denom_serials;
    185       uint64_t *my_denom_serials = NULL;
    186       struct TALER_DenominationHashP *my_denom_pub_hashes = NULL;
    187       struct GNUNET_PQ_ResultSpec rs[] = {
    188         GNUNET_PQ_result_spec_auto_from_type  ("planchets_h",
    189                                                &wd->planchets_h),
    190         GNUNET_PQ_result_spec_auto_from_type ("reserve_sig",
    191                                               &wd->reserve_sig),
    192         TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee",
    193                                      &wd->amount_with_fee),
    194         GNUNET_PQ_result_spec_allow_null (
    195           GNUNET_PQ_result_spec_uint16 ("max_age",
    196                                         &wd->max_age),
    197           &no_max_age),
    198         GNUNET_PQ_result_spec_allow_null (
    199           GNUNET_PQ_result_spec_uint16 ("noreveal_index",
    200                                         &wd->noreveal_index),
    201           &no_noreveal_index),
    202         GNUNET_PQ_result_spec_allow_null (
    203           GNUNET_PQ_result_spec_auto_from_type ("blinding_seed",
    204                                                 &wd->blinding_seed),
    205           &wd->no_blinding_seed),
    206         GNUNET_PQ_result_spec_allow_null (
    207           GNUNET_PQ_result_spec_auto_from_type ("selected_h",
    208                                                 &wd->selected_h),
    209           &no_selected_h),
    210         TALER_PQ_result_spec_array_denom_hash (pg->conn,
    211                                                "denom_pub_hashes",
    212                                                &num_denom_hs,
    213                                                &my_denom_pub_hashes),
    214         GNUNET_PQ_result_spec_array_uint64 (pg->conn,
    215                                             "denom_serials",
    216                                             &num_denom_serials,
    217                                             &my_denom_serials),
    218         GNUNET_PQ_result_spec_end
    219       };
    220 
    221       if (GNUNET_OK !=
    222           GNUNET_PQ_extract_result (result,
    223                                     rs,
    224                                     --num_results))
    225       {
    226         GNUNET_break (0);
    227         GNUNET_free (wd);
    228         rhc->failed = true;
    229         GNUNET_PQ_cleanup_result (rs);
    230         return;
    231       }
    232 
    233       if (num_denom_hs != num_denom_serials)
    234       {
    235         GNUNET_break (0);
    236         GNUNET_free (wd);
    237         rhc->failed = true;
    238         GNUNET_PQ_cleanup_result (rs);
    239         return;
    240       }
    241 
    242       if ((no_noreveal_index != no_max_age) ||
    243           (no_noreveal_index != no_selected_h))
    244       {
    245         GNUNET_break (0);
    246         GNUNET_free (wd);
    247         rhc->failed = true;
    248         GNUNET_PQ_cleanup_result (rs);
    249         return;
    250       }
    251       wd->age_proof_required = ! no_max_age;
    252       wd->num_coins = num_denom_serials;
    253       wd->reserve_pub = *rhc->reserve_pub;
    254       wd->denom_serials = my_denom_serials;
    255       wd->denom_pub_hashes = my_denom_pub_hashes;
    256       /* prevent cleanup from destroying our actual result */
    257       my_denom_serials = NULL;
    258       my_denom_pub_hashes = NULL;
    259       GNUNET_PQ_cleanup_result (rs);
    260     }
    261 
    262     tail = append_rh (rhc);
    263     tail->type = TALER_EXCHANGEDB_RO_WITHDRAW_COINS;
    264     tail->details.withdraw = wd;
    265   }
    266 }
    267 
    268 
    269 /**
    270  * Add recoups to result set for #TALER_EXCHANGEDB_get_reserve_history.
    271  *
    272  * @param cls a `struct ReserveHistoryContext *`
    273  * @param result SQL result
    274  * @param num_results number of rows in @a result
    275  */
    276 static void
    277 add_recoup (void *cls,
    278             PGresult *result,
    279             unsigned int num_results)
    280 {
    281   struct ReserveHistoryContext *rhc = cls;
    282   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    283 
    284   while (0 < num_results)
    285   {
    286     struct TALER_EXCHANGEDB_Recoup *recoup;
    287     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    288 
    289     recoup = GNUNET_new (struct TALER_EXCHANGEDB_Recoup);
    290     {
    291       struct GNUNET_PQ_ResultSpec rs[] = {
    292         TALER_PQ_RESULT_SPEC_AMOUNT ("amount",
    293                                      &recoup->value),
    294         GNUNET_PQ_result_spec_auto_from_type ("coin_pub",
    295                                               &recoup->coin.coin_pub),
    296         GNUNET_PQ_result_spec_auto_from_type ("coin_blind",
    297                                               &recoup->coin_blind),
    298         GNUNET_PQ_result_spec_auto_from_type ("coin_sig",
    299                                               &recoup->coin_sig),
    300         GNUNET_PQ_result_spec_timestamp ("recoup_timestamp",
    301                                          &recoup->timestamp),
    302         GNUNET_PQ_result_spec_auto_from_type ("planchets_h",
    303                                               &recoup->planchets_h),
    304         GNUNET_PQ_result_spec_auto_from_type ("denom_pub_hash",
    305                                               &recoup->coin.denom_pub_hash),
    306         TALER_PQ_result_spec_denom_sig (
    307           "denom_sig",
    308           &recoup->coin.denom_sig),
    309         GNUNET_PQ_result_spec_end
    310       };
    311 
    312       if (GNUNET_OK !=
    313           GNUNET_PQ_extract_result (result,
    314                                     rs,
    315                                     --num_results))
    316       {
    317         GNUNET_break (0);
    318         GNUNET_free (recoup);
    319         rhc->failed = true;
    320         return;
    321       }
    322     }
    323     recoup->reserve_pub = *rhc->reserve_pub;
    324     tail = append_rh (rhc);
    325     tail->type = TALER_EXCHANGEDB_RO_RECOUP_COIN;
    326     tail->details.recoup = recoup;
    327   } /* end of 'while (0 < rows)' */
    328 }
    329 
    330 
    331 /**
    332  * Add exchange-to-bank transfers to result set for
    333  * #TALER_EXCHANGEDB_get_reserve_history.
    334  *
    335  * @param cls a `struct ReserveHistoryContext *`
    336  * @param result SQL result
    337  * @param num_results number of rows in @a result
    338  */
    339 static void
    340 add_exchange_to_bank (void *cls,
    341                       PGresult *result,
    342                       unsigned int num_results)
    343 {
    344   struct ReserveHistoryContext *rhc = cls;
    345   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    346 
    347   while (0 < num_results)
    348   {
    349     struct TALER_EXCHANGEDB_ClosingTransfer *closing;
    350     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    351 
    352     closing = GNUNET_new (struct TALER_EXCHANGEDB_ClosingTransfer);
    353     {
    354       struct GNUNET_PQ_ResultSpec rs[] = {
    355         TALER_PQ_RESULT_SPEC_AMOUNT ("amount",
    356                                      &closing->amount),
    357         TALER_PQ_RESULT_SPEC_AMOUNT ("closing_fee",
    358                                      &closing->closing_fee),
    359         GNUNET_PQ_result_spec_timestamp ("execution_date",
    360                                          &closing->execution_date),
    361         GNUNET_PQ_result_spec_string ("receiver_account",
    362                                       &closing->receiver_account_details.
    363                                       full_payto),
    364         GNUNET_PQ_result_spec_auto_from_type ("wtid",
    365                                               &closing->wtid),
    366         GNUNET_PQ_result_spec_end
    367       };
    368 
    369       if (GNUNET_OK !=
    370           GNUNET_PQ_extract_result (result,
    371                                     rs,
    372                                     --num_results))
    373       {
    374         GNUNET_break (0);
    375         GNUNET_free (closing);
    376         rhc->failed = true;
    377         return;
    378       }
    379     }
    380     closing->reserve_pub = *rhc->reserve_pub;
    381     tail = append_rh (rhc);
    382     tail->type = TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK;
    383     tail->details.closing = closing;
    384   } /* end of 'while (0 < rows)' */
    385 }
    386 
    387 
    388 /**
    389  * Add purse merge transfers to result set for
    390  * #TALER_EXCHANGEDB_get_reserve_history.
    391  *
    392  * @param cls a `struct ReserveHistoryContext *`
    393  * @param result SQL result
    394  * @param num_results number of rows in @a result
    395  */
    396 static void
    397 add_p2p_merge (void *cls,
    398                PGresult *result,
    399                unsigned int num_results)
    400 {
    401   struct ReserveHistoryContext *rhc = cls;
    402   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    403 
    404   while (0 < num_results)
    405   {
    406     struct TALER_EXCHANGEDB_PurseMerge *merge;
    407     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    408 
    409     merge = GNUNET_new (struct TALER_EXCHANGEDB_PurseMerge);
    410     {
    411       uint32_t flags32;
    412       struct TALER_Amount balance;
    413       struct GNUNET_PQ_ResultSpec rs[] = {
    414         TALER_PQ_RESULT_SPEC_AMOUNT ("purse_fee",
    415                                      &merge->purse_fee),
    416         TALER_PQ_RESULT_SPEC_AMOUNT ("balance",
    417                                      &balance),
    418         TALER_PQ_RESULT_SPEC_AMOUNT ("amount_with_fee",
    419                                      &merge->amount_with_fee),
    420         GNUNET_PQ_result_spec_timestamp ("merge_timestamp",
    421                                          &merge->merge_timestamp),
    422         GNUNET_PQ_result_spec_timestamp ("purse_expiration",
    423                                          &merge->purse_expiration),
    424         GNUNET_PQ_result_spec_uint32 ("age_limit",
    425                                       &merge->min_age),
    426         GNUNET_PQ_result_spec_uint32 ("flags",
    427                                       &flags32),
    428         GNUNET_PQ_result_spec_auto_from_type ("h_contract_terms",
    429                                               &merge->h_contract_terms),
    430         GNUNET_PQ_result_spec_auto_from_type ("merge_pub",
    431                                               &merge->merge_pub),
    432         GNUNET_PQ_result_spec_auto_from_type ("purse_pub",
    433                                               &merge->purse_pub),
    434         GNUNET_PQ_result_spec_auto_from_type ("reserve_sig",
    435                                               &merge->reserve_sig),
    436         GNUNET_PQ_result_spec_end
    437       };
    438 
    439       if (GNUNET_OK !=
    440           GNUNET_PQ_extract_result (result,
    441                                     rs,
    442                                     --num_results))
    443       {
    444         GNUNET_break (0);
    445         GNUNET_free (merge);
    446         rhc->failed = true;
    447         return;
    448       }
    449       merge->flags = (enum TALER_WalletAccountMergeFlags) flags32;
    450       if ( (! GNUNET_TIME_absolute_is_future (
    451               merge->merge_timestamp.abs_time)) &&
    452            (-1 != TALER_amount_cmp (&balance,
    453                                     &merge->amount_with_fee)) )
    454         merge->merged = true;
    455     }
    456     merge->reserve_pub = *rhc->reserve_pub;
    457     tail = append_rh (rhc);
    458     tail->type = TALER_EXCHANGEDB_RO_PURSE_MERGE;
    459     tail->details.merge = merge;
    460   }
    461 }
    462 
    463 
    464 /**
    465  * Add paid for history requests to result set for
    466  * #TALER_EXCHANGEDB_get_reserve_history.
    467  *
    468  * @param cls a `struct ReserveHistoryContext *`
    469  * @param result SQL result
    470  * @param num_results number of rows in @a result
    471  */
    472 static void
    473 add_open_requests (void *cls,
    474                    PGresult *result,
    475                    unsigned int num_results)
    476 {
    477   struct ReserveHistoryContext *rhc = cls;
    478   struct TALER_EXCHANGEDB_PostgresContext *pg = rhc->pg;
    479 
    480   while (0 < num_results)
    481   {
    482     struct TALER_EXCHANGEDB_OpenRequest *orq;
    483     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    484 
    485     orq = GNUNET_new (struct TALER_EXCHANGEDB_OpenRequest);
    486     {
    487       struct GNUNET_PQ_ResultSpec rs[] = {
    488         TALER_PQ_RESULT_SPEC_AMOUNT ("open_fee",
    489                                      &orq->open_fee),
    490         GNUNET_PQ_result_spec_timestamp ("request_timestamp",
    491                                          &orq->request_timestamp),
    492         GNUNET_PQ_result_spec_timestamp ("expiration_date",
    493                                          &orq->reserve_expiration),
    494         GNUNET_PQ_result_spec_uint32 ("requested_purse_limit",
    495                                       &orq->purse_limit),
    496         GNUNET_PQ_result_spec_auto_from_type ("reserve_sig",
    497                                               &orq->reserve_sig),
    498         GNUNET_PQ_result_spec_end
    499       };
    500 
    501       if (GNUNET_OK !=
    502           GNUNET_PQ_extract_result (result,
    503                                     rs,
    504                                     --num_results))
    505       {
    506         GNUNET_break (0);
    507         GNUNET_free (orq);
    508         rhc->failed = true;
    509         return;
    510       }
    511     }
    512     orq->reserve_pub = *rhc->reserve_pub;
    513     tail = append_rh (rhc);
    514     tail->type = TALER_EXCHANGEDB_RO_OPEN_REQUEST;
    515     tail->details.open_request = orq;
    516   }
    517 }
    518 
    519 
    520 /**
    521  * Add paid for history requests to result set for
    522  * #TALER_EXCHANGEDB_get_reserve_history.
    523  *
    524  * @param cls a `struct ReserveHistoryContext *`
    525  * @param result SQL result
    526  * @param num_results number of rows in @a result
    527  */
    528 static void
    529 add_close_requests (void *cls,
    530                     PGresult *result,
    531                     unsigned int num_results)
    532 {
    533   struct ReserveHistoryContext *rhc = cls;
    534 
    535   while (0 < num_results)
    536   {
    537     struct TALER_EXCHANGEDB_CloseRequest *crq;
    538     struct TALER_EXCHANGEDB_ReserveHistory *tail;
    539 
    540     crq = GNUNET_new (struct TALER_EXCHANGEDB_CloseRequest);
    541     {
    542       struct TALER_FullPayto payto_uri;
    543       struct GNUNET_PQ_ResultSpec rs[] = {
    544         GNUNET_PQ_result_spec_timestamp ("close_timestamp",
    545                                          &crq->request_timestamp),
    546         GNUNET_PQ_result_spec_string ("payto_uri",
    547                                       &payto_uri.full_payto),
    548         GNUNET_PQ_result_spec_auto_from_type ("reserve_sig",
    549                                               &crq->reserve_sig),
    550         GNUNET_PQ_result_spec_end
    551       };
    552 
    553       if (GNUNET_OK !=
    554           GNUNET_PQ_extract_result (result,
    555                                     rs,
    556                                     --num_results))
    557       {
    558         GNUNET_break (0);
    559         GNUNET_free (crq);
    560         rhc->failed = true;
    561         return;
    562       }
    563       TALER_full_payto_hash (payto_uri,
    564                              &crq->target_account_h_payto);
    565       GNUNET_free (payto_uri.full_payto);
    566     }
    567     crq->reserve_pub = *rhc->reserve_pub;
    568     tail = append_rh (rhc);
    569     tail->type = TALER_EXCHANGEDB_RO_CLOSE_REQUEST;
    570     tail->details.close_request = crq;
    571   }
    572 }
    573 
    574 
    575 /**
    576  * Add reserve history entries found.
    577  *
    578  * @param cls a `struct ReserveHistoryContext *`
    579  * @param result SQL result
    580  * @param num_results number of rows in @a result
    581  */
    582 static void
    583 handle_history_entry (void *cls,
    584                       PGresult *result,
    585                       unsigned int num_results)
    586 {
    587   static const struct
    588   {
    589     /**
    590      * Table with reserve history entry we are responsible for.
    591      */
    592     const char *table;
    593     /**
    594      * Name of the prepared statement to run.
    595      */
    596     const char *statement;
    597     /**
    598      * Function to use to process the results.
    599      */
    600     GNUNET_PQ_PostgresResultHandler cb;
    601   } work[] = {
    602     /** #TALER_EXCHANGEDB_RO_BANK_TO_EXCHANGE */
    603     { "reserves_in",
    604       "get_reserve_history_reserves_in_get_transactions",
    605       add_bank_to_exchange },
    606     /** #TALER_EXCHANGEDB_RO_WITHDRAW_COINS */
    607     { "withdraw",
    608       "get_reserve_history_withdraw_details",
    609       &add_withdraw },
    610     /** #TALER_EXCHANGEDB_RO_RECOUP_COIN */
    611     { "recoup",
    612       "get_reserve_history_recoup_by_reserve",
    613       &add_recoup },
    614     /** #TALER_EXCHANGEDB_RO_EXCHANGE_TO_BANK */
    615     { "reserves_close",
    616       "get_reserve_history_close_by_reserve",
    617       &add_exchange_to_bank },
    618     /** #TALER_EXCHANGEDB_RO_PURSE_MERGE */
    619     { "purse_decision",
    620       "get_reserve_history_merge_by_reserve",
    621       &add_p2p_merge },
    622     /** #TALER_EXCHANGEDB_RO_OPEN_REQUEST */
    623     { "reserves_open_requests",
    624       "get_reserve_history_open_request_by_reserve",
    625       &add_open_requests },
    626     /** #TALER_EXCHANGEDB_RO_CLOSE_REQUEST */
    627     { "close_requests",
    628       "get_reserve_history_close_request_by_reserve",
    629       &add_close_requests },
    630     /* List terminator */
    631     { NULL, NULL, NULL }
    632   };
    633   struct ReserveHistoryContext *rhc = cls;
    634   char *table_name;
    635   uint64_t serial_id;
    636   struct GNUNET_PQ_ResultSpec rs[] = {
    637     GNUNET_PQ_result_spec_string ("table_name",
    638                                   &table_name),
    639     GNUNET_PQ_result_spec_uint64 ("serial_id",
    640                                   &serial_id),
    641     GNUNET_PQ_result_spec_uint64 ("reserve_history_serial_id",
    642                                   &rhc->current_history_offset),
    643     GNUNET_PQ_result_spec_end
    644   };
    645   struct GNUNET_PQ_QueryParam params[] = {
    646     GNUNET_PQ_query_param_auto_from_type (rhc->reserve_pub),
    647     GNUNET_PQ_query_param_uint64 (&serial_id),
    648     GNUNET_PQ_query_param_end
    649   };
    650 
    651   while (0 < num_results--)
    652   {
    653     enum GNUNET_DB_QueryStatus qs;
    654     bool found = false;
    655 
    656     if (GNUNET_OK !=
    657         GNUNET_PQ_extract_result (result,
    658                                   rs,
    659                                   num_results))
    660     {
    661       GNUNET_break (0);
    662       rhc->failed = true;
    663       return;
    664     }
    665 
    666     for (unsigned int i = 0;
    667          NULL != work[i].cb;
    668          i++)
    669     {
    670       if (0 != strcmp (table_name,
    671                        work[i].table))
    672         continue;
    673       found = true;
    674       qs = GNUNET_PQ_eval_prepared_multi_select (rhc->pg->conn,
    675                                                  work[i].statement,
    676                                                  params,
    677                                                  work[i].cb,
    678                                                  rhc);
    679       GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    680                   "Reserve %s had %d transactions at %llu in table %s\n",
    681                   TALER_B2S (rhc->reserve_pub),
    682                   (int) qs,
    683                   (unsigned long long) serial_id,
    684                   table_name);
    685       if (0 >= qs)
    686         rhc->failed = true;
    687       break;
    688     }
    689     if (! found)
    690     {
    691       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
    692                   "Reserve history includes unsupported table `%s`\n",
    693                   table_name);
    694       rhc->failed = true;
    695     }
    696     GNUNET_PQ_cleanup_result (rs);
    697     if (rhc->failed)
    698       break;
    699   }
    700 }
    701 
    702 
    703 enum GNUNET_DB_QueryStatus
    704 TALER_EXCHANGEDB_get_reserve_history (
    705   struct TALER_EXCHANGEDB_PostgresContext *pg,
    706   const struct TALER_ReservePublicKeyP *reserve_pub,
    707   uint64_t start_off,
    708   uint64_t etag_in,
    709   uint64_t *etag_out,
    710   struct TALER_Amount *balance,
    711   struct TALER_EXCHANGEDB_ReserveHistory **rhp)
    712 {
    713   struct ReserveHistoryContext rhc = {
    714     .pg = pg,
    715     .reserve_pub = reserve_pub
    716   };
    717   struct GNUNET_PQ_QueryParam params[] = {
    718     GNUNET_PQ_query_param_auto_from_type (reserve_pub),
    719     GNUNET_PQ_query_param_end
    720   };
    721   struct GNUNET_PQ_QueryParam lparams[] = {
    722     GNUNET_PQ_query_param_auto_from_type (reserve_pub),
    723     GNUNET_PQ_query_param_uint64 (&start_off),
    724     GNUNET_PQ_query_param_end
    725   };
    726 
    727   *rhp = NULL;
    728   GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
    729               "Getting transactions for reserve %s\n",
    730               TALER_B2S (reserve_pub));
    731   PREPARE (pg,
    732            "get_reserve_history_etag",
    733            "SELECT"
    734            " his.reserve_history_serial_id"
    735            ",r.current_balance"
    736            " FROM reserve_history his"
    737            " JOIN reserves r USING (reserve_pub)"
    738            " WHERE his.reserve_pub=$1"
    739            " ORDER BY reserve_history_serial_id DESC"
    740            " LIMIT 1;");
    741   PREPARE (pg,
    742            "get_reserve_history",
    743            "SELECT"
    744            " table_name"
    745            ",serial_id"
    746            ",reserve_history_serial_id"
    747            " FROM reserve_history"
    748            " WHERE reserve_pub=$1"
    749            "   AND reserve_history_serial_id > $2"
    750            " ORDER BY reserve_history_serial_id DESC;");
    751   PREPARE (pg,
    752            "get_reserve_history_reserves_in_get_transactions",
    753            "SELECT"
    754            " ri.wire_reference"
    755            ",ri.credit"
    756            ",ri.execution_date"
    757            ",wt.payto_uri AS sender_account_details"
    758            " FROM reserves_in ri"
    759            " JOIN wire_targets wt"
    760            "   ON (wire_source_h_payto = wire_target_h_payto)"
    761            " WHERE ri.reserve_pub=$1"
    762            "   AND ri.reserve_in_serial_id=$2;");
    763   PREPARE (pg,
    764            "get_reserve_history_withdraw_details",
    765            "SELECT"
    766            " planchets_h"
    767            ",amount_with_fee"
    768            ",reserve_sig"
    769            ",max_age"
    770            ",noreveal_index"
    771            ",selected_h"
    772            ",blinding_seed"
    773            ",denom_serials"
    774            ",ARRAY("
    775            "  SELECT denominations.denom_pub_hash FROM ("
    776            "    SELECT UNNEST(denom_serials) AS id,"
    777            "           generate_subscripts(denom_serials, 1) AS nr" /* for order */
    778            "  ) AS denoms"
    779            "  LEFT JOIN denominations ON denominations.denominations_serial=denoms.id"
    780            ") AS denom_pub_hashes"
    781            " FROM withdraw "
    782            " WHERE withdraw_id=$2"
    783            " AND reserve_pub=$1;");
    784   PREPARE (pg,
    785            "get_reserve_history_recoup_by_reserve",
    786            "SELECT"
    787            " rec.coin_pub"
    788            ",rec.coin_sig"
    789            ",rec.coin_blind"
    790            ",rec.amount"
    791            ",rec.recoup_timestamp"
    792            ",ro.planchets_h"
    793            ",denom.denom_pub_hash"
    794            ",kc.denom_sig"
    795            " FROM recoup rec"
    796            " JOIN withdraw ro"
    797            "   USING (withdraw_id)"
    798            " JOIN reserves res"
    799            "   USING (reserve_pub)"
    800            " JOIN known_coins kc"
    801            "   USING (coin_pub)"
    802            " JOIN denominations denom"
    803            "   ON (denom.denominations_serial = kc.denominations_serial)"
    804            " WHERE rec.recoup_uuid=$2"
    805            "   AND res.reserve_pub=$1;");
    806   PREPARE (pg,
    807            "get_reserve_history_close_by_reserve",
    808            "SELECT"
    809            " rc.amount"
    810            ",rc.closing_fee"
    811            ",rc.execution_date"
    812            ",wt.payto_uri AS receiver_account"
    813            ",rc.wtid"
    814            " FROM reserves_close rc"
    815            " JOIN wire_targets wt"
    816            "   USING (wire_target_h_payto)"
    817            " WHERE reserve_pub=$1"
    818            "   AND close_uuid=$2;");
    819   PREPARE (pg,
    820            "get_reserve_history_merge_by_reserve",
    821            "SELECT"
    822            " pr.amount_with_fee"
    823            ",pr.balance"
    824            ",pr.purse_fee"
    825            ",pr.h_contract_terms"
    826            ",pr.merge_pub"
    827            ",am.reserve_sig"
    828            ",pm.purse_pub"
    829            ",pm.merge_timestamp"
    830            ",pr.purse_expiration"
    831            ",pr.age_limit"
    832            ",pr.flags"
    833            " FROM purse_decision pdes"
    834            "   JOIN purse_requests pr"
    835            "     ON (pr.purse_pub = pdes.purse_pub)"
    836            "   JOIN purse_merges pm"
    837            "     ON (pm.purse_pub = pdes.purse_pub)"
    838            "   JOIN account_merges am"
    839            "     ON (am.purse_pub = pm.purse_pub AND"
    840            "         am.reserve_pub = pm.reserve_pub)"
    841            " WHERE pdes.purse_decision_serial_id=$2"
    842            "  AND pm.reserve_pub=$1"
    843            "  AND COALESCE(pm.partner_serial_id,0)=0" /* must be local! */
    844            "  AND NOT pdes.refunded;");
    845   PREPARE (pg,
    846            "get_reserve_history_open_request_by_reserve",
    847            "SELECT"
    848            " reserve_payment AS open_fee"
    849            ",request_timestamp"
    850            ",expiration_date"
    851            ",requested_purse_limit"
    852            ",reserve_sig"
    853            " FROM reserves_open_requests"
    854            " WHERE reserve_pub=$1"
    855            "   AND open_request_uuid=$2;");
    856   PREPARE (pg,
    857            "get_reserve_history_close_request_by_reserve",
    858            "SELECT"
    859            " close_timestamp"
    860            ",payto_uri"
    861            ",reserve_sig"
    862            " FROM close_requests"
    863            " WHERE reserve_pub=$1"
    864            "   AND close_request_serial_id=$2;");
    865 
    866   for (unsigned int i = 0; i<RETRIES; i++)
    867   {
    868     enum GNUNET_DB_QueryStatus qs;
    869     uint64_t end;
    870     struct GNUNET_PQ_ResultSpec rs[] = {
    871       GNUNET_PQ_result_spec_uint64 ("reserve_history_serial_id",
    872                                     &end),
    873       TALER_PQ_RESULT_SPEC_AMOUNT ("current_balance",
    874                                    balance),
    875       GNUNET_PQ_result_spec_end
    876     };
    877 
    878     if (GNUNET_OK !=
    879         TALER_EXCHANGEDB_start_read_committed (pg,
    880                                                "get-reserve-transactions")
    881         )
    882     {
    883       GNUNET_break (0);
    884       return GNUNET_DB_STATUS_HARD_ERROR;
    885     }
    886     /* First only check the last item, to see if
    887        we even need to iterate */
    888     qs = GNUNET_PQ_eval_prepared_singleton_select (
    889       pg->conn,
    890       "get_reserve_history_etag",
    891       params,
    892       rs);
    893     switch (qs)
    894     {
    895     case GNUNET_DB_STATUS_HARD_ERROR:
    896       TALER_EXCHANGEDB_rollback (pg);
    897       return qs;
    898     case GNUNET_DB_STATUS_SOFT_ERROR:
    899       TALER_EXCHANGEDB_rollback (pg);
    900       continue;
    901     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    902       TALER_EXCHANGEDB_rollback (pg);
    903       return qs;
    904     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    905       *etag_out = end;
    906       if (end == etag_in)
    907       {
    908         /* Nothing changed since the client last asked, so we are done --
    909            but the read-committed transaction opened above still has to be
    910            closed.  Leaving it open parks the connection in "idle in
    911            transaction" until the next TALER_EXCHANGEDB_start(), whose
    912            preflight then rolls it back and logs it as a bug. */
    913         TALER_EXCHANGEDB_rollback (pg);
    914         return qs;
    915       }
    916     }
    917     /* We indeed need to iterate over the history */
    918     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
    919                 "Current ETag for reserve %s is %llu\n",
    920                 TALER_B2S (reserve_pub),
    921                 (unsigned long long) end);
    922 
    923     qs = GNUNET_PQ_eval_prepared_multi_select (
    924       pg->conn,
    925       "get_reserve_history",
    926       lparams,
    927       &handle_history_entry,
    928       &rhc);
    929     switch (qs)
    930     {
    931     case GNUNET_DB_STATUS_HARD_ERROR:
    932       TALER_EXCHANGEDB_rollback (pg);
    933       return qs;
    934     case GNUNET_DB_STATUS_SOFT_ERROR:
    935       TALER_EXCHANGEDB_rollback (pg);
    936       TALER_EXCHANGEDB_free_reserve_history (rhc.rh);
    937       rhc.rh = NULL;
    938       rhc.rh_tail = NULL;
    939       continue;
    940     default:
    941       break;
    942     }
    943     if (rhc.failed)
    944     {
    945       TALER_EXCHANGEDB_rollback (pg);
    946       TALER_EXCHANGEDB_free_reserve_history (rhc.rh);
    947       return GNUNET_DB_STATUS_SOFT_ERROR;
    948     }
    949     qs = TALER_EXCHANGEDB_commit (pg);
    950     switch (qs)
    951     {
    952     case GNUNET_DB_STATUS_HARD_ERROR:
    953       TALER_EXCHANGEDB_free_reserve_history (rhc.rh);
    954       return qs;
    955     case GNUNET_DB_STATUS_SOFT_ERROR:
    956       TALER_EXCHANGEDB_free_reserve_history (rhc.rh);
    957       rhc.rh = NULL;
    958       rhc.rh_tail = NULL;
    959       continue;
    960     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
    961     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
    962       *rhp = rhc.rh;
    963       return GNUNET_DB_STATUS_SUCCESS_ONE_RESULT;
    964     }
    965   }
    966   return GNUNET_DB_STATUS_SOFT_ERROR;
    967 }