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 : ¤cy_round_unit)) ||
145 22 : (TALER_amount_is_zero (¤cy_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 : ¤cy_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 */
|