exchange

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

test_reserves_in.c (22567B)


      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_reserves_in.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `reserves_in`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_import_credits() (for its reserve half; the
     23  * KYC-auth half is in test_kycauths_in.c),
     24  * #TALER_EXCHANGEDB_get_reserve_origin(),
     25  * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id(),
     26  * #TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account() and
     27  * #TALER_EXCHANGEDB_iterate_exchange_credit_transfers().
     28  *
     29  * do_import_credits() creates the `wire_targets` and `reserves` rows it
     30  * needs on its own, so the only fixture the checks have to set up is a work
     31  * shard for the batch to advance.
     32  */
     33 #include "test_common.h"
     34 #include "exchange-database/begin_shard.h"
     35 #include "exchange-database/do_import_credits.h"
     36 #include "exchange-database/get_reserve_origin.h"
     37 #include "exchange-database/iterate_exchange_credit_transfers.h"
     38 #include "exchange-database/iterate_reserves_in_above_serial_id.h"
     39 #include "exchange-database/iterate_reserves_in_above_serial_id_by_account.h"
     40 
     41 
     42 /**
     43  * Account the first two transfers come from.
     44  */
     45 #define PAYTO_A "payto://x-taler-bank/localhost/ri-a?receiver-name=A"
     46 
     47 /**
     48  * Account the third transfer comes from.
     49  */
     50 #define PAYTO_B "payto://x-taler-bank/localhost/ri-b?receiver-name=B"
     51 
     52 
     53 /**
     54  * Closure for #reserve_in_cb().
     55  */
     56 struct InContext
     57 {
     58   /**
     59    * How many rows did the callback see?
     60    */
     61   unsigned int total;
     62 
     63   /**
     64    * Stop after this many rows; 0 for no limit.
     65    */
     66   unsigned int stop_after;
     67 
     68   /**
     69    * Reserve we are looking for, NULL to match nothing.
     70    */
     71   const struct TALER_ReservePublicKeyP *reserve_pub;
     72 
     73   /**
     74    * How many times did we see @e reserve_pub?
     75    */
     76   unsigned int matched;
     77 
     78   /**
     79    * Credit reported for @e reserve_pub.
     80    */
     81   struct TALER_Amount credit;
     82 
     83   /**
     84    * Row reported for @e reserve_pub.
     85    */
     86   uint64_t rowid;
     87 
     88   /**
     89    * Wire reference reported for @e reserve_pub.
     90    */
     91   uint64_t wire_reference;
     92 
     93   /**
     94    * Sender account reported for @e reserve_pub, owned by this struct.
     95    */
     96   char *sender;
     97 };
     98 
     99 
    100 /**
    101  * Callback for the `reserves_in` iterators.
    102  *
    103  * @param cls a `struct InContext *`
    104  * @param rowid row of the transfer
    105  * @param reserve_pub reserve that was credited
    106  * @param credit amount that was credited
    107  * @param sender_account_details account the money came from
    108  * @param wire_reference bank's identifier for the transfer
    109  * @param execution_date when the transfer was made
    110  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    111  */
    112 static enum GNUNET_GenericReturnValue
    113 reserve_in_cb (void *cls,
    114                uint64_t rowid,
    115                const struct TALER_ReservePublicKeyP *reserve_pub,
    116                const struct TALER_Amount *credit,
    117                const struct TALER_FullPayto sender_account_details,
    118                uint64_t wire_reference,
    119                struct GNUNET_TIME_Timestamp execution_date)
    120 {
    121   struct InContext *ctx = cls;
    122 
    123   (void) execution_date;
    124   ctx->total++;
    125   if ( (NULL != ctx->reserve_pub) &&
    126        (0 == GNUNET_memcmp (reserve_pub,
    127                             ctx->reserve_pub)) )
    128   {
    129     ctx->matched++;
    130     ctx->credit = *credit;
    131     ctx->rowid = rowid;
    132     ctx->wire_reference = wire_reference;
    133     GNUNET_free (ctx->sender);
    134     ctx->sender = (NULL == sender_account_details.full_payto)
    135       ? NULL
    136       : GNUNET_strdup (sender_account_details.full_payto);
    137   }
    138   if ( (0 != ctx->stop_after) &&
    139        (ctx->total >= ctx->stop_after) )
    140     return GNUNET_SYSERR;
    141   return GNUNET_OK;
    142 }
    143 
    144 
    145 /**
    146  * Closure for #transfer_cb().
    147  */
    148 struct TransferContext
    149 {
    150   /**
    151    * How many transfers did the callback see?
    152    */
    153   unsigned int total;
    154 
    155   /**
    156    * Row of the first transfer seen.
    157    */
    158   uint64_t first_row;
    159 
    160   /**
    161    * Sum of the whole-unit parts of the amounts seen.
    162    */
    163   uint64_t value_sum;
    164 };
    165 
    166 
    167 /**
    168  * Callback for #TALER_EXCHANGEDB_iterate_exchange_credit_transfers().
    169  *
    170  * @param cls a `struct TransferContext *`
    171  * @param row_id row of the transfer
    172  * @param payto_uri account the money came from
    173  * @param execution_time when the transfer was made
    174  * @param amount amount that was transferred
    175  */
    176 static void
    177 transfer_cb (void *cls,
    178              uint64_t row_id,
    179              const char *payto_uri,
    180              struct GNUNET_TIME_Absolute execution_time,
    181              const struct TALER_Amount *amount)
    182 {
    183   struct TransferContext *ctx = cls;
    184 
    185   (void) payto_uri;
    186   (void) execution_time;
    187   if (0 == ctx->total++)
    188     ctx->first_row = row_id;
    189   ctx->value_sum += amount->value;
    190 }
    191 
    192 
    193 /**
    194  * Import one wire transfer into a reserve, advancing a work shard of its
    195  * own along with it.
    196  *
    197  * @param pg the database context
    198  * @param job_name job whose shard to open and advance
    199  * @param account_name exchange bank account the transfer arrived at
    200  * @param payto account the money came from
    201  * @param seed seed for the reserve's public key
    202  * @param amount amount that was transferred, e.g. "10"
    203  * @param[out] reserve_pub set to the credited reserve
    204  * @param[out] status set to the per-transfer status
    205  * @return transaction status of the import
    206  */
    207 static enum GNUNET_DB_QueryStatus
    208 import_one (struct TALER_EXCHANGEDB_PostgresContext *pg,
    209             const char *job_name,
    210             const char *account_name,
    211             const char *payto,
    212             uint32_t seed,
    213             const char *amount,
    214             struct TALER_ReservePublicKeyP *reserve_pub,
    215             enum GNUNET_DB_QueryStatus *status)
    216 {
    217   struct TALER_Amount balance = TDB_amount (amount);
    218   struct TALER_EXCHANGEDB_ReserveInInfo info;
    219   struct TALER_EXCHANGEDB_CreditBatch batch;
    220   uint64_t start;
    221   uint64_t end;
    222   uint64_t progress;
    223 
    224   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    225                  TALER_EXCHANGEDB_begin_shard (pg,
    226                                                job_name,
    227                                                GNUNET_TIME_UNIT_HOURS,
    228                                                1024,
    229                                                &start,
    230                                                &end,
    231                                                &progress));
    232   TDB_fill (reserve_pub,
    233             sizeof (*reserve_pub),
    234             seed);
    235   memset (&info,
    236           0,
    237           sizeof (info));
    238   info.reserve_pub = reserve_pub;
    239   info.balance = &balance;
    240   info.execution_time = GNUNET_TIME_timestamp_get ();
    241   info.sender_account_details.full_payto = (char *) payto;
    242   info.wire_reference = start + 1;
    243   memset (&batch,
    244           0,
    245           sizeof (batch));
    246   batch.exchange_account_name = account_name;
    247   batch.reserves = &info;
    248   batch.reserves_length = 1;
    249   batch.job_name = job_name;
    250   batch.shard_start = start;
    251   batch.shard_end = end;
    252   batch.progress_row = end;
    253   batch.lease = GNUNET_TIME_UNIT_HOURS;
    254   return TALER_EXCHANGEDB_do_import_credits (pg,
    255                                              &batch,
    256                                              status);
    257 }
    258 
    259 
    260 /**
    261  * Nothing is known while the table is empty.
    262  *
    263  * @param pg the database context
    264  * @return 0 on success
    265  */
    266 static int
    267 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    268 {
    269   struct TALER_ReservePublicKeyP reserve_pub;
    270   struct TALER_FullPaytoHashP h_payto;
    271   struct TALER_FullPayto payto = { NULL };
    272   struct InContext ctx = { 0 };
    273   struct TransferContext tctx = { 0 };
    274   struct TALER_Amount zero = TDB_amount ("0");
    275 
    276   TDB_FILL (reserve_pub,
    277             1);
    278   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    279           TALER_EXCHANGEDB_get_reserve_origin (pg,
    280                                                &reserve_pub,
    281                                                &h_payto,
    282                                                &payto));
    283   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    284           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
    285                                                                 0,
    286                                                                 &reserve_in_cb,
    287                                                                 &ctx));
    288   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    289           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
    290             pg,
    291             "exchange-account-1",
    292             0,
    293             &reserve_in_cb,
    294             &ctx));
    295   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    296           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    297                                                               &zero,
    298                                                               0,
    299                                                               10,
    300                                                               NULL,
    301                                                               &transfer_cb,
    302                                                               &tctx));
    303   FAILIF (0 != ctx.total);
    304   FAILIF (0 != tctx.total);
    305   return 0;
    306 }
    307 
    308 
    309 /**
    310  * Importing a transfer creates the reserve, the account and the
    311  * `reserves_in` row, and doing it again is a no-op.
    312  *
    313  * @param pg the database context
    314  * @return 0 on success
    315  */
    316 static int
    317 check_import (struct TALER_EXCHANGEDB_PostgresContext *pg)
    318 {
    319   struct TALER_ReservePublicKeyP reserve_pub;
    320   struct TALER_FullPaytoHashP h_payto;
    321   struct TALER_FullPayto payto = { NULL };
    322   struct TALER_FullPayto expect = {
    323     .full_payto = (char *) PAYTO_A
    324   };
    325   struct TALER_FullPaytoHashP expect_h;
    326   struct TALER_Amount expect_amount = TDB_amount ("10");
    327   enum GNUNET_DB_QueryStatus status;
    328 
    329   FAILIF (0 >
    330           import_one (pg,
    331                       "ri-job-a",
    332                       "exchange-account-1",
    333                       PAYTO_A,
    334                       10,
    335                       "10",
    336                       &reserve_pub,
    337                       &status));
    338   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
    339   FAILIF (1 != TDB_count (pg,
    340                           "FROM reserves_in"));
    341   FAILIF (1 != TDB_count (pg,
    342                           "FROM reserves"));
    343 
    344   TALER_full_payto_hash (expect,
    345                          &expect_h);
    346   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    347           TALER_EXCHANGEDB_get_reserve_origin (pg,
    348                                                &reserve_pub,
    349                                                &h_payto,
    350                                                &payto));
    351   FAILIF (0 != GNUNET_memcmp (&h_payto,
    352                               &expect_h));
    353   FAILIF (0 != strcmp (payto.full_payto,
    354                        PAYTO_A));
    355   GNUNET_free (payto.full_payto);
    356 
    357   /* the same transfer again: the row is already there */
    358   {
    359     struct TALER_ReservePublicKeyP again;
    360 
    361     FAILIF (0 >
    362             import_one (pg,
    363                         "ri-job-a",
    364                         "exchange-account-1",
    365                         PAYTO_A,
    366                         10,
    367                         "10",
    368                         &again,
    369                         &status));
    370     FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS != status);
    371     FAILIF (1 != TDB_count (pg,
    372                             "FROM reserves_in"));
    373   }
    374 
    375   {
    376     struct InContext ctx;
    377 
    378     memset (&ctx,
    379             0,
    380             sizeof (ctx));
    381     ctx.reserve_pub = &reserve_pub;
    382     FAILIF (0 >=
    383             TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (
    384               pg,
    385               0,
    386               &reserve_in_cb,
    387               &ctx));
    388     FAILIF_C (1 != ctx.matched,
    389               GNUNET_free (ctx.sender));
    390     FAILIF_C (0 != TALER_amount_cmp (&ctx.credit,
    391                                      &expect_amount),
    392               GNUNET_free (ctx.sender));
    393     FAILIF_C (0 != strcmp (ctx.sender,
    394                            PAYTO_A),
    395               GNUNET_free (ctx.sender));
    396     GNUNET_free (ctx.sender);
    397   }
    398   return 0;
    399 }
    400 
    401 
    402 /**
    403  * With several transfers on file the iterators filter by serial and by
    404  * exchange bank account, and honour an aborting callback.
    405  *
    406  * @param pg the database context
    407  * @return 0 on success
    408  */
    409 static int
    410 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    411 {
    412   struct TALER_ReservePublicKeyP r2;
    413   struct TALER_ReservePublicKeyP r3;
    414   struct InContext ctx;
    415   enum GNUNET_DB_QueryStatus status;
    416 
    417   /* a second transfer from the same account to the same exchange account */
    418   FAILIF (0 >
    419           import_one (pg,
    420                       "ri-job-a",
    421                       "exchange-account-1",
    422                       PAYTO_A,
    423                       11,
    424                       "2",
    425                       &r2,
    426                       &status));
    427   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
    428   /* and one from a different account to a different exchange account */
    429   FAILIF (0 >
    430           import_one (pg,
    431                       "ri-job-b",
    432                       "exchange-account-2",
    433                       PAYTO_B,
    434                       12,
    435                       "5",
    436                       &r3,
    437                       &status));
    438   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != status);
    439   FAILIF (3 != TDB_count (pg,
    440                           "FROM reserves_in"));
    441 
    442   memset (&ctx,
    443           0,
    444           sizeof (ctx));
    445   ctx.reserve_pub = &r3;
    446   FAILIF (3 !=
    447           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
    448                                                                 0,
    449                                                                 &reserve_in_cb,
    450                                                                 &ctx));
    451   FAILIF_C (1 != ctx.matched,
    452             GNUNET_free (ctx.sender));
    453   FAILIF_C (0 != strcmp (ctx.sender,
    454                          PAYTO_B),
    455             GNUNET_free (ctx.sender));
    456   GNUNET_free (ctx.sender);
    457 
    458   /* by exchange bank account */
    459   memset (&ctx,
    460           0,
    461           sizeof (ctx));
    462   FAILIF (2 !=
    463           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
    464             pg,
    465             "exchange-account-1",
    466             0,
    467             &reserve_in_cb,
    468             &ctx));
    469   GNUNET_free (ctx.sender);
    470   memset (&ctx,
    471           0,
    472           sizeof (ctx));
    473   FAILIF (1 !=
    474           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
    475             pg,
    476             "exchange-account-2",
    477             0,
    478             &reserve_in_cb,
    479             &ctx));
    480   GNUNET_free (ctx.sender);
    481   memset (&ctx,
    482           0,
    483           sizeof (ctx));
    484   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    485           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id_by_account (
    486             pg,
    487             "exchange-account-does-not-exist",
    488             0,
    489             &reserve_in_cb,
    490             &ctx));
    491   FAILIF (0 != ctx.total);
    492 
    493   /* A callback that gives up stops the iteration but is not an error: the
    494      status stays the number of rows the query matched, not the number the
    495      callback was shown. */
    496   memset (&ctx,
    497           0,
    498           sizeof (ctx));
    499   ctx.stop_after = 1;
    500   FAILIF (3 !=
    501           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
    502                                                                 0,
    503                                                                 &reserve_in_cb,
    504                                                                 &ctx));
    505   FAILIF (1 != ctx.total);
    506   GNUNET_free (ctx.sender);
    507   return 0;
    508 }
    509 
    510 
    511 /**
    512  * The serial number bound of the plain iterator excludes earlier rows.
    513  * The bound is inclusive, so passing the row of the second transfer
    514  * leaves the first one out and nothing else.
    515  *
    516  * @param pg the database context
    517  * @return 0 on success
    518  */
    519 static int
    520 check_serial_bound (struct TALER_EXCHANGEDB_PostgresContext *pg)
    521 {
    522   struct InContext ctx;
    523 
    524   FAILIF (3 != TDB_count (pg,
    525                           "FROM reserves_in"));
    526   memset (&ctx,
    527           0,
    528           sizeof (ctx));
    529   FAILIF (2 !=
    530           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
    531                                                                 2,
    532                                                                 &reserve_in_cb,
    533                                                                 &ctx));
    534   GNUNET_free (ctx.sender);
    535   memset (&ctx,
    536           0,
    537           sizeof (ctx));
    538   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    539           TALER_EXCHANGEDB_iterate_reserves_in_above_serial_id (pg,
    540                                                                 1000,
    541                                                                 &reserve_in_cb,
    542                                                                 &ctx));
    543   GNUNET_free (ctx.sender);
    544   return 0;
    545 }
    546 
    547 
    548 /**
    549  * The AML view of the same rows filters by amount and by account, and can
    550  * walk the table backwards.
    551  *
    552  * @param pg the database context
    553  * @return 0 on success
    554  */
    555 static int
    556 check_credit_transfers (struct TALER_EXCHANGEDB_PostgresContext *pg)
    557 {
    558   struct TALER_NormalizedPayto npayto;
    559   struct TALER_NormalizedPaytoHashP h_payto;
    560   struct TALER_FullPayto payto_a = {
    561     .full_payto = (char *) PAYTO_A
    562   };
    563   struct TALER_Amount zero = TDB_amount ("0");
    564   struct TALER_Amount five = TDB_amount ("5");
    565   struct TALER_Amount huge = TDB_amount ("1000");
    566   struct TransferContext ctx;
    567   uint64_t lowest;
    568 
    569   /* everything, ascending */
    570   memset (&ctx,
    571           0,
    572           sizeof (ctx));
    573   FAILIF (3 !=
    574           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    575                                                               &zero,
    576                                                               0,
    577                                                               10,
    578                                                               NULL,
    579                                                               &transfer_cb,
    580                                                               &ctx));
    581   FAILIF (3 != ctx.total);
    582   FAILIF (10 + 2 + 5 != ctx.value_sum);
    583   lowest = ctx.first_row;
    584 
    585   /* the amount threshold drops the EUR:2 transfer */
    586   memset (&ctx,
    587           0,
    588           sizeof (ctx));
    589   FAILIF (2 !=
    590           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    591                                                               &five,
    592                                                               0,
    593                                                               10,
    594                                                               NULL,
    595                                                               &transfer_cb,
    596                                                               &ctx));
    597   FAILIF (10 + 5 != ctx.value_sum);
    598 
    599   /* a threshold nobody meets */
    600   memset (&ctx,
    601           0,
    602           sizeof (ctx));
    603   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    604           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    605                                                               &huge,
    606                                                               0,
    607                                                               10,
    608                                                               NULL,
    609                                                               &transfer_cb,
    610                                                               &ctx));
    611   FAILIF (0 != ctx.total);
    612 
    613   /* filtering by account keeps the two transfers from PAYTO_A */
    614   npayto = TALER_payto_normalize (payto_a);
    615   GNUNET_assert (NULL != npayto.normalized_payto);
    616   TALER_normalized_payto_hash (npayto,
    617                                &h_payto);
    618   GNUNET_free (npayto.normalized_payto);
    619   memset (&ctx,
    620           0,
    621           sizeof (ctx));
    622   FAILIF (2 !=
    623           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    624                                                               &zero,
    625                                                               0,
    626                                                               10,
    627                                                               &h_payto,
    628                                                               &transfer_cb,
    629                                                               &ctx));
    630   FAILIF (10 + 2 != ctx.value_sum);
    631 
    632   /* the limit caps the result set... */
    633   memset (&ctx,
    634           0,
    635           sizeof (ctx));
    636   FAILIF (1 !=
    637           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    638                                                               &zero,
    639                                                               0,
    640                                                               1,
    641                                                               NULL,
    642                                                               &transfer_cb,
    643                                                               &ctx));
    644   FAILIF (lowest != ctx.first_row);
    645 
    646   /* ...and a negative limit walks the table the other way, starting from
    647      the offset rather than from the beginning */
    648   memset (&ctx,
    649           0,
    650           sizeof (ctx));
    651   FAILIF (3 !=
    652           TALER_EXCHANGEDB_iterate_exchange_credit_transfers (pg,
    653                                                               &zero,
    654                                                               1000,
    655                                                               -10,
    656                                                               NULL,
    657                                                               &transfer_cb,
    658                                                               &ctx));
    659   FAILIF (lowest == ctx.first_row);
    660   FAILIF (10 + 2 + 5 != ctx.value_sum);
    661   return 0;
    662 }
    663 
    664 
    665 /**
    666  * The checks to run, in order.
    667  */
    668 static const struct TDB_Test tests[] = {
    669   { "reserves-in-empty",
    670     &check_empty },
    671   { "reserves-in-import",
    672     &check_import },
    673   { "reserves-in-iterate",
    674     &check_iterate },
    675   { "reserves-in-serial-bound",
    676     &check_serial_bound },
    677   { "reserves-in-credit-transfers",
    678     &check_credit_transfers },
    679   { NULL, NULL }
    680 };
    681 
    682 
    683 int
    684 main (int argc,
    685       char *const *argv)
    686 {
    687   return TDB_main (argc,
    688                    argv,
    689                    "test-reserves-in",
    690                    "Tests for the exchangedb `reserves_in' table",
    691                    tests);
    692 }
    693 
    694 
    695 /* end of test_reserves_in.c */