exchange

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

test_account_merges.c (20150B)


      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_account_merges.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `account_merges`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_do_reserve_purse(),
     23  * #TALER_EXCHANGEDB_iterate_account_merges_above_serial_id() and
     24  * #TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check().
     25  *
     26  * `account_merges` is the account holder's side of a merge: the request
     27  * the wallet signs to have a purse land in its reserve.  do_reserve_purse()
     28  * writes it (together with the `purse_merges` row) and declines when the
     29  * reserve is unknown, when its purse quota is used up, and when the purse
     30  * was already merged into a different reserve.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/do_reserve_purse.h"
     34 #include "exchange-database/insert_purse_request.h"
     35 #include "exchange-database/iterate_account_merges_above_serial_id.h"
     36 #include "exchange-database/iterate_merge_amounts_for_kyc_check.h"
     37 #include "exchange-database/rollback.h"
     38 #include "exchange-database/start.h"
     39 
     40 
     41 /**
     42  * Wallet account the reserves of the checks belong to.
     43  */
     44 static struct TDB_Account account;
     45 
     46 
     47 /**
     48  * Build a timestamp from a number of seconds since the epoch.
     49  *
     50  * @param secs seconds since the epoch
     51  * @return the timestamp
     52  */
     53 static struct GNUNET_TIME_Timestamp
     54 ts (uint64_t secs)
     55 {
     56   struct GNUNET_TIME_Absolute abs = {
     57     .abs_value_us = secs * 1000LLU * 1000LLU
     58   };
     59 
     60   return GNUNET_TIME_absolute_to_timestamp (abs);
     61 }
     62 
     63 
     64 /**
     65  * Closure for #merge_cb().
     66  */
     67 struct MergeContext
     68 {
     69   /**
     70    * How many rows did the callback see?
     71    */
     72   unsigned int total;
     73 
     74   /**
     75    * Stop after this many rows; 0 for no limit.
     76    */
     77   unsigned int stop_after;
     78 
     79   /**
     80    * Purse we are looking for, NULL to match nothing.
     81    */
     82   const struct TALER_PurseContractPublicKeyP *purse_pub;
     83 
     84   /**
     85    * How many times did we see it?
     86    */
     87   unsigned int matched;
     88 
     89   /**
     90    * Reserve reported for it.
     91    */
     92   struct TALER_ReservePublicKeyP reserve_pub;
     93 
     94   /**
     95    * Amount reported for it.
     96    */
     97   struct TALER_Amount amount;
     98 
     99   /**
    100    * Purse fee reported for it.
    101    */
    102   struct TALER_Amount purse_fee;
    103 };
    104 
    105 
    106 /**
    107  * Callback for
    108  * #TALER_EXCHANGEDB_iterate_account_merges_above_serial_id().
    109  *
    110  * @param cls a `struct MergeContext *`
    111  * @param rowid row of the request
    112  * @param reserve_pub reserve the purse is to land in
    113  * @param purse_pub the purse
    114  * @param h_contract_terms contract of the purse
    115  * @param purse_expiration when the purse expires
    116  * @param amount target amount of the purse
    117  * @param min_age age limit of the purse
    118  * @param flags flags of the purse
    119  * @param purse_fee fee charged to the reserve for the purse
    120  * @param merge_timestamp when the request was made
    121  * @param reserve_sig signature of the account holder
    122  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    123  */
    124 static enum GNUNET_GenericReturnValue
    125 merge_cb (void *cls,
    126           uint64_t rowid,
    127           const struct TALER_ReservePublicKeyP *reserve_pub,
    128           const struct TALER_PurseContractPublicKeyP *purse_pub,
    129           const struct TALER_PrivateContractHashP *h_contract_terms,
    130           struct GNUNET_TIME_Timestamp purse_expiration,
    131           const struct TALER_Amount *amount,
    132           uint32_t min_age,
    133           enum TALER_WalletAccountMergeFlags flags,
    134           const struct TALER_Amount *purse_fee,
    135           struct GNUNET_TIME_Timestamp merge_timestamp,
    136           const struct TALER_ReserveSignatureP *reserve_sig)
    137 {
    138   struct MergeContext *ctx = cls;
    139 
    140   (void) rowid;
    141   (void) h_contract_terms;
    142   (void) purse_expiration;
    143   (void) min_age;
    144   (void) flags;
    145   (void) merge_timestamp;
    146   (void) reserve_sig;
    147   ctx->total++;
    148   if ( (NULL != ctx->purse_pub) &&
    149        (0 == GNUNET_memcmp (purse_pub,
    150                             ctx->purse_pub)) )
    151   {
    152     ctx->matched++;
    153     ctx->reserve_pub = *reserve_pub;
    154     ctx->amount = *amount;
    155     ctx->purse_fee = *purse_fee;
    156   }
    157   if ( (0 != ctx->stop_after) &&
    158        (ctx->total >= ctx->stop_after) )
    159     return GNUNET_SYSERR;
    160   return GNUNET_OK;
    161 }
    162 
    163 
    164 /**
    165  * Closure for #amount_cb().
    166  */
    167 struct AmountContext
    168 {
    169   /**
    170    * How many amounts did the callback see?
    171    */
    172   unsigned int total;
    173 
    174   /**
    175    * Sum of the whole-unit parts of the amounts seen.
    176    */
    177   uint64_t value_sum;
    178 
    179   /**
    180    * Return this from the callback.
    181    */
    182   enum GNUNET_GenericReturnValue ret;
    183 };
    184 
    185 
    186 /**
    187  * Callback for
    188  * #TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check().
    189  *
    190  * @param cls a `struct AmountContext *`
    191  * @param amount how much was merged
    192  * @param date when it was merged
    193  * @return what @e ret of the closure says
    194  */
    195 static enum GNUNET_GenericReturnValue
    196 amount_cb (void *cls,
    197            const struct TALER_Amount *amount,
    198            struct GNUNET_TIME_Absolute date)
    199 {
    200   struct AmountContext *ctx = cls;
    201 
    202   (void) date;
    203   ctx->total++;
    204   ctx->value_sum += amount->value;
    205   return ctx->ret;
    206 }
    207 
    208 
    209 /**
    210  * Outcome of a reserve-purse request.
    211  */
    212 struct PurseStatus
    213 {
    214   /**
    215    * Was the purse already merged into another reserve?
    216    */
    217   bool in_conflict;
    218 
    219   /**
    220    * Is the reserve unknown?
    221    */
    222   bool no_reserve;
    223 
    224   /**
    225    * Has the reserve run out of purses?
    226    */
    227   bool insufficient_funds;
    228 };
    229 
    230 
    231 /**
    232  * Ask for a purse to be merged into a reserve.
    233  *
    234  * @param pg the database context
    235  * @param purse purse to merge
    236  * @param reserve_pub reserve to merge it into
    237  * @param seed seed for the signatures
    238  * @param purse_fee fee to charge the reserve, NULL to use the quota
    239  * @param[out] st set to the outcome
    240  * @return transaction status
    241  */
    242 static enum GNUNET_DB_QueryStatus
    243 run_reserve_purse (struct TALER_EXCHANGEDB_PostgresContext *pg,
    244                    const struct TDB_Purse *purse,
    245                    const struct TALER_ReservePublicKeyP *reserve_pub,
    246                    uint32_t seed,
    247                    const struct TALER_Amount *purse_fee,
    248                    struct PurseStatus *st)
    249 {
    250   struct TALER_PurseMergeSignatureP merge_sig;
    251   struct TALER_ReserveSignatureP reserve_sig;
    252 
    253   TDB_fill (&merge_sig,
    254             sizeof (merge_sig),
    255             seed);
    256   TDB_fill (&reserve_sig,
    257             sizeof (reserve_sig),
    258             seed);
    259   memset (st,
    260           0,
    261           sizeof (*st));
    262   return TALER_EXCHANGEDB_do_reserve_purse (pg,
    263                                             &purse->purse_pub,
    264                                             &merge_sig,
    265                                             ts (1600000000),
    266                                             &reserve_sig,
    267                                             purse_fee,
    268                                             reserve_pub,
    269                                             &st->in_conflict,
    270                                             &st->no_reserve,
    271                                             &st->insufficient_funds);
    272 }
    273 
    274 
    275 /**
    276  * Create a purse that records a purse fee, the way the /reserves/$RP/purse
    277  * endpoint does when the account holder offers to pay for the purse.
    278  * TDB_purse() always records a zero fee, and the auditor's view reports
    279  * the fee the *purse* recorded, not the one do_reserve_purse() charged.
    280  *
    281  * @param pg the database context
    282  * @param seed seed for the purse's keys and signature
    283  * @param amount target amount of the purse, e.g. "5"
    284  * @param fee purse fee the account holder offers, e.g. "1"
    285  * @param[out] purse set to the purse
    286  */
    287 static void
    288 make_purse (struct TALER_EXCHANGEDB_PostgresContext *pg,
    289             uint32_t seed,
    290             const char *amount,
    291             const char *fee,
    292             struct TDB_Purse *purse)
    293 {
    294   struct TALER_Amount purse_fee = TDB_amount (fee);
    295   enum GNUNET_DB_QueryStatus qs;
    296   bool in_conflict = false;
    297 
    298   memset (purse,
    299           0,
    300           sizeof (*purse));
    301   TDB_fill (&purse->purse_pub,
    302             sizeof (purse->purse_pub),
    303             seed);
    304   TDB_fill (&purse->merge_pub,
    305             sizeof (purse->merge_pub),
    306             seed);
    307   TDB_fill (&purse->h_contract_terms,
    308             sizeof (purse->h_contract_terms),
    309             seed);
    310   TDB_fill (&purse->purse_sig,
    311             sizeof (purse->purse_sig),
    312             seed);
    313   purse->purse_expiration = ts (1700000000);
    314   purse->amount = TDB_amount (amount);
    315   qs = TALER_EXCHANGEDB_insert_purse_request (
    316     pg,
    317     &purse->purse_pub,
    318     &purse->merge_pub,
    319     purse->purse_expiration,
    320     &purse->h_contract_terms,
    321     0,
    322     TALER_WAMF_MODE_CREATE_WITH_PURSE_FEE,
    323     &purse_fee,
    324     &purse->amount,
    325     &purse->purse_sig,
    326     &in_conflict);
    327   GNUNET_assert (0 <= qs);
    328   GNUNET_assert (! in_conflict);
    329 }
    330 
    331 
    332 /**
    333  * Nothing is reported while the table is empty.
    334  *
    335  * @param pg the database context
    336  * @return 0 on success
    337  */
    338 static int
    339 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    340 {
    341   struct MergeContext ctx = { 0 };
    342   struct AmountContext actx = { 0 };
    343 
    344   TDB_account (pg,
    345                10,
    346                &account);
    347   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    348           TALER_EXCHANGEDB_iterate_account_merges_above_serial_id (
    349             pg,
    350             0,
    351             &merge_cb,
    352             &ctx));
    353   actx.ret = GNUNET_OK;
    354   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    355           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    356             pg,
    357             &account.h_normalized,
    358             GNUNET_TIME_UNIT_ZERO_ABS,
    359             &amount_cb,
    360             &actx));
    361   FAILIF (0 != ctx.total);
    362   FAILIF (0 != actx.total);
    363   return 0;
    364 }
    365 
    366 
    367 /**
    368  * A reserve that does not exist cannot take a purse.
    369  *
    370  * @param pg the database context
    371  * @return 0 on success
    372  */
    373 static int
    374 check_no_reserve (struct TALER_EXCHANGEDB_PostgresContext *pg)
    375 {
    376   struct TDB_Purse purse;
    377   struct TALER_ReservePublicKeyP reserve_pub;
    378   struct TALER_Amount purse_fee = TDB_amount ("1");
    379   struct PurseStatus st;
    380 
    381   TDB_purse (pg,
    382              9,
    383              "5",
    384              ts (1700000000),
    385              &purse);
    386   TDB_FILL (reserve_pub,
    387             99);
    388   /* A non-zero purse fee is what makes an unknown reserve fatal: with a
    389      zero fee the function creates the reserve instead.  The `purse_merges`
    390      row goes in before the reserve is looked at, so the caller has to roll
    391      back -- which is what the /purses/.../merge handler does. */
    392   FAILIF (GNUNET_OK !=
    393           TALER_EXCHANGEDB_start (pg,
    394                                   "test-account-merges-no-reserve"));
    395   FAILIF_C (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    396             run_reserve_purse (pg,
    397                                &purse,
    398                                &reserve_pub,
    399                                9,
    400                                &purse_fee,
    401                                &st),
    402             TALER_EXCHANGEDB_rollback (pg));
    403   TALER_EXCHANGEDB_rollback (pg);
    404   FAILIF (! st.no_reserve);
    405   FAILIF (! st.insufficient_funds);
    406   FAILIF (0 != TDB_count (pg,
    407                           "FROM account_merges"));
    408   FAILIF (0 != TDB_count (pg,
    409                           "FROM purse_merges"));
    410   return 0;
    411 }
    412 
    413 
    414 /**
    415  * Paying the purse fee from the reserve creates the merge request.
    416  *
    417  * @param pg the database context
    418  * @return 0 on success
    419  */
    420 static int
    421 check_reserve_purse (struct TALER_EXCHANGEDB_PostgresContext *pg)
    422 {
    423   struct TDB_Purse purse;
    424   struct TALER_ReservePublicKeyP reserve_pub;
    425   struct TALER_Amount purse_fee = TDB_amount ("1");
    426   struct TALER_Amount expect_balance = TDB_amount ("9");
    427   struct MergeContext ctx;
    428   struct PurseStatus st;
    429   char *hex;
    430 
    431   make_purse (pg,
    432               10,
    433               "5",
    434               "1",
    435               &purse);
    436   TDB_reserve (pg,
    437                10,
    438                "10",
    439                &reserve_pub);
    440   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    441           run_reserve_purse (pg,
    442                              &purse,
    443                              &reserve_pub,
    444                              10,
    445                              &purse_fee,
    446                              &st));
    447   FAILIF (st.no_reserve);
    448   FAILIF (st.in_conflict);
    449   FAILIF (st.insufficient_funds);
    450   FAILIF (1 != TDB_count (pg,
    451                           "FROM account_merges"));
    452   /* the merge is also recorded from the purse's side */
    453   FAILIF (1 != TDB_count (pg,
    454                           "FROM purse_merges"));
    455   /* the reserve paid the purse fee */
    456   hex = TDB_hex (&reserve_pub,
    457                  sizeof (reserve_pub));
    458   FAILIF_C (1 != TDB_count (pg,
    459                             "FROM reserves"
    460                             " WHERE reserve_pub=decode('%s','hex')"
    461                             "   AND current_balance=ROW(9,0)::taler_amount",
    462                             hex),
    463             GNUNET_free (hex));
    464   GNUNET_free (hex);
    465   (void) expect_balance;
    466 
    467   memset (&ctx,
    468           0,
    469           sizeof (ctx));
    470   ctx.purse_pub = &purse.purse_pub;
    471   FAILIF (0 >=
    472           TALER_EXCHANGEDB_iterate_account_merges_above_serial_id (
    473             pg,
    474             0,
    475             &merge_cb,
    476             &ctx));
    477   FAILIF (1 != ctx.matched);
    478   FAILIF (0 != GNUNET_memcmp (&ctx.reserve_pub,
    479                               &reserve_pub));
    480   FAILIF (0 != TALER_amount_cmp (&ctx.amount,
    481                                  &purse.amount));
    482   FAILIF (0 != TALER_amount_cmp (&ctx.purse_fee,
    483                                  &purse_fee));
    484 
    485   /* the same purse into a different reserve is a conflict */
    486   {
    487     struct TALER_ReservePublicKeyP other;
    488 
    489     TDB_reserve (pg,
    490                  11,
    491                  "10",
    492                  &other);
    493     FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    494             run_reserve_purse (pg,
    495                                &purse,
    496                                &other,
    497                                11,
    498                                &purse_fee,
    499                                &st));
    500     FAILIF (! st.in_conflict);
    501     FAILIF (1 != TDB_count (pg,
    502                             "FROM account_merges"));
    503   }
    504   return 0;
    505 }
    506 
    507 
    508 /**
    509  * A reserve that cannot pay the purse fee is refused.
    510  *
    511  * @param pg the database context
    512  * @return 0 on success
    513  */
    514 static int
    515 check_insufficient_funds (struct TALER_EXCHANGEDB_PostgresContext *pg)
    516 {
    517   struct TDB_Purse purse;
    518   struct TALER_ReservePublicKeyP reserve_pub;
    519   struct TALER_Amount purse_fee = TDB_amount ("5");
    520   struct PurseStatus st;
    521 
    522   TDB_purse (pg,
    523              12,
    524              "5",
    525              ts (1700000000),
    526              &purse);
    527   TDB_reserve (pg,
    528                12,
    529                "1",
    530                &reserve_pub);
    531   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    532           run_reserve_purse (pg,
    533                              &purse,
    534                              &reserve_pub,
    535                              12,
    536                              &purse_fee,
    537                              &st));
    538   FAILIF (! st.insufficient_funds);
    539   FAILIF (1 != TDB_count (pg,
    540                           "FROM account_merges"));
    541   return 0;
    542 }
    543 
    544 
    545 /**
    546  * The wallet's KYC view sums the merges that landed in its account.
    547  *
    548  * The account in question is the reserve's own `taler-reserve://` account,
    549  * which is what do_reserve_purse() records in `wallet_h_payto` -- not a
    550  * bank account.
    551  *
    552  * @param pg the database context
    553  * @return 0 on success
    554  */
    555 static int
    556 check_kyc_amounts (struct TALER_EXCHANGEDB_PostgresContext *pg)
    557 {
    558   struct AmountContext ctx;
    559   struct TALER_ReservePublicKeyP reserve_pub;
    560   struct TALER_NormalizedPayto payto;
    561   struct TALER_NormalizedPaytoHashP h_payto;
    562   struct TALER_NormalizedPaytoHashP other;
    563 
    564   TDB_fill (&reserve_pub,
    565             sizeof (reserve_pub),
    566             10);
    567   payto = TALER_reserve_make_payto (pg->exchange_url,
    568                                     &reserve_pub);
    569   TALER_normalized_payto_hash (payto,
    570                                &h_payto);
    571   GNUNET_free (payto.normalized_payto);
    572 
    573   /* the merge of check_reserve_purse() is charged to our account, but the
    574      purse has not been decided yet, so the KYC view has nothing */
    575   memset (&ctx,
    576           0,
    577           sizeof (ctx));
    578   ctx.ret = GNUNET_OK;
    579   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    580           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    581             pg,
    582             &h_payto,
    583             GNUNET_TIME_UNIT_ZERO_ABS,
    584             &amount_cb,
    585             &ctx));
    586   FAILIF (0 != ctx.total);
    587 
    588   /* once the purse is decided in favour of the reserve, it counts */
    589   FAILIF (GNUNET_OK !=
    590           TDB_exec (pg,
    591                     "INSERT INTO purse_decision"
    592                     " (purse_pub,action_timestamp,refunded)"
    593                     " SELECT purse_pub,1000000,FALSE"
    594                     "   FROM account_merges;"));
    595   memset (&ctx,
    596           0,
    597           sizeof (ctx));
    598   ctx.ret = GNUNET_OK;
    599   FAILIF (0 >=
    600           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    601             pg,
    602             &h_payto,
    603             GNUNET_TIME_UNIT_ZERO_ABS,
    604             &amount_cb,
    605             &ctx));
    606   FAILIF (0 == ctx.total);
    607   FAILIF (0 == ctx.value_sum);
    608 
    609   /* an account nobody merged into has nothing */
    610   TDB_FILL (other,
    611             97);
    612   memset (&ctx,
    613           0,
    614           sizeof (ctx));
    615   ctx.ret = GNUNET_OK;
    616   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    617           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    618             pg,
    619             &other,
    620             GNUNET_TIME_UNIT_ZERO_ABS,
    621             &amount_cb,
    622             &ctx));
    623   FAILIF (0 != ctx.total);
    624 
    625   /* a time limit past the merges hides them */
    626   memset (&ctx,
    627           0,
    628           sizeof (ctx));
    629   ctx.ret = GNUNET_OK;
    630   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    631           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    632             pg,
    633             &h_payto,
    634             ts (1700000000).abs_time,
    635             &amount_cb,
    636             &ctx));
    637   FAILIF (0 != ctx.total);
    638 
    639   /* and an aborting callback is not an error */
    640   memset (&ctx,
    641           0,
    642           sizeof (ctx));
    643   ctx.ret = GNUNET_NO;
    644   FAILIF (0 >
    645           TALER_EXCHANGEDB_iterate_merge_amounts_for_kyc_check (
    646             pg,
    647             &h_payto,
    648             GNUNET_TIME_UNIT_ZERO_ABS,
    649             &amount_cb,
    650             &ctx));
    651   FAILIF (1 != ctx.total);
    652   return 0;
    653 }
    654 
    655 
    656 /**
    657  * The iterator's bound and abort return behave as documented.
    658  *
    659  * @param pg the database context
    660  * @return 0 on success
    661  */
    662 static int
    663 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    664 {
    665   struct MergeContext ctx;
    666 
    667   memset (&ctx,
    668           0,
    669           sizeof (ctx));
    670   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    671           TALER_EXCHANGEDB_iterate_account_merges_above_serial_id (
    672             pg,
    673             1000,
    674             &merge_cb,
    675             &ctx));
    676   FAILIF (0 != ctx.total);
    677   memset (&ctx,
    678           0,
    679           sizeof (ctx));
    680   ctx.stop_after = 1;
    681   FAILIF (0 >=
    682           TALER_EXCHANGEDB_iterate_account_merges_above_serial_id (
    683             pg,
    684             0,
    685             &merge_cb,
    686             &ctx));
    687   FAILIF (1 != ctx.total);
    688   return 0;
    689 }
    690 
    691 
    692 /**
    693  * The checks to run, in order.
    694  */
    695 static const struct TDB_Test tests[] = {
    696   { "account-merges-empty",
    697     &check_empty },
    698   { "account-merges-no-reserve",
    699     &check_no_reserve },
    700   { "account-merges-reserve-purse",
    701     &check_reserve_purse },
    702   { "account-merges-insufficient-funds",
    703     &check_insufficient_funds },
    704   { "account-merges-kyc-amounts",
    705     &check_kyc_amounts },
    706   { "account-merges-iterate",
    707     &check_iterate },
    708   { NULL, NULL }
    709 };
    710 
    711 
    712 int
    713 main (int argc,
    714       char *const *argv)
    715 {
    716   int ret;
    717 
    718   ret = TDB_main (argc,
    719                   argv,
    720                   "test-account-merges",
    721                   "Tests for the exchangedb `account_merges' table",
    722                   tests);
    723   TDB_account_free (&account);
    724   return ret;
    725 }
    726 
    727 
    728 /* end of test_account_merges.c */