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 (21401B)


      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 merged purse has expired and been decided, the deposit that
    501  * would complete it is refused as too late.
    502  *
    503  * Note what this shows about the shape of the function: the "too late"
    504  * answer only comes out of the attempt to record the decision, which is
    505  * only reached for a purse that has been merged *and* is completed by this
    506  * deposit.  A deposit into an expired purse that neither completes it nor
    507  * has been merged is accepted -- see EDBT-9 in bugs.txt.
    508  *
    509  * @param pg the database context
    510  * @return 0 on success
    511  */
    512 static int
    513 check_too_late (struct TALER_EXCHANGEDB_PostgresContext *pg)
    514 {
    515   struct TDB_Purse purse;
    516   struct TALER_CoinPublicInfo coin;
    517   struct TALER_ReservePublicKeyP reserve_pub;
    518   struct DepositStatus st;
    519   struct GNUNET_TIME_Absolute expiration;
    520 
    521   /* a purse that expired in 2020, merged into a reserve */
    522   TDB_purse (pg,
    523              11,
    524              "5",
    525              ts (1600000000),
    526              &purse);
    527   TDB_reserve (pg,
    528                11,
    529                "0",
    530                &reserve_pub);
    531   {
    532     char *phex = TDB_hex (&purse.purse_pub,
    533                           sizeof (purse.purse_pub));
    534     char *rhex = TDB_hex (&reserve_pub,
    535                           sizeof (reserve_pub));
    536 
    537     FAILIF_C (GNUNET_OK !=
    538               TDB_exec (pg,
    539                         "INSERT INTO purse_merges"
    540                         " (reserve_pub,purse_pub,merge_sig,merge_timestamp)"
    541                         " VALUES (decode('%s','hex'),decode('%s','hex')"
    542                         "        ,decode(repeat('11',64),'hex'),1000000);",
    543                         rhex,
    544                         phex),
    545               GNUNET_free (phex); GNUNET_free (rhex));
    546     GNUNET_free (phex);
    547     GNUNET_free (rhex);
    548   }
    549   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    550           TALER_EXCHANGEDB_do_expire_purse (pg,
    551                                             GNUNET_TIME_UNIT_ZERO_ABS,
    552                                             GNUNET_TIME_absolute_get (),
    553                                             &expiration));
    554   TDB_coin (pg,
    555             &denom,
    556             22,
    557             &coin,
    558             NULL);
    559   /* Pay in the full target amount: the "too late" answer only comes out
    560      when the purse would be decided by this deposit, which is when the
    561      function tries to insert the `purse_decision` row that already exists.
    562      As with the balance check, the row is written first, so roll back. */
    563   FAILIF_C (GNUNET_OK !=
    564             TALER_EXCHANGEDB_start (pg,
    565                                     "test-purse-deposit-too-late"),
    566             TDB_coin_free (&coin));
    567   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    568             run_deposit (pg,
    569                          &purse,
    570                          &coin,
    571                          22,
    572                          "5",
    573                          "0",
    574                          &st),
    575             TALER_EXCHANGEDB_rollback (pg); TDB_coin_free (&coin));
    576   TALER_EXCHANGEDB_rollback (pg);
    577   TDB_coin_free (&coin);
    578   FAILIF (! st.too_late);
    579   FAILIF (1 != TDB_count (pg,
    580                           "FROM purse_deposits"));
    581   return 0;
    582 }
    583 
    584 
    585 /**
    586  * The iterators see the deposit; the by-purse one only for its own purse.
    587  *
    588  * @param pg the database context
    589  * @return 0 on success
    590  */
    591 static int
    592 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    593 {
    594   struct TALER_CoinSpendPublicKeyP coin_pub;
    595   struct TALER_PurseContractPublicKeyP purse_pub;
    596   struct DepositContext ctx;
    597   struct TALER_Amount expect = TDB_amount ("1");
    598 
    599   TDB_FILL (coin_pub,
    600             20);
    601   TDB_FILL (purse_pub,
    602             10);
    603   memset (&ctx,
    604           0,
    605           sizeof (ctx));
    606   ctx.coin_pub = &coin_pub;
    607   FAILIF (1 !=
    608           TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
    609             pg,
    610             0,
    611             &deposit_cb,
    612             &ctx));
    613   FAILIF (1 != ctx.matched);
    614   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    615                                  &expect));
    616   /* the purse was never merged, so there is no reserve */
    617   FAILIF (ctx.have_reserve);
    618 
    619   memset (&ctx,
    620           0,
    621           sizeof (ctx));
    622   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    623           TALER_EXCHANGEDB_iterate_purse_deposits_above_serial_id (
    624             pg,
    625             1000,
    626             &deposit_cb,
    627             &ctx));
    628   FAILIF (0 != ctx.total);
    629 
    630   memset (&ctx,
    631           0,
    632           sizeof (ctx));
    633   ctx.coin_pub = &coin_pub;
    634   FAILIF (1 !=
    635           TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
    636                                                             &purse_pub,
    637                                                             &refund_cb,
    638                                                             &ctx));
    639   FAILIF (1 != ctx.matched);
    640   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    641                                  &expect));
    642 
    643   /* a purse nobody paid into has no coins */
    644   {
    645     struct TALER_PurseContractPublicKeyP other;
    646 
    647     TDB_FILL (other,
    648               11);
    649     memset (&ctx,
    650             0,
    651             sizeof (ctx));
    652     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    653             TALER_EXCHANGEDB_iterate_purse_deposits_by_purse (pg,
    654                                                               &other,
    655                                                               &refund_cb,
    656                                                               &ctx));
    657     FAILIF (0 != ctx.total);
    658   }
    659   return 0;
    660 }
    661 
    662 
    663 /**
    664  * The checks to run, in order.
    665  */
    666 static const struct TDB_Test tests[] = {
    667   { "purse-deposits-empty",
    668     &check_empty },
    669   { "purse-deposits-deposit",
    670     &check_deposit },
    671   { "purse-deposits-insufficient-balance",
    672     &check_insufficient_balance },
    673   { "purse-deposits-too-late",
    674     &check_too_late },
    675   { "purse-deposits-iterate",
    676     &check_iterate },
    677   { NULL, NULL }
    678 };
    679 
    680 
    681 int
    682 main (int argc,
    683       char *const *argv)
    684 {
    685   int ret;
    686 
    687   ret = TDB_main (argc,
    688                   argv,
    689                   "test-purse-deposits",
    690                   "Tests for the exchangedb `purse_deposits' table",
    691                   tests);
    692   TDB_denom_free (&denom);
    693   return ret;
    694 }
    695 
    696 
    697 /* end of test_purse_deposits.c */