exchange

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

test_purse_deposits.c (24513B)


      1 /*
      2   This file is part of TALER
      3   Copyright (C) 2026 Taler Systems SA
      4 
      5   TALER is free software; you can redistribute it and/or modify it under the
      6   terms of the GNU General Public License as published by the Free Software
      7   Foundation; either version 3, or (at your option) any later version.
      8 
      9   TALER is distributed in the hope that it will be useful, but WITHOUT ANY
     10   WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
     11   A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
     12 
     13   You should have received a copy of the GNU General Public License along with
     14   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
     15 */
     16 /**
     17  * @file exchangedb/test_purse_deposits.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `purse_deposits`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_purse_deposit(),
     23  * #TALER_EXCHANGEDB_get_purse_deposit(),
     24  * #TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id() and
     25  * #TALER_EXCHANGEDB_iterate_purse_deposits_by_purse().
     26  *
     27  * `purse_deposits` references `purse_requests` and `known_coins`, which
     28  * TDB_purse() and TDB_coin() create.  do_purse_deposit() declines in three
     29  * ways: the coin has too little left, the purse has already been decided
     30  * (too late), and the same coin was already paid into the purse with a
     31  * different amount (conflict).
     32  */
     33 #include "test_common.h"
     34 #include "exchange-database/do_expire_purse.h"
     35 #include "exchange-database/do_purse_deposit.h"
     36 #include "exchange-database/get_purse_deposit.h"
     37 #include "exchange-database/iterate_purse_deposits_above_serial_id.h"
     38 #include "exchange-database/iterate_purse_deposits_by_purse.h"
     39 #include "exchange-database/rollback.h"
     40 #include "exchange-database/start.h"
     41 
     42 
     43 /**
     44  * Denomination the checks deposit.
     45  */
     46 static struct TDB_Denom denom;
     47 
     48 
     49 /**
     50  * Build a timestamp from a number of seconds since the epoch.
     51  *
     52  * @param secs seconds since the epoch
     53  * @return the timestamp
     54  */
     55 static struct GNUNET_TIME_Timestamp
     56 ts (uint64_t secs)
     57 {
     58   struct GNUNET_TIME_Absolute abs = {
     59     .abs_value_us = secs * 1000LLU * 1000LLU
     60   };
     61 
     62   return GNUNET_TIME_absolute_to_timestamp (abs);
     63 }
     64 
     65 
     66 /**
     67  * Closure for #deposit_cb() and #refund_cb().
     68  */
     69 struct DepositContext
     70 {
     71   /**
     72    * How many rows did the callback see?
     73    */
     74   unsigned int total;
     75 
     76   /**
     77    * Stop after this many rows; 0 for no limit.
     78    */
     79   unsigned int stop_after;
     80 
     81   /**
     82    * Coin we are looking for, NULL to match nothing.
     83    */
     84   const struct TALER_CoinSpendPublicKeyP *coin_pub;
     85 
     86   /**
     87    * How many times did we see it?
     88    */
     89   unsigned int matched;
     90 
     91   /**
     92    * Amount reported for it.
     93    */
     94   struct TALER_Amount amount;
     95 
     96   /**
     97    * Was a reserve reported for its purse?
     98    */
     99   bool have_reserve;
    100 };
    101 
    102 
    103 /**
    104  * Callback for
    105  * #TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id().
    106  *
    107  * @param cls a `struct DepositContext *`
    108  * @param rowid row of the deposit
    109  * @param deposit details of the deposit
    110  * @param reserve_pub reserve the purse was merged into, NULL if none
    111  * @param flags flags of the purse
    112  * @param auditor_balance balance of the purse
    113  * @param purse_total target amount of the purse
    114  * @param denom_pub denomination of the coin
    115  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    116  */
    117 static enum GNUNET_GenericReturnValue
    118 deposit_cb (void *cls,
    119             uint64_t rowid,
    120             const struct TALER_EXCHANGEDB_PurseDeposit *deposit,
    121             const struct TALER_ReservePublicKeyP *reserve_pub,
    122             enum TALER_WalletAccountMergeFlags flags,
    123             const struct TALER_Amount *auditor_balance,
    124             const struct TALER_Amount *purse_total,
    125             const struct TALER_DenominationPublicKey *denom_pub)
    126 {
    127   struct DepositContext *ctx = cls;
    128 
    129   (void) rowid;
    130   (void) flags;
    131   (void) auditor_balance;
    132   (void) purse_total;
    133   (void) denom_pub;
    134   ctx->total++;
    135   if ( (NULL != ctx->coin_pub) &&
    136        (0 == GNUNET_memcmp (&deposit->coin_pub,
    137                             ctx->coin_pub)) )
    138   {
    139     ctx->matched++;
    140     ctx->amount = deposit->amount;
    141     ctx->have_reserve = (NULL != reserve_pub);
    142   }
    143   if ( (0 != ctx->stop_after) &&
    144        (ctx->total >= ctx->stop_after) )
    145     return GNUNET_SYSERR;
    146   return GNUNET_OK;
    147 }
    148 
    149 
    150 /**
    151  * Callback for #TALER_EXCHANGEDB_iterate_purse_deposits_by_purse().
    152  *
    153  * @param cls a `struct DepositContext *`
    154  * @param rowid row of the deposit
    155  * @param amount_with_fee how much was paid in
    156  * @param coin_pub which coin paid
    157  * @param denom_pub denomination of that coin
    158  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    159  */
    160 static enum GNUNET_GenericReturnValue
    161 refund_cb (void *cls,
    162            uint64_t rowid,
    163            const struct TALER_Amount *amount_with_fee,
    164            const struct TALER_CoinSpendPublicKeyP *coin_pub,
    165            const struct TALER_DenominationPublicKey *denom_pub)
    166 {
    167   struct DepositContext *ctx = cls;
    168 
    169   (void) rowid;
    170   (void) denom_pub;
    171   ctx->total++;
    172   if ( (NULL != ctx->coin_pub) &&
    173        (0 == GNUNET_memcmp (coin_pub,
    174                             ctx->coin_pub)) )
    175   {
    176     ctx->matched++;
    177     ctx->amount = *amount_with_fee;
    178   }
    179   if ( (0 != ctx->stop_after) &&
    180        (ctx->total >= ctx->stop_after) )
    181     return GNUNET_SYSERR;
    182   return GNUNET_OK;
    183 }
    184 
    185 
    186 /**
    187  * Outcome of a purse deposit.
    188  */
    189 struct DepositStatus
    190 {
    191   /**
    192    * Did the coin have enough left?
    193    */
    194   bool balance_ok;
    195 
    196   /**
    197    * Was the purse already decided?
    198    */
    199   bool too_late;
    200 
    201   /**
    202    * Was the coin already paid into this purse differently?
    203    */
    204   bool conflict;
    205 };
    206 
    207 
    208 /**
    209  * Pay a coin into a purse.
    210  *
    211  * @param pg the database context
    212  * @param purse purse to credit
    213  * @param coin coin to debit
    214  * @param seed seed for the coin signature
    215  * @param amount how much to take from the coin, e.g. "1"
    216  * @param fee purse deposit fee included in @a amount, e.g. "0"
    217  * @param[out] st set to the outcome
    218  * @return transaction status
    219  */
    220 static enum GNUNET_DB_QueryStatus
    221 run_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg,
    222              const struct TDB_Purse *purse,
    223              const struct TALER_CoinPublicInfo *coin,
    224              uint32_t seed,
    225              const char *amount,
    226              const char *fee,
    227              struct DepositStatus *st)
    228 {
    229   struct TALER_CoinSpendSignatureP coin_sig;
    230   struct TALER_Amount a = TDB_amount (amount);
    231   struct TALER_Amount f = TDB_amount (fee);
    232   struct TALER_Amount net;
    233 
    234   TDB_fill (&coin_sig,
    235             sizeof (coin_sig),
    236             seed);
    237   GNUNET_assert (0 <=
    238                  TALER_amount_subtract (&net,
    239                                         &a,
    240                                         &f));
    241   memset (st,
    242           0,
    243           sizeof (*st));
    244   return TALER_EXCHANGEDB_do_purse_deposit (pg,
    245                                             &purse->purse_pub,
    246                                             &coin->coin_pub,
    247                                             &a,
    248                                             &coin_sig,
    249                                             &net,
    250                                             &st->balance_ok,
    251                                             &st->too_late,
    252                                             &st->conflict);
    253 }
    254 
    255 
    256 /**
    257  * Nothing is reported while the table is empty.
    258  *
    259  * @param pg the database context
    260  * @return 0 on success
    261  */
    262 static int
    263 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    264 {
    265   struct TALER_PurseContractPublicKeyP purse_pub;
    266   struct TALER_CoinSpendPublicKeyP coin_pub;
    267   struct TALER_Amount amount;
    268   struct TALER_DenominationHashP h_denom_pub;
    269   struct TALER_AgeCommitmentHashP phac;
    270   struct TALER_CoinSpendSignatureP coin_sig;
    271   struct DepositContext ctx = { 0 };
    272   char *partner_url = NULL;
    273   bool no_age_commitment;
    274 
    275   TDB_denom (pg,
    276              10,
    277              "5",
    278              "0.1",
    279              &denom);
    280   TDB_FILL (purse_pub,
    281             1);
    282   TDB_FILL (coin_pub,
    283             1);
    284   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    285           TALER_EXCHANGEDB_get_purse_deposit (pg,
    286                                               &purse_pub,
    287                                               &coin_pub,
    288                                               &amount,
    289                                               &h_denom_pub,
    290                                               &phac,
    291                                               &no_age_commitment,
    292                                               &coin_sig,
    293                                               &partner_url));
    294   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    295           TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
    296             pg,
    297             0,
    298             &deposit_cb,
    299             &ctx));
    300   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    301           TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
    302                                                             &purse_pub,
    303                                                             &refund_cb,
    304                                                             &ctx));
    305   FAILIF (0 != ctx.total);
    306   return 0;
    307 }
    308 
    309 
    310 /**
    311  * Paying a coin into a purse debits the coin and credits the purse.
    312  *
    313  * @param pg the database context
    314  * @return 0 on success
    315  */
    316 static int
    317 check_deposit (struct TALER_EXCHANGEDB_PostgresContext *pg)
    318 {
    319   struct TDB_Purse purse;
    320   struct TALER_CoinPublicInfo coin;
    321   struct DepositStatus st;
    322   struct TALER_Amount amount;
    323   struct TALER_Amount expect = TDB_amount ("1");
    324   struct TALER_DenominationHashP h_denom_pub;
    325   struct TALER_AgeCommitmentHashP phac;
    326   struct TALER_CoinSpendSignatureP coin_sig;
    327   struct TALER_CoinSpendSignatureP expect_sig;
    328   char *partner_url = NULL;
    329   bool no_age_commitment = false;
    330 
    331   TDB_purse (pg,
    332              10,
    333              "5",
    334              ts (1700000000),
    335              &purse);
    336   TDB_coin (pg,
    337             &denom,
    338             20,
    339             &coin,
    340             NULL);
    341   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    342             run_deposit (pg,
    343                          &purse,
    344                          &coin,
    345                          20,
    346                          "1",
    347                          "0",
    348                          &st),
    349             TDB_coin_free (&coin));
    350   FAILIF_C (! st.balance_ok,
    351             TDB_coin_free (&coin));
    352   FAILIF_C (st.too_late,
    353             TDB_coin_free (&coin));
    354   FAILIF_C (st.conflict,
    355             TDB_coin_free (&coin));
    356   FAILIF_C (1 != TDB_count (pg,
    357                             "FROM purse_deposits"),
    358             TDB_coin_free (&coin));
    359 
    360   /* the coin was debited... */
    361   {
    362     char *hex = TDB_hex (&coin.coin_pub,
    363                          sizeof (coin.coin_pub));
    364 
    365     FAILIF_C (1 != TDB_count (pg,
    366                               "FROM known_coins"
    367                               " WHERE coin_pub=decode('%s','hex')"
    368                               "   AND remaining=ROW(4,0)::taler_amount",
    369                               hex),
    370               GNUNET_free (hex); TDB_coin_free (&coin));
    371     GNUNET_free (hex);
    372   }
    373   /* ...and the purse credited */
    374   {
    375     char *hex = TDB_hex (&purse.purse_pub,
    376                          sizeof (purse.purse_pub));
    377 
    378     FAILIF_C (1 != TDB_count (pg,
    379                               "FROM purse_requests"
    380                               " WHERE purse_pub=decode('%s','hex')"
    381                               "   AND balance=ROW(1,0)::taler_amount",
    382                               hex),
    383               GNUNET_free (hex); TDB_coin_free (&coin));
    384     GNUNET_free (hex);
    385   }
    386 
    387   TDB_FILL (expect_sig,
    388             20);
    389   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    390             TALER_EXCHANGEDB_get_purse_deposit (pg,
    391                                                 &purse.purse_pub,
    392                                                 &coin.coin_pub,
    393                                                 &amount,
    394                                                 &h_denom_pub,
    395                                                 &phac,
    396                                                 &no_age_commitment,
    397                                                 &coin_sig,
    398                                                 &partner_url),
    399             TDB_coin_free (&coin));
    400   FAILIF_C (0 != TALER_amount_cmp (&amount,
    401                                    &expect),
    402             GNUNET_free (partner_url); TDB_coin_free (&coin));
    403   FAILIF_C (0 != GNUNET_memcmp (&h_denom_pub,
    404                                 &denom.h_denom_pub),
    405             GNUNET_free (partner_url); TDB_coin_free (&coin));
    406   FAILIF_C (0 != GNUNET_memcmp (&coin_sig,
    407                                 &expect_sig),
    408             GNUNET_free (partner_url); TDB_coin_free (&coin));
    409   FAILIF_C (! no_age_commitment,
    410             GNUNET_free (partner_url); TDB_coin_free (&coin));
    411   /* the purse is ours, so there is no partner exchange */
    412   FAILIF_C (NULL != partner_url,
    413             GNUNET_free (partner_url); TDB_coin_free (&coin));
    414 
    415   /* re-depositing the same coin with the same amount is idempotent */
    416   FAILIF_C (0 >
    417             run_deposit (pg,
    418                          &purse,
    419                          &coin,
    420                          20,
    421                          "1",
    422                          "0",
    423                          &st),
    424             TDB_coin_free (&coin));
    425   FAILIF_C (1 != TDB_count (pg,
    426                             "FROM purse_deposits"),
    427             TDB_coin_free (&coin));
    428 
    429   /* ...but a second attempt with a different coin signature is a conflict:
    430      the idempotency check compares the signature, not the amount */
    431   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    432             run_deposit (pg,
    433                          &purse,
    434                          &coin,
    435                          29,
    436                          "1",
    437                          "0",
    438                          &st),
    439             TDB_coin_free (&coin));
    440   FAILIF_C (! st.conflict,
    441             TDB_coin_free (&coin));
    442   FAILIF_C (1 != TDB_count (pg,
    443                             "FROM purse_deposits"),
    444             TDB_coin_free (&coin));
    445   TDB_coin_free (&coin);
    446   return 0;
    447 }
    448 
    449 
    450 /**
    451  * A coin without enough left cannot be paid into a purse.
    452  *
    453  * @param pg the database context
    454  * @return 0 on success
    455  */
    456 static int
    457 check_insufficient_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    458 {
    459   struct TDB_Purse purse;
    460   struct TALER_CoinPublicInfo coin;
    461   struct DepositStatus st;
    462 
    463   TDB_purse (pg,
    464              10,
    465              "5",
    466              ts (1700000000),
    467              &purse);
    468   TDB_coin (pg,
    469             &denom,
    470             21,
    471             &coin,
    472             NULL);
    473   /* The `purse_deposits` row goes in before the coin balance is checked,
    474      so the caller has to roll back -- which is what the /purses/.../deposit
    475      handler does. */
    476   FAILIF_C (GNUNET_OK !=
    477             TALER_EXCHANGEDB_start (pg,
    478                                     "test-purse-deposit-insufficient"),
    479             TDB_coin_free (&coin));
    480   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    481             run_deposit (pg,
    482                          &purse,
    483                          &coin,
    484                          21,
    485                          "6",
    486                          "0",
    487                          &st),
    488             TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
    489   TALER_EXCHANGEDB_rollback (pg);
    490   TDB_coin_free (&coin);
    491   FAILIF (st.balance_ok);
    492   FAILIF (st.conflict);
    493   FAILIF (1 != TDB_count (pg,
    494                           "FROM purse_deposits"));
    495   return 0;
    496 }
    497 
    498 
    499 /**
    500  * Once a purse has been decided, a new deposit into it is refused as too
    501  * late, whether or not the purse was merged and whether or not the
    502  * deposit would complete it.  An idempotent replay of a deposit made
    503  * before the decision still succeeds.
    504  *
    505  * @param pg the database context
    506  * @return 0 on success
    507  */
    508 static int
    509 check_too_late (struct TALER_EXCHANGEDB_PostgresContext *pg)
    510 {
    511   struct TDB_Purse purse;
    512   struct TALER_CoinPublicInfo coin;
    513   struct TALER_ReservePublicKeyP reserve_pub;
    514   struct DepositStatus st;
    515   struct GNUNET_TIME_Absolute expiration;
    516 
    517   /* a purse that expired in 2020, merged into a reserve */
    518   TDB_purse (pg,
    519              11,
    520              "5",
    521              ts (1600000000),
    522              &purse);
    523   TDB_reserve (pg,
    524                11,
    525                "0",
    526                &reserve_pub);
    527   {
    528     char *phex = TDB_hex (&purse.purse_pub,
    529                           sizeof (purse.purse_pub));
    530     char *rhex = TDB_hex (&reserve_pub,
    531                           sizeof (reserve_pub));
    532 
    533     FAILIF_C (GNUNET_OK !=
    534               TDB_exec (pg,
    535                         "INSERT INTO purse_merges"
    536                         " (reserve_pub,purse_pub,merge_sig,merge_timestamp)"
    537                         " VALUES (decode('%s','hex'),decode('%s','hex')"
    538                         "        ,decode(repeat('11',64),'hex'),1000000);",
    539                         rhex,
    540                         phex),
    541               GNUNET_free (phex); GNUNET_free (rhex));
    542     GNUNET_free (phex);
    543     GNUNET_free (rhex);
    544   }
    545   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    546           TALER_EXCHANGEDB_do_expire_purse (pg,
    547                                             GNUNET_TIME_UNIT_ZERO_ABS,
    548                                             GNUNET_TIME_absolute_get (),
    549                                             &expiration));
    550   TDB_coin (pg,
    551             &denom,
    552             22,
    553             &coin,
    554             NULL);
    555   /* Pay in the full target amount.  As with the balance check, the
    556      `purse_deposits` row is written first, so roll back. */
    557   FAILIF_C (GNUNET_OK !=
    558             TALER_EXCHANGEDB_start (pg,
    559                                     "test-purse-deposit-too-late"),
    560             TDB_coin_free (&coin));
    561   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    562             run_deposit (pg,
    563                          &purse,
    564                          &coin,
    565                          22,
    566                          "5",
    567                          "0",
    568                          &st),
    569             TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
    570   TALER_EXCHANGEDB_rollback (pg);
    571   TDB_coin_free (&coin);
    572   FAILIF (! st.too_late);
    573   FAILIF (st.balance_ok);
    574   FAILIF (st.conflict);
    575   FAILIF (1 != TDB_count (pg,
    576                           "FROM purse_deposits"));
    577 
    578   /* A purse that was never merged and has been decided (refunded):
    579      a deposit that would not complete it is refused as well, but a
    580      replay of a deposit made before the decision is still idempotent.
    581      All of this runs in one transaction that is rolled back, so the
    582      later checks see the table unchanged. */
    583   {
    584     struct TDB_Purse purse2;
    585     struct TALER_CoinPublicInfo coin2;
    586     struct TALER_CoinPublicInfo coin3;
    587     char *phex;
    588 
    589     TDB_purse (pg,
    590                12,
    591                "5",
    592                ts (1600000000),
    593                &purse2);
    594     TDB_coin (pg,
    595               &denom,
    596               23,
    597               &coin2,
    598               NULL);
    599     TDB_coin (pg,
    600               &denom,
    601               24,
    602               &coin3,
    603               NULL);
    604     phex = TDB_hex (&purse2.purse_pub,
    605                     sizeof (purse2.purse_pub));
    606     FAILIF_C (GNUNET_OK !=
    607               TALER_EXCHANGEDB_start (pg,
    608                                       "test-purse-deposit-decided"),
    609               GNUNET_free (phex);
    610               TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    611     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    612               run_deposit (pg,
    613                            &purse2,
    614                            &coin2,
    615                            23,
    616                            "1",
    617                            "0",
    618                            &st),
    619               TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
    620               TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    621     FAILIF_C ( (! st.balance_ok) || st.too_late || st.conflict,
    622                TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
    623                TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    624     FAILIF_C (GNUNET_OK !=
    625               TDB_exec (pg,
    626                         "INSERT INTO purse_decision"
    627                         " (purse_pub,action_timestamp,refunded)"
    628                         " VALUES (decode('%s','hex'),1000000,TRUE);",
    629                         phex),
    630               TALER_EXCHANGEDB_rollback (pg); GNUNET_free (phex);
    631               TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    632     GNUNET_free (phex);
    633     /* replay of the deposit made before the decision */
    634     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    635               run_deposit (pg,
    636                            &purse2,
    637                            &coin2,
    638                            23,
    639                            "1",
    640                            "0",
    641                            &st),
    642               TALER_EXCHANGEDB_rollback (pg);
    643               TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    644     FAILIF_C ( (! st.balance_ok) || st.too_late || st.conflict,
    645                TALER_EXCHANGEDB_rollback (pg);
    646                TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    647     /* a new coin, not completing the purse */
    648     FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    649               run_deposit (pg,
    650                            &purse2,
    651                            &coin3,
    652                            24,
    653                            "1",
    654                            "0",
    655                            &st),
    656               TALER_EXCHANGEDB_rollback (pg);
    657               TDB_coin_free (&coin2); TDB_coin_free (&coin3));
    658     TALER_EXCHANGEDB_rollback (pg);
    659     TDB_coin_free (&coin2);
    660     TDB_coin_free (&coin3);
    661     FAILIF (! st.too_late);
    662     FAILIF (st.balance_ok);
    663     FAILIF (st.conflict);
    664     FAILIF (1 != TDB_count (pg,
    665                             "FROM purse_deposits"));
    666   }
    667   return 0;
    668 }
    669 
    670 
    671 /**
    672  * The iterators see the deposit; the by-purse one only for its own purse.
    673  *
    674  * @param pg the database context
    675  * @return 0 on success
    676  */
    677 static int
    678 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    679 {
    680   struct TALER_CoinSpendPublicKeyP coin_pub;
    681   struct TALER_PurseContractPublicKeyP purse_pub;
    682   struct DepositContext ctx;
    683   struct TALER_Amount expect = TDB_amount ("1");
    684 
    685   TDB_FILL (coin_pub,
    686             20);
    687   TDB_FILL (purse_pub,
    688             10);
    689   memset (&ctx,
    690           0,
    691           sizeof (ctx));
    692   ctx.coin_pub = &coin_pub;
    693   FAILIF (1 !=
    694           TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
    695             pg,
    696             0,
    697             &deposit_cb,
    698             &ctx));
    699   FAILIF (1 != ctx.matched);
    700   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    701                                  &expect));
    702   /* the purse was never merged, so there is no reserve */
    703   FAILIF (ctx.have_reserve);
    704 
    705   memset (&ctx,
    706           0,
    707           sizeof (ctx));
    708   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    709           TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
    710             pg,
    711             1000,
    712             &deposit_cb,
    713             &ctx));
    714   FAILIF (0 != ctx.total);
    715 
    716   memset (&ctx,
    717           0,
    718           sizeof (ctx));
    719   ctx.coin_pub = &coin_pub;
    720   FAILIF (1 !=
    721           TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
    722                                                             &purse_pub,
    723                                                             &refund_cb,
    724                                                             &ctx));
    725   FAILIF (1 != ctx.matched);
    726   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    727                                  &expect));
    728 
    729   /* a purse nobody paid into has no coins */
    730   {
    731     struct TALER_PurseContractPublicKeyP other;
    732 
    733     TDB_FILL (other,
    734               11);
    735     memset (&ctx,
    736             0,
    737             sizeof (ctx));
    738     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    739             TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
    740                                                               &other,
    741                                                               &refund_cb,
    742                                                               &ctx));
    743     FAILIF (0 != ctx.total);
    744   }
    745   return 0;
    746 }
    747 
    748 
    749 /**
    750  * The checks to run, in order.
    751  */
    752 static const struct TDB_Test tests[] = {
    753   { "purse-deposits-empty",
    754     &check_empty },
    755   { "purse-deposits-deposit",
    756     &check_deposit },
    757   { "purse-deposits-insufficient-balance",
    758     &check_insufficient_balance },
    759   { "purse-deposits-too-late",
    760     &check_too_late },
    761   { "purse-deposits-iterate",
    762     &check_iterate },
    763   { NULL, NULL }
    764 };
    765 
    766 
    767 int
    768 main (int argc,
    769       char *const *argv)
    770 {
    771   int ret;
    772 
    773   ret = TDB_main (argc,
    774                   argv,
    775                   "test-purse-deposits",
    776                   "Tests for the exchangedb `purse_deposits' table",
    777                   tests);
    778   TDB_denom_free (&denom);
    779   return ret;
    780 }
    781 
    782 
    783 /* end of test_purse_deposits.c */