exchange

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

test_purse_merges.c (21679B)


      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_merges.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `purse_merges`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_purse_merge(),
     23  * #TALER_EXCHANGEDB_get_purse_merge(),
     24  * #TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id(),
     25  * #TALER_EXCHANGEDB_iterate_wallet_merges().
     26  *
     27  * A merge is the exchange acting on the merge capability key: it moves a
     28  * paid-up purse into a reserve.  do_purse_merge() therefore declines when
     29  * the purse is not paid up and when the named partner exchange is unknown,
     30  * and reports a conflict when the purse was already merged elsewhere.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/do_purse_deposit.h"
     34 #include "exchange-database/do_purse_merge.h"
     35 #include "exchange-database/get_purse_merge.h"
     36 #include "exchange-database/get_reserve.h"
     37 #include "exchange-database/iterate_purse_merges_above_serial_id.h"
     38 #include "exchange-database/iterate_wallet_merges.h"
     39 
     40 
     41 /**
     42  * Denomination the checks pay purses with.
     43  */
     44 static struct TDB_Denom denom;
     45 
     46 
     47 /**
     48  * Wallet account the reserve of the checks belongs to.
     49  */
     50 static struct TDB_Account account;
     51 
     52 
     53 /**
     54  * Build a timestamp from a number of seconds since the epoch.
     55  *
     56  * @param secs seconds since the epoch
     57  * @return the timestamp
     58  */
     59 static struct GNUNET_TIME_Timestamp
     60 ts (uint64_t secs)
     61 {
     62   struct GNUNET_TIME_Absolute abs = {
     63     .abs_value_us = secs * 1000LLU * 1000LLU
     64   };
     65 
     66   return GNUNET_TIME_absolute_to_timestamp (abs);
     67 }
     68 
     69 
     70 /**
     71  * Closure for #merge_cb().
     72  */
     73 struct MergeContext
     74 {
     75   /**
     76    * How many rows did the callback see?
     77    */
     78   unsigned int total;
     79 
     80   /**
     81    * Stop after this many rows; 0 for no limit.
     82    */
     83   unsigned int stop_after;
     84 
     85   /**
     86    * Purse we are looking for, NULL to match nothing.
     87    */
     88   const struct TALER_PurseContractPublicKeyP *purse_pub;
     89 
     90   /**
     91    * How many times did we see it?
     92    */
     93   unsigned int matched;
     94 
     95   /**
     96    * Reserve reported for it.
     97    */
     98   struct TALER_ReservePublicKeyP reserve_pub;
     99 
    100   /**
    101    * Amount reported for it.
    102    */
    103   struct TALER_Amount amount;
    104 
    105   /**
    106    * Was a partner exchange reported for it?
    107    */
    108   bool have_partner;
    109 };
    110 
    111 
    112 /**
    113  * Callback for
    114  * #TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id().
    115  *
    116  * @param cls a `struct MergeContext *`
    117  * @param rowid row of the merge
    118  * @param partner_base_url partner exchange, NULL for us
    119  * @param amount target amount of the purse
    120  * @param balance balance of the purse
    121  * @param flags flags of the purse
    122  * @param merge_pub merge capability key
    123  * @param reserve_pub reserve the purse went into
    124  * @param merge_sig signature affirming the merge
    125  * @param purse_pub the purse
    126  * @param merge_timestamp when it was merged
    127  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    128  */
    129 static enum GNUNET_GenericReturnValue
    130 merge_cb (void *cls,
    131           uint64_t rowid,
    132           const char *partner_base_url,
    133           const struct TALER_Amount *amount,
    134           const struct TALER_Amount *balance,
    135           enum TALER_WalletAccountMergeFlags flags,
    136           const struct TALER_PurseMergePublicKeyP *merge_pub,
    137           const struct TALER_ReservePublicKeyP *reserve_pub,
    138           const struct TALER_PurseMergeSignatureP *merge_sig,
    139           const struct TALER_PurseContractPublicKeyP *purse_pub,
    140           struct GNUNET_TIME_Timestamp merge_timestamp)
    141 {
    142   struct MergeContext *ctx = cls;
    143 
    144   (void) rowid;
    145   (void) balance;
    146   (void) flags;
    147   (void) merge_pub;
    148   (void) merge_sig;
    149   (void) merge_timestamp;
    150   ctx->total++;
    151   if ( (NULL != ctx->purse_pub) &&
    152        (0 == GNUNET_memcmp (purse_pub,
    153                             ctx->purse_pub)) )
    154   {
    155     ctx->matched++;
    156     ctx->reserve_pub = *reserve_pub;
    157     ctx->amount = *amount;
    158     ctx->have_partner = (NULL != partner_base_url);
    159   }
    160   if ( (0 != ctx->stop_after) &&
    161        (ctx->total >= ctx->stop_after) )
    162     return GNUNET_SYSERR;
    163   return GNUNET_OK;
    164 }
    165 
    166 
    167 /**
    168  * Closure for #transfer_cb() and #amount_cb().
    169  */
    170 struct SumContext
    171 {
    172   /**
    173    * How many entries did the callback see?
    174    */
    175   unsigned int total;
    176 
    177   /**
    178    * Sum of the whole-unit parts of the amounts seen.
    179    */
    180   uint64_t value_sum;
    181 
    182 };
    183 
    184 
    185 /**
    186  * Callback for #TALER_EXCHANGEDB_iterate_wallet_merges().
    187  *
    188  * @param cls a `struct SumContext *`
    189  * @param row_id row of the merge
    190  * @param payto_uri wallet account involved
    191  * @param execution_time when the merge happened
    192  * @param amount how much was merged
    193  */
    194 static void
    195 transfer_cb (void *cls,
    196              uint64_t row_id,
    197              const char *payto_uri,
    198              struct GNUNET_TIME_Absolute execution_time,
    199              const struct TALER_Amount *amount)
    200 {
    201   struct SumContext *ctx = cls;
    202 
    203   (void) row_id;
    204   (void) payto_uri;
    205   (void) execution_time;
    206   ctx->total++;
    207   ctx->value_sum += amount->value;
    208 }
    209 
    210 
    211 /**
    212  * Outcome of a merge.
    213  */
    214 struct MergeStatus
    215 {
    216   /**
    217    * Was the named partner exchange unknown?
    218    */
    219   bool no_partner;
    220 
    221   /**
    222    * Was the purse not paid up?
    223    */
    224   bool no_balance;
    225 
    226   /**
    227    * Was the purse already merged elsewhere?
    228    */
    229   bool in_conflict;
    230 };
    231 
    232 
    233 /**
    234  * Merge a purse into a reserve.
    235  *
    236  * @param pg the database context
    237  * @param purse purse to merge
    238  * @param reserve_pub reserve to credit
    239  * @param h_payto normalized account of the reserve's wallet
    240  * @param seed seed for the merge and reserve signatures
    241  * @param partner_url partner exchange, NULL for us
    242  * @param[out] st set to the outcome
    243  * @return transaction status
    244  */
    245 static enum GNUNET_DB_QueryStatus
    246 run_merge (struct TALER_EXCHANGEDB_PostgresContext *pg,
    247            const struct TDB_Purse *purse,
    248            const struct TALER_ReservePublicKeyP *reserve_pub,
    249            uint32_t seed,
    250            const char *partner_url,
    251            struct MergeStatus *st)
    252 {
    253   struct TALER_PurseMergeSignatureP merge_sig;
    254   struct TALER_ReserveSignatureP reserve_sig;
    255 
    256   TDB_fill (&merge_sig,
    257             sizeof (merge_sig),
    258             seed);
    259   TDB_fill (&reserve_sig,
    260             sizeof (reserve_sig),
    261             seed);
    262   memset (st,
    263           0,
    264           sizeof (*st));
    265   return TALER_EXCHANGEDB_do_purse_merge (pg,
    266                                           &purse->purse_pub,
    267                                           &merge_sig,
    268                                           ts (1600000000),
    269                                           &reserve_sig,
    270                                           partner_url,
    271                                           reserve_pub,
    272                                           &st->no_partner,
    273                                           &st->no_balance,
    274                                           &st->in_conflict);
    275 }
    276 
    277 
    278 /**
    279  * Pay a purse up to its target amount.
    280  *
    281  * @param pg the database context
    282  * @param purse purse to fill
    283  * @param seed seed for the coin
    284  * @param amount how much to pay in, e.g. "5"
    285  */
    286 static void
    287 fill_purse (struct TALER_EXCHANGEDB_PostgresContext *pg,
    288             const struct TDB_Purse *purse,
    289             uint32_t seed,
    290             const char *amount)
    291 {
    292   struct TALER_CoinPublicInfo coin;
    293   struct TALER_CoinSpendSignatureP coin_sig;
    294   struct TALER_Amount a = TDB_amount (amount);
    295   bool balance_ok;
    296   bool too_late;
    297   bool conflict;
    298 
    299   TDB_coin (pg,
    300             &denom,
    301             seed,
    302             &coin,
    303             NULL);
    304   TDB_fill (&coin_sig,
    305             sizeof (coin_sig),
    306             seed);
    307   GNUNET_assert (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT ==
    308                  TALER_EXCHANGEDB_do_purse_deposit (pg,
    309                                                     &purse->purse_pub,
    310                                                     &coin.coin_pub,
    311                                                     &a,
    312                                                     &coin_sig,
    313                                                     &a,
    314                                                     &balance_ok,
    315                                                     &too_late,
    316                                                     &conflict));
    317   GNUNET_assert (balance_ok);
    318   TDB_coin_free (&coin);
    319 }
    320 
    321 
    322 /**
    323  * Nothing is reported while the table is empty.
    324  *
    325  * @param pg the database context
    326  * @return 0 on success
    327  */
    328 static int
    329 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    330 {
    331   struct TALER_PurseContractPublicKeyP purse_pub;
    332   struct TALER_PurseMergeSignatureP merge_sig;
    333   struct GNUNET_TIME_Timestamp merge_timestamp;
    334   struct TALER_ReservePublicKeyP reserve_pub;
    335   struct MergeContext ctx = { 0 };
    336   struct SumContext sctx = { 0 };
    337   struct TALER_Amount zero = TDB_amount ("0");
    338   char *partner_url = NULL;
    339   bool refunded;
    340 
    341   TDB_denom (pg,
    342              10,
    343              "5",
    344              "0.1",
    345              &denom);
    346   TDB_account (pg,
    347                10,
    348                &account);
    349   TDB_FILL (purse_pub,
    350             1);
    351   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    352           TALER_EXCHANGEDB_get_purse_merge (pg,
    353                                             &purse_pub,
    354                                             &merge_sig,
    355                                             &merge_timestamp,
    356                                             &partner_url,
    357                                             &reserve_pub,
    358                                             &refunded));
    359   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    360           TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id (
    361             pg,
    362             0,
    363             &merge_cb,
    364             &ctx));
    365   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    366           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    367                                                   &zero,
    368                                                   0,
    369                                                   10,
    370                                                   NULL,
    371                                                   &transfer_cb,
    372                                                   &sctx));
    373   FAILIF (0 != ctx.total);
    374   FAILIF (0 != sctx.total);
    375   return 0;
    376 }
    377 
    378 
    379 /**
    380  * A purse that has not been paid up cannot be merged.
    381  *
    382  * @param pg the database context
    383  * @return 0 on success
    384  */
    385 static int
    386 check_no_balance (struct TALER_EXCHANGEDB_PostgresContext *pg)
    387 {
    388   struct TDB_Purse purse;
    389   struct TALER_ReservePublicKeyP reserve_pub;
    390   struct MergeStatus st;
    391 
    392   TDB_purse (pg,
    393              10,
    394              "5",
    395              ts (1700000000),
    396              &purse);
    397   TDB_reserve (pg,
    398                10,
    399                "0",
    400                &reserve_pub);
    401   /* only EUR:1 of the EUR:5 has been paid in */
    402   fill_purse (pg,
    403               &purse,
    404               20,
    405               "1");
    406   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    407           run_merge (pg,
    408                      &purse,
    409                      &reserve_pub,
    410                      10,
    411                      NULL,
    412                      &st));
    413   FAILIF (! st.no_balance);
    414   FAILIF (st.no_partner);
    415   FAILIF (st.in_conflict);
    416   FAILIF (0 != TDB_count (pg,
    417                           "FROM purse_merges"));
    418   return 0;
    419 }
    420 
    421 
    422 /**
    423  * A partner exchange the exchange does not know cannot receive a merge.
    424  *
    425  * @param pg the database context
    426  * @return 0 on success
    427  */
    428 static int
    429 check_no_partner (struct TALER_EXCHANGEDB_PostgresContext *pg)
    430 {
    431   struct TDB_Purse purse;
    432   struct TALER_ReservePublicKeyP reserve_pub;
    433   struct MergeStatus st;
    434 
    435   TDB_purse (pg,
    436              10,
    437              "5",
    438              ts (1700000000),
    439              &purse);
    440   TDB_fill (&reserve_pub,
    441             sizeof (reserve_pub),
    442             10);
    443   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    444           run_merge (pg,
    445                      &purse,
    446                      &reserve_pub,
    447                      10,
    448                      "https://partner.example/",
    449                      &st));
    450   FAILIF (! st.no_partner);
    451   FAILIF (0 != TDB_count (pg,
    452                           "FROM purse_merges"));
    453   return 0;
    454 }
    455 
    456 
    457 /**
    458  * A paid-up purse is merged into the reserve, which is credited.
    459  *
    460  * @param pg the database context
    461  * @return 0 on success
    462  */
    463 static int
    464 check_merge (struct TALER_EXCHANGEDB_PostgresContext *pg)
    465 {
    466   struct TDB_Purse purse;
    467   struct TALER_ReservePublicKeyP reserve_pub;
    468   struct TALER_ReservePublicKeyP got_reserve;
    469   struct TALER_EXCHANGEDB_Reserve reserve;
    470   struct MergeStatus st;
    471   struct MergeContext ctx;
    472   struct TALER_PurseMergeSignatureP merge_sig;
    473   struct TALER_PurseMergeSignatureP expect_sig;
    474   struct GNUNET_TIME_Timestamp merge_timestamp;
    475   struct TALER_Amount expect_balance = TDB_amount ("5");
    476   char *partner_url = NULL;
    477   bool refunded = true;
    478 
    479   TDB_purse (pg,
    480              10,
    481              "5",
    482              ts (1700000000),
    483              &purse);
    484   TDB_fill (&reserve_pub,
    485             sizeof (reserve_pub),
    486             10);
    487   /* pay in the remaining EUR:4 */
    488   fill_purse (pg,
    489               &purse,
    490               21,
    491               "4");
    492   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    493           run_merge (pg,
    494                      &purse,
    495                      &reserve_pub,
    496                      10,
    497                      NULL,
    498                      &st));
    499   FAILIF (st.no_balance);
    500   FAILIF (st.no_partner);
    501   FAILIF (st.in_conflict);
    502   FAILIF (1 != TDB_count (pg,
    503                           "FROM purse_merges"));
    504 
    505   /* the reserve got the money */
    506   memset (&reserve,
    507           0,
    508           sizeof (reserve));
    509   reserve.pub = reserve_pub;
    510   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    511           TALER_EXCHANGEDB_get_reserve (pg,
    512                                         &reserve));
    513   FAILIF (0 != TALER_amount_cmp (&reserve.balance,
    514                                  &expect_balance));
    515 
    516   TDB_FILL (expect_sig,
    517             10);
    518   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    519           TALER_EXCHANGEDB_get_purse_merge (pg,
    520                                             &purse.purse_pub,
    521                                             &merge_sig,
    522                                             &merge_timestamp,
    523                                             &partner_url,
    524                                             &got_reserve,
    525                                             &refunded));
    526   FAILIF (0 != GNUNET_memcmp (&merge_sig,
    527                               &expect_sig));
    528   FAILIF (0 != GNUNET_memcmp (&got_reserve,
    529                               &reserve_pub));
    530   FAILIF (GNUNET_TIME_timestamp_cmp (merge_timestamp,
    531                                      !=,
    532                                      ts (1600000000)));
    533   FAILIF (refunded);
    534   /* the purse stayed with us, so there is no partner exchange */
    535   FAILIF (NULL != partner_url);
    536 
    537   memset (&ctx,
    538           0,
    539           sizeof (ctx));
    540   ctx.purse_pub = &purse.purse_pub;
    541   FAILIF (0 >=
    542           TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id (
    543             pg,
    544             0,
    545             &merge_cb,
    546             &ctx));
    547   FAILIF (1 != ctx.matched);
    548   FAILIF (0 != GNUNET_memcmp (&ctx.reserve_pub,
    549                               &reserve_pub));
    550   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    551                                  &expect_balance));
    552   FAILIF (ctx.have_partner);
    553 
    554   /* merging the same purse into a different reserve is a conflict */
    555   {
    556     struct TALER_ReservePublicKeyP other;
    557 
    558     TDB_reserve (pg,
    559                  12,
    560                  "0",
    561                  &other);
    562     FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    563             run_merge (pg,
    564                        &purse,
    565                        &other,
    566                        12,
    567                        NULL,
    568                        &st));
    569     FAILIF (! st.in_conflict);
    570     FAILIF (1 != TDB_count (pg,
    571                             "FROM purse_merges"));
    572   }
    573   return 0;
    574 }
    575 
    576 
    577 /**
    578  * The wallet's view of its merges reports the purse once the reserve is
    579  * tied to a wallet account.
    580  *
    581  * @param pg the database context
    582  * @return 0 on success
    583  */
    584 static int
    585 check_wallet_merges (struct TALER_EXCHANGEDB_PostgresContext *pg)
    586 {
    587   struct TALER_ReservePublicKeyP reserve_pub;
    588   struct TALER_Amount zero = TDB_amount ("0");
    589   struct TALER_Amount huge = TDB_amount ("1000");
    590   struct SumContext ctx;
    591   char *rhex;
    592   char *nhex;
    593 
    594   /* Until the reserve is known to belong to a wallet account, the view
    595      has nothing to join against. */
    596   memset (&ctx,
    597           0,
    598           sizeof (ctx));
    599   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    600           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    601                                                   &zero,
    602                                                   0,
    603                                                   10,
    604                                                   NULL,
    605                                                   &transfer_cb,
    606                                                   &ctx));
    607   FAILIF (0 != ctx.total);
    608 
    609   TDB_fill (&reserve_pub,
    610             sizeof (reserve_pub),
    611             10);
    612   rhex = TDB_hex (&reserve_pub,
    613                   sizeof (reserve_pub));
    614   nhex = TDB_hex (&account.h_normalized,
    615                   sizeof (account.h_normalized));
    616   FAILIF_C (GNUNET_OK !=
    617             TDB_exec (pg,
    618                       "UPDATE kyc_targets"
    619                       " SET target_pub=decode('%s','hex')"
    620                       "    ,is_wallet=TRUE"
    621                       " WHERE h_normalized_payto=decode('%s','hex');",
    622                       rhex,
    623                       nhex),
    624             GNUNET_free (rhex); GNUNET_free (nhex));
    625   GNUNET_free (rhex);
    626   GNUNET_free (nhex);
    627 
    628   memset (&ctx,
    629           0,
    630           sizeof (ctx));
    631   FAILIF (1 !=
    632           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    633                                                   &zero,
    634                                                   0,
    635                                                   10,
    636                                                   NULL,
    637                                                   &transfer_cb,
    638                                                   &ctx));
    639   FAILIF (5 != ctx.value_sum);
    640 
    641   /* filtering by that very account keeps it */
    642   memset (&ctx,
    643           0,
    644           sizeof (ctx));
    645   FAILIF (1 !=
    646           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    647                                                   &zero,
    648                                                   0,
    649                                                   10,
    650                                                   &account.h_normalized,
    651                                                   &transfer_cb,
    652                                                   &ctx));
    653   FAILIF (1 != ctx.total);
    654 
    655   /* a threshold nobody meets drops it */
    656   memset (&ctx,
    657           0,
    658           sizeof (ctx));
    659   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    660           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    661                                                   &huge,
    662                                                   0,
    663                                                   10,
    664                                                   NULL,
    665                                                   &transfer_cb,
    666                                                   &ctx));
    667   FAILIF (0 != ctx.total);
    668 
    669   /* and the negative limit walks the table the other way */
    670   memset (&ctx,
    671           0,
    672           sizeof (ctx));
    673   FAILIF (1 !=
    674           TALER_EXCHANGEDB_iterate_wallet_merges (pg,
    675                                                   &zero,
    676                                                   1000,
    677                                                   -10,
    678                                                   NULL,
    679                                                   &transfer_cb,
    680                                                   &ctx));
    681   FAILIF (1 != ctx.total);
    682   return 0;
    683 }
    684 
    685 
    686 /**
    687  * The iterators' bounds and abort return behave as documented.
    688  *
    689  * @param pg the database context
    690  * @return 0 on success
    691  */
    692 static int
    693 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    694 {
    695   struct MergeContext ctx;
    696 
    697   memset (&ctx,
    698           0,
    699           sizeof (ctx));
    700   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    701           TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id (
    702             pg,
    703             1000,
    704             &merge_cb,
    705             &ctx));
    706   FAILIF (0 != ctx.total);
    707   memset (&ctx,
    708           0,
    709           sizeof (ctx));
    710   ctx.stop_after = 1;
    711   FAILIF (1 !=
    712           TALER_EXCHANGEDB_iterate_purse_merges_above_serial_id (
    713             pg,
    714             0,
    715             &merge_cb,
    716             &ctx));
    717   FAILIF (1 != ctx.total);
    718   return 0;
    719 }
    720 
    721 
    722 /**
    723  * The checks to run, in order.
    724  */
    725 static const struct TDB_Test tests[] = {
    726   { "purse-merges-empty",
    727     &check_empty },
    728   { "purse-merges-no-balance",
    729     &check_no_balance },
    730   { "purse-merges-no-partner",
    731     &check_no_partner },
    732   { "purse-merges-merge",
    733     &check_merge },
    734   { "purse-merges-wallet-merges",
    735     &check_wallet_merges },
    736   { "purse-merges-iterate",
    737     &check_iterate },
    738   { NULL, NULL }
    739 };
    740 
    741 
    742 int
    743 main (int argc,
    744       char *const *argv)
    745 {
    746   int ret;
    747 
    748   ret = TDB_main (argc,
    749                   argv,
    750                   "test-purse-merges",
    751                   "Tests for the exchangedb `purse_merges' table",
    752                   tests);
    753   TDB_account_free (&account);
    754   TDB_denom_free (&denom);
    755   return ret;
    756 }
    757 
    758 
    759 /* end of test_purse_merges.c */