LCOV - code coverage report
Current view: top level - exchange - taler-exchange-closer.c (source / functions) Hit Total Coverage
Test: coverage.info Lines: 95 181 52.5 %
Date: 2025-07-03 11:36:01 Functions: 7 7 100.0 %

          Line data    Source code
       1             : /*
       2             :   This file is part of TALER
       3             :   Copyright (C) 2016-2022 Taler Systems SA
       4             : 
       5             :   TALER is free software; you can redistribute it and/or modify it under the
       6             :   terms of the GNU Affero 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 Affero General Public License for more details.
      12             : 
      13             :   You should have received a copy of the GNU Affero General Public License along with
      14             :   TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
      15             : */
      16             : 
      17             : /**
      18             :  * @file taler-exchange-closer.c
      19             :  * @brief Process that closes expired reserves
      20             :  * @author Christian Grothoff
      21             :  */
      22             : #include "taler/platform.h"
      23             : #include <gnunet/gnunet_util_lib.h>
      24             : #include <jansson.h>
      25             : #include <pthread.h>
      26             : #include "taler/taler_exchangedb_lib.h"
      27             : #include "taler/taler_exchangedb_plugin.h"
      28             : #include "taler/taler_json_lib.h"
      29             : #include "taler/taler_bank_service.h"
      30             : 
      31             : 
      32             : /**
      33             :  * What is the smallest unit we support for wire transfers?
      34             :  * We will need to round down to a multiple of this amount.
      35             :  */
      36             : static struct TALER_Amount currency_round_unit;
      37             : 
      38             : /**
      39             :  * What is the base URL of this exchange?  Used in the
      40             :  * wire transfer subjects so that merchants and governments
      41             :  * can ask for the list of aggregated deposits.
      42             :  */
      43             : static char *exchange_base_url;
      44             : 
      45             : /**
      46             :  * The exchange's configuration.
      47             :  */
      48             : static const struct GNUNET_CONFIGURATION_Handle *cfg;
      49             : 
      50             : /**
      51             :  * Our database plugin.
      52             :  */
      53             : static struct TALER_EXCHANGEDB_Plugin *db_plugin;
      54             : 
      55             : /**
      56             :  * Next task to run, if any.
      57             :  */
      58             : static struct GNUNET_SCHEDULER_Task *task;
      59             : 
      60             : /**
      61             :  * How long should we sleep when idle before trying to find more work?
      62             :  */
      63             : static struct GNUNET_TIME_Relative closer_idle_sleep_interval;
      64             : 
      65             : /**
      66             :  * Value to return from main(). 0 on success, non-zero
      67             :  * on serious errors.
      68             :  */
      69             : static int global_ret;
      70             : 
      71             : /**
      72             :  * #GNUNET_YES if we are in test mode and should exit when idle.
      73             :  */
      74             : static int test_mode;
      75             : 
      76             : 
      77             : /**
      78             :  * Main work function that finds and triggers transfers for reserves
      79             :  * closures.
      80             :  *
      81             :  * @param cls closure
      82             :  */
      83             : static void
      84             : run_reserve_closures (void *cls);
      85             : 
      86             : 
      87             : /**
      88             :  * We're being aborted with CTRL-C (or SIGTERM). Shut down.
      89             :  *
      90             :  * @param cls closure
      91             :  */
      92             : static void
      93          22 : shutdown_task (void *cls)
      94             : {
      95             :   (void) cls;
      96          22 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
      97             :               "Running shutdown\n");
      98          22 :   if (NULL != task)
      99             :   {
     100           0 :     GNUNET_SCHEDULER_cancel (task);
     101           0 :     task = NULL;
     102             :   }
     103          22 :   TALER_EXCHANGEDB_plugin_unload (db_plugin);
     104          22 :   db_plugin = NULL;
     105          22 :   TALER_EXCHANGEDB_unload_accounts ();
     106          22 :   cfg = NULL;
     107          22 : }
     108             : 
     109             : 
     110             : /**
     111             :  * Parse the configuration for wirewatch.
     112             :  *
     113             :  * @return #GNUNET_OK on success
     114             :  */
     115             : static enum GNUNET_GenericReturnValue
     116          22 : parse_closer_config (void)
     117             : {
     118          22 :   if (GNUNET_OK !=
     119          22 :       GNUNET_CONFIGURATION_get_value_string (cfg,
     120             :                                              "exchange",
     121             :                                              "BASE_URL",
     122             :                                              &exchange_base_url))
     123             :   {
     124           0 :     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
     125             :                                "exchange",
     126             :                                "BASE_URL");
     127           0 :     return GNUNET_SYSERR;
     128             :   }
     129          22 :   if (GNUNET_OK !=
     130          22 :       GNUNET_CONFIGURATION_get_value_time (cfg,
     131             :                                            "exchange",
     132             :                                            "CLOSER_IDLE_SLEEP_INTERVAL",
     133             :                                            &closer_idle_sleep_interval))
     134             :   {
     135           0 :     GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR,
     136             :                                "exchange",
     137             :                                "CLOSER_IDLE_SLEEP_INTERVAL");
     138           0 :     return GNUNET_SYSERR;
     139             :   }
     140          22 :   if ( (GNUNET_OK !=
     141          22 :         TALER_config_get_amount (cfg,
     142             :                                  "exchange",
     143             :                                  "CURRENCY_ROUND_UNIT",
     144          22 :                                  &currency_round_unit)) ||
     145          22 :        (TALER_amount_is_zero (&currency_round_unit)) )
     146             :   {
     147           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     148             :                 "Need non-zero amount in section `exchange' under `CURRENCY_ROUND_UNIT'\n");
     149           0 :     return GNUNET_SYSERR;
     150             :   }
     151             : 
     152          22 :   if (NULL ==
     153          22 :       (db_plugin = TALER_EXCHANGEDB_plugin_load (cfg,
     154             :                                                  false)))
     155             :   {
     156           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     157             :                 "Failed to initialize DB subsystem\n");
     158           0 :     return GNUNET_SYSERR;
     159             :   }
     160          22 :   if (GNUNET_OK !=
     161          22 :       TALER_EXCHANGEDB_load_accounts (cfg,
     162             :                                       TALER_EXCHANGEDB_ALO_DEBIT))
     163             :   {
     164           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     165             :                 "No wire accounts configured for debit!\n");
     166           0 :     TALER_EXCHANGEDB_plugin_unload (db_plugin);
     167           0 :     db_plugin = NULL;
     168           0 :     return GNUNET_SYSERR;
     169             :   }
     170          22 :   return GNUNET_OK;
     171             : }
     172             : 
     173             : 
     174             : /**
     175             :  * Perform a database commit. If it fails, print a warning.
     176             :  *
     177             :  * @return status of commit
     178             :  */
     179             : static enum GNUNET_DB_QueryStatus
     180          11 : commit_or_warn (void)
     181             : {
     182             :   enum GNUNET_DB_QueryStatus qs;
     183             : 
     184          11 :   qs = db_plugin->commit (db_plugin->cls);
     185          11 :   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
     186          11 :     return qs;
     187           0 :   GNUNET_log ((GNUNET_DB_STATUS_SOFT_ERROR == qs)
     188             :               ? GNUNET_ERROR_TYPE_INFO
     189             :               : GNUNET_ERROR_TYPE_ERROR,
     190             :               "Failed to commit database transaction!\n");
     191           0 :   return qs;
     192             : }
     193             : 
     194             : 
     195             : /**
     196             :  * Function called with details about expired reserves.
     197             :  * We trigger the reserve closure by inserting the respective
     198             :  * closing record and prewire instructions into the respective
     199             :  * tables.
     200             :  *
     201             :  * @param cls NULL
     202             :  * @param reserve_pub public key of the reserve
     203             :  * @param left amount left in the reserve
     204             :  * @param account_payto_uri information about the bank account that initially
     205             :  *        caused the reserve to be created
     206             :  * @param expiration_date when did the reserve expire
     207             :  * @param close_request_row row of request asking for
     208             :  *         closure, 0 for expired reserves
     209             :  * @return #GNUNET_OK on success (continue)
     210             :  *         #GNUNET_NO on non-fatal errors (try again)
     211             :  *         #GNUNET_SYSERR on fatal errors (abort)
     212             :  */
     213             : static enum GNUNET_GenericReturnValue
     214          11 : expired_reserve_cb (void *cls,
     215             :                     const struct TALER_ReservePublicKeyP *reserve_pub,
     216             :                     const struct TALER_Amount *left,
     217             :                     const struct TALER_FullPayto account_payto_uri,
     218             :                     struct GNUNET_TIME_Timestamp expiration_date,
     219             :                     uint64_t close_request_row)
     220             : {
     221             :   struct GNUNET_TIME_Timestamp now;
     222             :   struct TALER_WireTransferIdentifierRawP wtid;
     223             :   struct TALER_Amount amount_without_fee;
     224             :   struct TALER_Amount closing_fee;
     225             :   struct TALER_WireFeeSet fees;
     226             :   enum TALER_AmountArithmeticResult ret;
     227             :   const struct TALER_EXCHANGEDB_AccountInfo *wa;
     228             : 
     229             :   (void) cls;
     230             :   /* NOTE: potential optimization: use custom SQL API to not
     231             :      fetch this: */
     232          11 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     233             :               "Processing reserve closure at %s\n",
     234             :               GNUNET_TIME_timestamp2s (expiration_date));
     235          11 :   now = GNUNET_TIME_timestamp_get ();
     236             : 
     237             :   /* lookup account we should use */
     238          11 :   wa = TALER_EXCHANGEDB_find_account_by_payto_uri (account_payto_uri);
     239          11 :   if (NULL == wa)
     240             :   {
     241           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     242             :                 "No wire account configured to deal with target URI `%s'\n",
     243             :                 account_payto_uri.full_payto);
     244           0 :     global_ret = EXIT_FAILURE;
     245           0 :     GNUNET_SCHEDULER_shutdown ();
     246           0 :     return GNUNET_SYSERR;
     247             :   }
     248             : 
     249             :   /* lookup `fees` from time of actual reserve expiration
     250             :      (we may be lagging behind!) */
     251             :   {
     252             :     struct GNUNET_TIME_Timestamp start_date;
     253             :     struct GNUNET_TIME_Timestamp end_date;
     254             :     struct TALER_MasterSignatureP master_sig;
     255             :     enum GNUNET_DB_QueryStatus qs;
     256             :     uint64_t rowid;
     257             : 
     258          11 :     qs = db_plugin->get_wire_fee (db_plugin->cls,
     259          11 :                                   wa->method,
     260             :                                   expiration_date,
     261             :                                   &rowid,
     262             :                                   &start_date,
     263             :                                   &end_date,
     264             :                                   &fees,
     265             :                                   &master_sig);
     266          11 :     switch (qs)
     267             :     {
     268           0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     269           0 :       GNUNET_break (0);
     270           0 :       return GNUNET_SYSERR;
     271           0 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     272           0 :       GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     273             :                   "Could not get wire fees for %s at %s. Aborting run.\n",
     274             :                   wa->method,
     275             :                   GNUNET_TIME_timestamp2s (expiration_date));
     276           0 :       return GNUNET_SYSERR;
     277           0 :     case GNUNET_DB_STATUS_SOFT_ERROR:
     278           0 :       return GNUNET_NO;
     279          11 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     280             :       /* continued below */
     281          11 :       break;
     282             :     }
     283             :   }
     284             : 
     285             :   /* calculate transfer amount */
     286          11 :   closing_fee = fees.closing;
     287          11 :   ret = TALER_amount_subtract (&amount_without_fee,
     288             :                                left,
     289             :                                &closing_fee);
     290          11 :   if ( (TALER_AAR_INVALID_NEGATIVE_RESULT == ret) ||
     291             :        (TALER_AAR_RESULT_ZERO == ret) )
     292             :   {
     293             :     /* Closing fee higher than or equal to remaining balance, close
     294             :        without wire transfer. */
     295           0 :     closing_fee = *left;
     296           0 :     GNUNET_assert (GNUNET_OK ==
     297             :                    TALER_amount_set_zero (left->currency,
     298             :                                           &amount_without_fee));
     299           0 :     ret = TALER_AAR_RESULT_ZERO;
     300             :   }
     301             :   /* round down to enable transfer */
     302          11 :   if (GNUNET_SYSERR ==
     303          11 :       TALER_amount_round_down (&amount_without_fee,
     304             :                                &currency_round_unit))
     305             :   {
     306           0 :     GNUNET_break (0);
     307           0 :     global_ret = EXIT_FAILURE;
     308           0 :     GNUNET_SCHEDULER_shutdown ();
     309           0 :     return GNUNET_SYSERR;
     310             :   }
     311             :   /* NOTE: sizeof (*reserve_pub) == sizeof (wtid) right now, but to
     312             :      be future-compatible, we use the memset + min construction */
     313          11 :   memset (&wtid,
     314             :           0,
     315             :           sizeof (wtid));
     316          11 :   GNUNET_memcpy (&wtid,
     317             :                  reserve_pub,
     318             :                  GNUNET_MIN (sizeof (wtid),
     319             :                              sizeof (*reserve_pub)));
     320             : 
     321             :   {
     322             :     enum GNUNET_DB_QueryStatus qs;
     323             : 
     324          11 :     qs = db_plugin->insert_reserve_closed (db_plugin->cls,
     325             :                                            reserve_pub,
     326             :                                            now,
     327             :                                            account_payto_uri,
     328             :                                            &wtid,
     329             :                                            left,
     330             :                                            &closing_fee,
     331             :                                            close_request_row);
     332          11 :     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     333             :                 "Closing reserve %s over %s (%d, %d)\n",
     334             :                 TALER_B2S (reserve_pub),
     335             :                 TALER_amount2s (left),
     336             :                 (int) ret,
     337             :                 qs);
     338             :     /* Check for hard failure */
     339          11 :     if (GNUNET_DB_STATUS_HARD_ERROR == qs)
     340             :     {
     341           0 :       GNUNET_break (0);
     342           0 :       global_ret = EXIT_FAILURE;
     343           0 :       GNUNET_SCHEDULER_shutdown ();
     344           0 :       return GNUNET_SYSERR;
     345             :     }
     346             :   }
     347          11 :   if (TALER_amount_is_zero (&amount_without_fee))
     348             :   {
     349             :     enum GNUNET_DB_QueryStatus qs;
     350             : 
     351             :     /* Reserve balance was zero OR soft error */
     352           0 :     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     353             :                 "Reserve was virtually empty, moving on\n");
     354           0 :     qs = commit_or_warn ();
     355           0 :     switch (qs)
     356             :     {
     357           0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     358           0 :       GNUNET_break (0);
     359           0 :       return GNUNET_SYSERR;
     360           0 :     case GNUNET_DB_STATUS_SOFT_ERROR:
     361           0 :       return GNUNET_NO;
     362           0 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     363             :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     364           0 :       return GNUNET_OK;
     365             :     }
     366             :   }
     367             : 
     368             :   /* success, perform wire transfer */
     369             :   {
     370             :     void *buf;
     371             :     size_t buf_size;
     372             :     enum GNUNET_DB_QueryStatus qs;
     373             : 
     374          11 :     TALER_BANK_prepare_transfer (account_payto_uri,
     375             :                                  &amount_without_fee,
     376             :                                  exchange_base_url,
     377             :                                  &wtid,
     378             :                                  &buf,
     379             :                                  &buf_size);
     380             :     /* Commit our intention to execute the wire transfer! */
     381          11 :     qs = db_plugin->wire_prepare_data_insert (db_plugin->cls,
     382          11 :                                               wa->method,
     383             :                                               buf,
     384             :                                               buf_size);
     385          11 :     GNUNET_free (buf);
     386          11 :     switch (qs)
     387             :     {
     388           0 :     case GNUNET_DB_STATUS_HARD_ERROR:
     389           0 :       GNUNET_break (0);
     390           0 :       global_ret = EXIT_FAILURE;
     391           0 :       GNUNET_SCHEDULER_shutdown ();
     392           0 :       return GNUNET_SYSERR;
     393           0 :     case GNUNET_DB_STATUS_SOFT_ERROR:
     394             :       /* start again */
     395           0 :       return GNUNET_NO;
     396           0 :     case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     397           0 :       GNUNET_break (0);
     398           0 :       global_ret = EXIT_FAILURE;
     399           0 :       GNUNET_SCHEDULER_shutdown ();
     400           0 :       return GNUNET_SYSERR;
     401          11 :     case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     402          11 :       break;
     403             :     }
     404             :   }
     405          11 :   return GNUNET_OK;
     406             : }
     407             : 
     408             : 
     409             : /**
     410             :  * Main work function that finds and triggers transfers for reserves
     411             :  * closures.
     412             :  *
     413             :  * @param cls closure
     414             :  */
     415             : static void
     416          33 : run_reserve_closures (void *cls)
     417             : {
     418             :   enum GNUNET_DB_QueryStatus qs;
     419             :   struct GNUNET_TIME_Timestamp now;
     420             : 
     421             :   (void) cls;
     422          33 :   task = NULL;
     423          33 :   if (GNUNET_SYSERR ==
     424          33 :       db_plugin->preflight (db_plugin->cls))
     425             :   {
     426           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     427             :                 "Failed to obtain database connection!\n");
     428           0 :     global_ret = EXIT_FAILURE;
     429           0 :     GNUNET_SCHEDULER_shutdown ();
     430          33 :     return;
     431             :   }
     432             : 
     433          33 :   if (GNUNET_OK !=
     434          33 :       db_plugin->start (db_plugin->cls,
     435             :                         "aggregator reserve closures"))
     436             :   {
     437           0 :     GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
     438             :                 "Failed to start database transaction!\n");
     439           0 :     global_ret = EXIT_FAILURE;
     440           0 :     GNUNET_SCHEDULER_shutdown ();
     441           0 :     return;
     442             :   }
     443          33 :   now = GNUNET_TIME_timestamp_get ();
     444          33 :   GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     445             :               "Checking for reserves to close by date %s\n",
     446             :               GNUNET_TIME_timestamp2s (now));
     447          33 :   qs = db_plugin->get_unfinished_close_requests (db_plugin->cls,
     448             :                                                  &expired_reserve_cb,
     449             :                                                  NULL);
     450          33 :   if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
     451             :   {
     452             :     /* Try expired reserves as well */
     453          31 :     qs = db_plugin->get_expired_reserves (
     454          31 :       db_plugin->cls,
     455             :       now,
     456             :       &expired_reserve_cb,
     457             :       NULL);
     458             :   }
     459          33 :   switch (qs)
     460             :   {
     461           0 :   case GNUNET_DB_STATUS_HARD_ERROR:
     462           0 :     GNUNET_break (0);
     463           0 :     db_plugin->rollback (db_plugin->cls);
     464           0 :     global_ret = EXIT_FAILURE;
     465           0 :     GNUNET_SCHEDULER_shutdown ();
     466           0 :     return;
     467           0 :   case GNUNET_DB_STATUS_SOFT_ERROR:
     468           0 :     db_plugin->rollback (db_plugin->cls);
     469           0 :     GNUNET_assert (NULL == task);
     470           0 :     task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
     471             :                                      NULL);
     472           0 :     return;
     473          22 :   case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
     474          22 :     GNUNET_log (GNUNET_ERROR_TYPE_INFO,
     475             :                 "No more idle reserves to close, going to sleep.\n");
     476          22 :     db_plugin->rollback (db_plugin->cls);
     477          22 :     GNUNET_assert (NULL == task);
     478          22 :     if (GNUNET_YES == test_mode)
     479             :     {
     480          22 :       GNUNET_SCHEDULER_shutdown ();
     481          22 :       return;
     482             :     }
     483           0 :     task = GNUNET_SCHEDULER_add_delayed (closer_idle_sleep_interval,
     484             :                                          &run_reserve_closures,
     485             :                                          NULL);
     486           0 :     return;
     487          11 :   case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
     488          11 :     (void) commit_or_warn ();
     489          11 :     GNUNET_assert (NULL == task);
     490          11 :     task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
     491             :                                      NULL);
     492          11 :     return;
     493             :   }
     494             : }
     495             : 
     496             : 
     497             : /**
     498             :  * First task.  Parses the configuration and starts the
     499             :  * main loop of #run_reserve_closures(). Also schedules
     500             :  * the #shutdown_task() to clean up.
     501             :  *
     502             :  * @param cls closure, NULL
     503             :  * @param args remaining command-line arguments
     504             :  * @param cfgfile name of the configuration file used (for saving, can be NULL!)
     505             :  * @param c configuration
     506             :  */
     507             : static void
     508          22 : run (void *cls,
     509             :      char *const *args,
     510             :      const char *cfgfile,
     511             :      const struct GNUNET_CONFIGURATION_Handle *c)
     512             : {
     513             :   (void) cls;
     514             :   (void) args;
     515             :   (void) cfgfile;
     516             : 
     517          22 :   cfg = c;
     518          22 :   if (GNUNET_OK != parse_closer_config ())
     519             :   {
     520           0 :     cfg = NULL;
     521           0 :     global_ret = EXIT_NOTCONFIGURED;
     522           0 :     return;
     523             :   }
     524          22 :   GNUNET_assert (NULL == task);
     525          22 :   task = GNUNET_SCHEDULER_add_now (&run_reserve_closures,
     526             :                                    NULL);
     527          22 :   GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
     528             :                                  cls);
     529             : }
     530             : 
     531             : 
     532             : /**
     533             :  * The main function of the taler-exchange-closer.
     534             :  *
     535             :  * @param argc number of arguments from the command line
     536             :  * @param argv command line arguments
     537             :  * @return 0 ok, non-zero on error
     538             :  */
     539             : int
     540          22 : main (int argc,
     541             :       char *const *argv)
     542             : {
     543          22 :   struct GNUNET_GETOPT_CommandLineOption options[] = {
     544          22 :     GNUNET_GETOPT_option_timetravel ('T',
     545             :                                      "timetravel"),
     546          22 :     GNUNET_GETOPT_option_flag ('t',
     547             :                                "test",
     548             :                                "run in test mode and exit when idle",
     549             :                                &test_mode),
     550             :     GNUNET_GETOPT_OPTION_END
     551             :   };
     552             :   enum GNUNET_GenericReturnValue ret;
     553             : 
     554          22 :   ret = GNUNET_PROGRAM_run (
     555             :     TALER_EXCHANGE_project_data (),
     556             :     argc, argv,
     557             :     "taler-exchange-closer",
     558             :     gettext_noop ("background process that closes expired reserves"),
     559             :     options,
     560             :     &run, NULL);
     561          22 :   if (GNUNET_SYSERR == ret)
     562           0 :     return EXIT_INVALIDARGUMENT;
     563          22 :   if (GNUNET_NO == ret)
     564           0 :     return EXIT_SUCCESS;
     565          22 :   return global_ret;
     566             : }
     567             : 
     568             : 
     569             : /* end of taler-exchange-closer.c */

Generated by: LCOV version 1.16