exchange

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

test_wire_out.c (18528B)


      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_wire_out.c
     18  * @brief tests for the exchangedb functions whose primary table is
     19  *        `wire_out`
     20  * @author Christian Grothoff
     21  *
     22  * Covers #TALER_EXCHANGEDB_insert_wire_out(),
     23  * #TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id(),
     24  * #TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id_by_account(),
     25  * #TALER_EXCHANGEDB_iterate_exchange_debit_transfers() and
     26  * #TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check().
     27  *
     28  * `wire_out` records the transfers the exchange actually made.  It
     29  * references `wire_targets` for the receiver, which TDB_account() creates;
     30  * the exchange's own account is stored as a plain string.
     31  */
     32 #include "test_common.h"
     33 #include "exchange-database/insert_wire_out.h"
     34 #include "exchange-database/iterate_aggregation_amounts_for_kyc_check.h"
     35 #include "exchange-database/iterate_exchange_debit_transfers.h"
     36 #include "exchange-database/iterate_wire_outs_above_serial_id.h"
     37 #include "exchange-database/iterate_wire_outs_above_serial_id_by_account.h"
     38 
     39 
     40 /**
     41  * Account the checks pay out to.
     42  */
     43 static struct TDB_Account account;
     44 
     45 
     46 /**
     47  * A second account, to show the filters are per account.
     48  */
     49 static struct TDB_Account other;
     50 
     51 
     52 /**
     53  * Build a timestamp from a number of seconds since the epoch.
     54  *
     55  * @param secs seconds since the epoch
     56  * @return the timestamp
     57  */
     58 static struct GNUNET_TIME_Timestamp
     59 ts (uint64_t secs)
     60 {
     61   struct GNUNET_TIME_Absolute abs = {
     62     .abs_value_us = secs * 1000LLU * 1000LLU
     63   };
     64 
     65   return GNUNET_TIME_absolute_to_timestamp (abs);
     66 }
     67 
     68 
     69 /**
     70  * Closure for #wire_out_cb().
     71  */
     72 struct OutContext
     73 {
     74   /**
     75    * How many rows did the callback see?
     76    */
     77   unsigned int total;
     78 
     79   /**
     80    * Stop after this many rows; 0 for no limit.
     81    */
     82   unsigned int stop_after;
     83 
     84   /**
     85    * Wire transfer we are looking for, NULL to match nothing.
     86    */
     87   const struct TALER_WireTransferIdentifierRawP *wtid;
     88 
     89   /**
     90    * How many times did we see it?
     91    */
     92   unsigned int matched;
     93 
     94   /**
     95    * Amount reported for it.
     96    */
     97   struct TALER_Amount amount;
     98 
     99   /**
    100    * Receiver account reported for it, owned by this struct.
    101    */
    102   char *payto;
    103 };
    104 
    105 
    106 /**
    107  * Callback for the `wire_out` iterators.
    108  *
    109  * @param cls a `struct OutContext *`
    110  * @param rowid row of the transfer
    111  * @param date when it was made
    112  * @param wtid its wire transfer identifier
    113  * @param payto_uri where the money went
    114  * @param amount how much was transferred
    115  * @return #GNUNET_OK to continue, #GNUNET_SYSERR to stop
    116  */
    117 static enum GNUNET_GenericReturnValue
    118 wire_out_cb (void *cls,
    119              uint64_t rowid,
    120              struct GNUNET_TIME_Timestamp date,
    121              const struct TALER_WireTransferIdentifierRawP *wtid,
    122              const struct TALER_FullPayto payto_uri,
    123              const struct TALER_Amount *amount)
    124 {
    125   struct OutContext *ctx = cls;
    126 
    127   (void) rowid;
    128   (void) date;
    129   ctx->total++;
    130   if ( (NULL != ctx->wtid) &&
    131        (0 == GNUNET_memcmp (wtid,
    132                             ctx->wtid)) )
    133   {
    134     ctx->matched++;
    135     ctx->amount = *amount;
    136     GNUNET_free (ctx->payto);
    137     ctx->payto = GNUNET_strdup (payto_uri.full_payto);
    138   }
    139   if ( (0 != ctx->stop_after) &&
    140        (ctx->total >= ctx->stop_after) )
    141     return GNUNET_SYSERR;
    142   return GNUNET_OK;
    143 }
    144 
    145 
    146 /**
    147  * Closure for #transfer_cb().
    148  */
    149 struct TransferContext
    150 {
    151   /**
    152    * How many transfers did the callback see?
    153    */
    154   unsigned int total;
    155 
    156   /**
    157    * Row of the first transfer seen.
    158    */
    159   uint64_t first_row;
    160 
    161   /**
    162    * Sum of the whole-unit parts of the amounts seen.
    163    */
    164   uint64_t value_sum;
    165 };
    166 
    167 
    168 /**
    169  * Callback for #TALER_EXCHANGEDB_iterate_exchange_debit_transfers().
    170  *
    171  * @param cls a `struct TransferContext *`
    172  * @param row_id row of the transfer
    173  * @param payto_uri where the money went
    174  * @param execution_time when it was made
    175  * @param amount how much was transferred
    176  */
    177 static void
    178 transfer_cb (void *cls,
    179              uint64_t row_id,
    180              const char *payto_uri,
    181              struct GNUNET_TIME_Absolute execution_time,
    182              const struct TALER_Amount *amount)
    183 {
    184   struct TransferContext *ctx = cls;
    185 
    186   (void) payto_uri;
    187   (void) execution_time;
    188   if (0 == ctx->total++)
    189     ctx->first_row = row_id;
    190   ctx->value_sum += amount->value;
    191 }
    192 
    193 
    194 /**
    195  * Closure for #amount_cb().
    196  */
    197 struct AmountContext
    198 {
    199   /**
    200    * How many amounts did the callback see?
    201    */
    202   unsigned int total;
    203 
    204   /**
    205    * Sum of the whole-unit parts of the amounts seen.
    206    */
    207   uint64_t value_sum;
    208 
    209   /**
    210    * Return this from the callback.
    211    */
    212   enum GNUNET_GenericReturnValue ret;
    213 };
    214 
    215 
    216 /**
    217  * Callback for
    218  * #TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check().
    219  *
    220  * @param cls a `struct AmountContext *`
    221  * @param amount the transferred amount
    222  * @param date when it was transferred
    223  * @return what @e ret of the closure says
    224  */
    225 static enum GNUNET_GenericReturnValue
    226 amount_cb (void *cls,
    227            const struct TALER_Amount *amount,
    228            struct GNUNET_TIME_Absolute date)
    229 {
    230   struct AmountContext *ctx = cls;
    231 
    232   (void) date;
    233   ctx->total++;
    234   ctx->value_sum += amount->value;
    235   return ctx->ret;
    236 }
    237 
    238 
    239 /**
    240  * Record an executed wire transfer.
    241  *
    242  * @param pg the database context
    243  * @param acc account the money went to
    244  * @param seed seed for the wire transfer identifier
    245  * @param when when the transfer was made, in seconds since the epoch
    246  * @param amount how much was transferred, e.g. "10"
    247  * @param metadata extra wire subject metadata, NULL for none
    248  * @param[out] wtid set to the wire transfer identifier used
    249  * @return transaction status
    250  */
    251 static enum GNUNET_DB_QueryStatus
    252 add_wire_out (struct TALER_EXCHANGEDB_PostgresContext *pg,
    253               const struct TDB_Account *acc,
    254               uint32_t seed,
    255               uint64_t when,
    256               const char *amount,
    257               const char *metadata,
    258               struct TALER_WireTransferIdentifierRawP *wtid)
    259 {
    260   struct TALER_FullPayto exchange_payto = {
    261     .full_payto = (char *) "payto://x-taler-bank/localhost/exchange"
    262   };
    263   struct TALER_Amount a = TDB_amount (amount);
    264 
    265   TDB_fill (wtid,
    266             sizeof (*wtid),
    267             seed);
    268   return TALER_EXCHANGEDB_insert_wire_out (pg,
    269                                            ts (when),
    270                                            wtid,
    271                                            &acc->h_full,
    272                                            "exchange-account-1",
    273                                            exchange_payto,
    274                                            &a,
    275                                            metadata);
    276 }
    277 
    278 
    279 /**
    280  * Nothing is reported while the table is empty.
    281  *
    282  * @param pg the database context
    283  * @return 0 on success
    284  */
    285 static int
    286 check_empty (struct TALER_EXCHANGEDB_PostgresContext *pg)
    287 {
    288   struct OutContext ctx = { 0 };
    289   struct TransferContext tctx = { 0 };
    290   struct AmountContext actx = { 0 };
    291   struct TALER_Amount zero = TDB_amount ("0");
    292 
    293   TDB_account (pg,
    294                10,
    295                &account);
    296   TDB_account (pg,
    297                11,
    298                &other);
    299   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    300           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id (pg,
    301                                                               0,
    302                                                               &wire_out_cb,
    303                                                               &ctx));
    304   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    305           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id_by_account (
    306             pg,
    307             "exchange-account-1",
    308             0,
    309             &wire_out_cb,
    310             &ctx));
    311   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    312           TALER_EXCHANGEDB_iterate_exchange_debit_transfers (pg,
    313                                                              &zero,
    314                                                              0,
    315                                                              10,
    316                                                              NULL,
    317                                                              &transfer_cb,
    318                                                              &tctx));
    319   actx.ret = GNUNET_OK;
    320   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    321           TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check (
    322             pg,
    323             &account.h_normalized,
    324             GNUNET_TIME_UNIT_ZERO_ABS,
    325             &amount_cb,
    326             &actx));
    327   FAILIF (0 != ctx.total);
    328   FAILIF (0 != tctx.total);
    329   FAILIF (0 != actx.total);
    330   return 0;
    331 }
    332 
    333 
    334 /**
    335  * A recorded transfer comes back with what it was recorded with.
    336  *
    337  * @param pg the database context
    338  * @return 0 on success
    339  */
    340 static int
    341 check_insert (struct TALER_EXCHANGEDB_PostgresContext *pg)
    342 {
    343   struct TALER_WireTransferIdentifierRawP wtid;
    344   struct OutContext ctx;
    345   struct TALER_Amount expect = TDB_amount ("10");
    346 
    347   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    348           add_wire_out (pg,
    349                         &account,
    350                         10,
    351                         1600000000,
    352                         "10",
    353                         NULL,
    354                         &wtid));
    355   FAILIF (1 != TDB_count (pg,
    356                           "FROM wire_out"));
    357   memset (&ctx,
    358           0,
    359           sizeof (ctx));
    360   ctx.wtid = &wtid;
    361   FAILIF (0 >=
    362           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id (pg,
    363                                                               0,
    364                                                               &wire_out_cb,
    365                                                               &ctx));
    366   FAILIF_C (1 != ctx.matched,
    367             GNUNET_free (ctx.payto));
    368   FAILIF_C (0 != TALER_amount_cmp (&ctx.amount,
    369                                    &expect),
    370             GNUNET_free (ctx.payto));
    371   FAILIF_C (0 != strcmp (ctx.payto,
    372                          account.payto.full_payto),
    373             GNUNET_free (ctx.payto));
    374   GNUNET_free (ctx.payto);
    375   return 0;
    376 }
    377 
    378 
    379 /**
    380  * The by-account iterator filters on the exchange's own bank account, and
    381  * the serial bound and the abort return behave as documented.
    382  *
    383  * @param pg the database context
    384  * @return 0 on success
    385  */
    386 static int
    387 check_iterate (struct TALER_EXCHANGEDB_PostgresContext *pg)
    388 {
    389   struct TALER_WireTransferIdentifierRawP w2;
    390   struct TALER_WireTransferIdentifierRawP w3;
    391   struct OutContext ctx;
    392 
    393   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    394           add_wire_out (pg,
    395                         &account,
    396                         11,
    397                         1600003600,
    398                         "2",
    399                         "invoice-42",
    400                         &w2));
    401   FAILIF (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT !=
    402           add_wire_out (pg,
    403                         &other,
    404                         12,
    405                         1600007200,
    406                         "5",
    407                         NULL,
    408                         &w3));
    409   FAILIF (3 != TDB_count (pg,
    410                           "FROM wire_out"));
    411 
    412   memset (&ctx,
    413           0,
    414           sizeof (ctx));
    415   FAILIF (3 !=
    416           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id (pg,
    417                                                               0,
    418                                                               &wire_out_cb,
    419                                                               &ctx));
    420   GNUNET_free (ctx.payto);
    421   memset (&ctx,
    422           0,
    423           sizeof (ctx));
    424   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    425           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id (pg,
    426                                                               1000,
    427                                                               &wire_out_cb,
    428                                                               &ctx));
    429   FAILIF (0 != ctx.total);
    430 
    431   /* everything went out through the same exchange account */
    432   memset (&ctx,
    433           0,
    434           sizeof (ctx));
    435   FAILIF (3 !=
    436           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id_by_account (
    437             pg,
    438             "exchange-account-1",
    439             0,
    440             &wire_out_cb,
    441             &ctx));
    442   GNUNET_free (ctx.payto);
    443   memset (&ctx,
    444           0,
    445           sizeof (ctx));
    446   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    447           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id_by_account (
    448             pg,
    449             "exchange-account-2",
    450             0,
    451             &wire_out_cb,
    452             &ctx));
    453   FAILIF (0 != ctx.total);
    454 
    455   /* a callback that gives up stops the iteration */
    456   memset (&ctx,
    457           0,
    458           sizeof (ctx));
    459   ctx.stop_after = 1;
    460   FAILIF (3 !=
    461           TALER_EXCHANGEDB_iterate_wire_outs_above_serial_id (pg,
    462                                                               0,
    463                                                               &wire_out_cb,
    464                                                               &ctx));
    465   FAILIF (1 != ctx.total);
    466   GNUNET_free (ctx.payto);
    467   return 0;
    468 }
    469 
    470 
    471 /**
    472  * The AML views filter by amount and by receiving account.
    473  *
    474  * @param pg the database context
    475  * @return 0 on success
    476  */
    477 static int
    478 check_aml_views (struct TALER_EXCHANGEDB_PostgresContext *pg)
    479 {
    480   struct TALER_Amount zero = TDB_amount ("0");
    481   struct TALER_Amount five = TDB_amount ("5");
    482   struct TransferContext ctx;
    483   struct AmountContext actx;
    484   uint64_t lowest;
    485 
    486   memset (&ctx,
    487           0,
    488           sizeof (ctx));
    489   FAILIF (3 !=
    490           TALER_EXCHANGEDB_iterate_exchange_debit_transfers (pg,
    491                                                              &zero,
    492                                                              0,
    493                                                              10,
    494                                                              NULL,
    495                                                              &transfer_cb,
    496                                                              &ctx));
    497   FAILIF (10 + 2 + 5 != ctx.value_sum);
    498   lowest = ctx.first_row;
    499 
    500   memset (&ctx,
    501           0,
    502           sizeof (ctx));
    503   FAILIF (2 !=
    504           TALER_EXCHANGEDB_iterate_exchange_debit_transfers (pg,
    505                                                              &five,
    506                                                              0,
    507                                                              10,
    508                                                              NULL,
    509                                                              &transfer_cb,
    510                                                              &ctx));
    511   FAILIF (10 + 5 != ctx.value_sum);
    512 
    513   memset (&ctx,
    514           0,
    515           sizeof (ctx));
    516   FAILIF (2 !=
    517           TALER_EXCHANGEDB_iterate_exchange_debit_transfers (pg,
    518                                                              &zero,
    519                                                              0,
    520                                                              10,
    521                                                              &account.
    522                                                              h_normalized,
    523                                                              &transfer_cb,
    524                                                              &ctx));
    525   FAILIF (10 + 2 != ctx.value_sum);
    526 
    527   /* the negative limit walks the table backwards */
    528   memset (&ctx,
    529           0,
    530           sizeof (ctx));
    531   FAILIF (3 !=
    532           TALER_EXCHANGEDB_iterate_exchange_debit_transfers (pg,
    533                                                              &zero,
    534                                                              1000,
    535                                                              -10,
    536                                                              NULL,
    537                                                              &transfer_cb,
    538                                                              &ctx));
    539   FAILIF (lowest == ctx.first_row);
    540 
    541   /* the KYC view groups by the credited account */
    542   memset (&actx,
    543           0,
    544           sizeof (actx));
    545   actx.ret = GNUNET_OK;
    546   FAILIF (2 !=
    547           TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check (
    548             pg,
    549             &account.h_normalized,
    550             GNUNET_TIME_UNIT_ZERO_ABS,
    551             &amount_cb,
    552             &actx));
    553   FAILIF (10 + 2 != actx.value_sum);
    554 
    555   memset (&actx,
    556           0,
    557           sizeof (actx));
    558   actx.ret = GNUNET_OK;
    559   FAILIF (1 !=
    560           TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check (
    561             pg,
    562             &other.h_normalized,
    563             GNUNET_TIME_UNIT_ZERO_ABS,
    564             &amount_cb,
    565             &actx));
    566   FAILIF (5 != actx.value_sum);
    567 
    568   /* a limit in the future hides everything */
    569   memset (&actx,
    570           0,
    571           sizeof (actx));
    572   actx.ret = GNUNET_OK;
    573   FAILIF (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS !=
    574           TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check (
    575             pg,
    576             &account.h_normalized,
    577             GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS),
    578             &amount_cb,
    579             &actx));
    580   FAILIF (0 != actx.total);
    581 
    582   /* and an aborting callback is not an error */
    583   memset (&actx,
    584           0,
    585           sizeof (actx));
    586   actx.ret = GNUNET_NO;
    587   FAILIF (0 >
    588           TALER_EXCHANGEDB_iterate_aggregation_amounts_for_kyc_check (
    589             pg,
    590             &account.h_normalized,
    591             GNUNET_TIME_UNIT_ZERO_ABS,
    592             &amount_cb,
    593             &actx));
    594   FAILIF (1 != actx.total);
    595   return 0;
    596 }
    597 
    598 
    599 /**
    600  * The checks to run, in order.
    601  */
    602 static const struct TDB_Test tests[] = {
    603   { "wire-out-empty",
    604     &check_empty },
    605   { "wire-out-insert",
    606     &check_insert },
    607   { "wire-out-iterate",
    608     &check_iterate },
    609   { "wire-out-aml-views",
    610     &check_aml_views },
    611   { NULL, NULL }
    612 };
    613 
    614 
    615 int
    616 main (int argc,
    617       char *const *argv)
    618 {
    619   int ret;
    620 
    621   ret = TDB_main (argc,
    622                   argv,
    623                   "test-wire-out",
    624                   "Tests for the exchangedb `wire_out' table",
    625                   tests);
    626   TDB_account_free (&account);
    627   TDB_account_free (&other);
    628   return ret;
    629 }
    630 
    631 
    632 /* end of test_wire_out.c */