Line data Source code
1 : /*
2 : This file is part of TALER
3 : Copyright (C) 2023, 2025 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 : * @file taler-merchant-wirewatch.c
18 : * @brief Process that imports information about incoming bank transfers into the merchant backend
19 : * @author Christian Grothoff
20 : */
21 : #include "platform.h"
22 : #include "microhttpd.h"
23 : #include <gnunet/gnunet_util_lib.h>
24 : #include <jansson.h>
25 : #include <pthread.h>
26 : #include <taler/taler_dbevents.h>
27 : #include "taler_merchant_util.h"
28 : #include "taler_merchant_bank_lib.h"
29 : #include "taler_merchantdb_lib.h"
30 : #include "taler_merchantdb_plugin.h"
31 :
32 : /**
33 : * Timeout for the bank interaction. Rather long as we should do long-polling
34 : * and do not want to wake up too often.
35 : */
36 : #define BANK_TIMEOUT GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MINUTES, \
37 : 5)
38 :
39 :
40 : /**
41 : * Information about a watch job.
42 : */
43 : struct Watch
44 : {
45 : /**
46 : * Kept in a DLL.
47 : */
48 : struct Watch *next;
49 :
50 : /**
51 : * Kept in a DLL.
52 : */
53 : struct Watch *prev;
54 :
55 : /**
56 : * Next task to run, if any.
57 : */
58 : struct GNUNET_SCHEDULER_Task *task;
59 :
60 : /**
61 : * Dynamically adjusted long polling time-out.
62 : */
63 : struct GNUNET_TIME_Relative bank_timeout;
64 :
65 : /**
66 : * For which instance are we importing bank transfers?
67 : */
68 : char *instance_id;
69 :
70 : /**
71 : * For which account are we importing bank transfers?
72 : */
73 : struct TALER_FullPayto payto_uri;
74 :
75 : /**
76 : * Bank history request.
77 : */
78 : struct TALER_MERCHANT_BANK_CreditHistoryHandle *hh;
79 :
80 : /**
81 : * Start row for the bank interaction. Exclusive.
82 : */
83 : uint64_t start_row;
84 :
85 : /**
86 : * Artificial delay to use between API calls. Used to
87 : * throttle on failures.
88 : */
89 : struct GNUNET_TIME_Relative delay;
90 :
91 : /**
92 : * When did we start our last HTTP request?
93 : */
94 : struct GNUNET_TIME_Absolute start_time;
95 :
96 : /**
97 : * How long should long-polling take at least?
98 : */
99 : struct GNUNET_TIME_Absolute long_poll_timeout;
100 :
101 : /**
102 : * Login data for the bank.
103 : */
104 : struct TALER_MERCHANT_BANK_AuthenticationData ad;
105 :
106 : /**
107 : * Set to true if we found a transaction in the last iteration.
108 : */
109 : bool found;
110 :
111 : };
112 :
113 :
114 : /**
115 : * Head of active watches.
116 : */
117 : static struct Watch *w_head;
118 :
119 : /**
120 : * Tail of active watches.
121 : */
122 : static struct Watch *w_tail;
123 :
124 : /**
125 : * The merchant's configuration.
126 : */
127 : static const struct GNUNET_CONFIGURATION_Handle *cfg;
128 :
129 : /**
130 : * Our database plugin.
131 : */
132 : static struct TALER_MERCHANTDB_Plugin *db_plugin;
133 :
134 : /**
135 : * Handle to the context for interacting with the bank.
136 : */
137 : static struct GNUNET_CURL_Context *ctx;
138 :
139 : /**
140 : * Scheduler context for running the @e ctx.
141 : */
142 : static struct GNUNET_CURL_RescheduleContext *rc;
143 :
144 : /**
145 : * Event handler to learn that the configuration changed
146 : * and we should shutdown (to be restarted).
147 : */
148 : static struct GNUNET_DB_EventHandler *eh;
149 :
150 : /**
151 : * Value to return from main(). 0 on success, non-zero on errors.
152 : */
153 : static int global_ret;
154 :
155 : /**
156 : * How many transactions should we fetch at most per batch?
157 : */
158 : static unsigned int batch_size = 32;
159 :
160 : /**
161 : * #GNUNET_YES if we are in test mode and should exit when idle.
162 : */
163 : static int test_mode;
164 :
165 : /**
166 : * #GNUNET_YES if we are in persistent mode and do
167 : * not exit on #config_changed.
168 : */
169 : static int persist_mode;
170 :
171 : /**
172 : * Set to true if we are shutting down due to a
173 : * configuration change.
174 : */
175 : static bool config_changed_flag;
176 :
177 : /**
178 : * Save progress in DB.
179 : */
180 : static void
181 8 : save (struct Watch *w)
182 : {
183 : enum GNUNET_DB_QueryStatus qs;
184 :
185 8 : qs = db_plugin->update_wirewatch_progress (db_plugin->cls,
186 8 : w->instance_id,
187 : w->payto_uri,
188 : w->start_row);
189 8 : if (qs < 0)
190 : {
191 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
192 : "Failed to persist wirewatch progress for %s/%s (%d)\n",
193 : w->instance_id,
194 : w->payto_uri.full_payto,
195 : qs);
196 0 : GNUNET_SCHEDULER_shutdown ();
197 0 : global_ret = EXIT_FAILURE;
198 : }
199 8 : }
200 :
201 :
202 : /**
203 : * Free resources of @a w.
204 : *
205 : * @param w watch job to terminate
206 : */
207 : static void
208 4 : end_watch (struct Watch *w)
209 : {
210 4 : if (NULL != w->task)
211 : {
212 0 : GNUNET_SCHEDULER_cancel (w->task);
213 0 : w->task = NULL;
214 : }
215 4 : if (NULL != w->hh)
216 : {
217 0 : TALER_MERCHANT_BANK_credit_history_cancel (w->hh);
218 0 : w->hh = NULL;
219 : }
220 4 : GNUNET_free (w->instance_id);
221 4 : GNUNET_free (w->payto_uri.full_payto);
222 4 : TALER_MERCHANT_BANK_auth_free (&w->ad);
223 4 : GNUNET_CONTAINER_DLL_remove (w_head,
224 : w_tail,
225 : w);
226 4 : GNUNET_free (w);
227 4 : }
228 :
229 :
230 : /**
231 : * We're being aborted with CTRL-C (or SIGTERM). Shut down.
232 : *
233 : * @param cls closure
234 : */
235 : static void
236 4 : shutdown_task (void *cls)
237 : {
238 : (void) cls;
239 4 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
240 : "Running shutdown\n");
241 4 : while (NULL != w_head)
242 : {
243 0 : struct Watch *w = w_head;
244 :
245 0 : save (w);
246 0 : end_watch (w);
247 : }
248 4 : if (NULL != eh)
249 : {
250 4 : db_plugin->event_listen_cancel (eh);
251 4 : eh = NULL;
252 : }
253 4 : TALER_MERCHANTDB_plugin_unload (db_plugin);
254 4 : db_plugin = NULL;
255 4 : cfg = NULL;
256 4 : if (NULL != ctx)
257 : {
258 4 : GNUNET_CURL_fini (ctx);
259 4 : ctx = NULL;
260 : }
261 4 : if (NULL != rc)
262 : {
263 4 : GNUNET_CURL_gnunet_rc_destroy (rc);
264 4 : rc = NULL;
265 : }
266 4 : }
267 :
268 :
269 : /**
270 : * Parse @a subject from wire transfer into @a wtid and @a exchange_url.
271 : *
272 : * @param subject wire transfer subject to parse;
273 : * format is "$WTID $URL"
274 : * @param[out] wtid wire transfer ID to extract
275 : * @param[out] exchange_url set to exchange URL
276 : * @return #GNUNET_OK on success
277 : */
278 : static enum GNUNET_GenericReturnValue
279 4 : parse_subject (const char *subject,
280 : struct TALER_WireTransferIdentifierRawP *wtid,
281 : char **exchange_url)
282 : {
283 : const char *space;
284 :
285 4 : space = strchr (subject, ' ');
286 4 : if (NULL == space)
287 0 : return GNUNET_NO;
288 4 : if (GNUNET_OK !=
289 4 : GNUNET_STRINGS_string_to_data (subject,
290 4 : space - subject,
291 : wtid,
292 : sizeof (*wtid)))
293 0 : return GNUNET_NO;
294 4 : space++;
295 4 : if (! TALER_url_valid_charset (space))
296 0 : return GNUNET_NO;
297 4 : if ( (0 != strncasecmp ("http://",
298 : space,
299 0 : strlen ("http://"))) &&
300 0 : (0 != strncasecmp ("https://",
301 : space,
302 : strlen ("https://"))) )
303 0 : return GNUNET_NO;
304 4 : *exchange_url = GNUNET_strdup (space);
305 4 : return GNUNET_OK;
306 : }
307 :
308 :
309 : /**
310 : * Run next iteration.
311 : *
312 : * @param cls a `struct Watch *`
313 : */
314 : static void
315 : do_work (void *cls);
316 :
317 :
318 : /**
319 : * Callbacks of this type are used to serve the result of asking
320 : * the bank for the credit transaction history.
321 : *
322 : * @param cls a `struct Watch *`
323 : * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful status request
324 : * 0 if the bank's reply is bogus (fails to follow the protocol),
325 : * #MHD_HTTP_NO_CONTENT if there are no more results; on success the
326 : * last callback is always of this status (even if `abs(num_results)` were
327 : * already returned).
328 : * @param ec detailed error code
329 : * @param serial_id monotonically increasing counter corresponding to the transaction
330 : * @param details details about the wire transfer
331 : * @return #GNUNET_OK to continue, #GNUNET_SYSERR to abort iteration
332 : */
333 : static enum GNUNET_GenericReturnValue
334 12 : credit_cb (
335 : void *cls,
336 : unsigned int http_status,
337 : enum TALER_ErrorCode ec,
338 : uint64_t serial_id,
339 : const struct TALER_MERCHANT_BANK_CreditDetails *details)
340 : {
341 12 : struct Watch *w = cls;
342 :
343 12 : switch (http_status)
344 : {
345 0 : case 0:
346 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
347 : "Invalid HTTP response (HTTP status: 0, %d) from bank\n",
348 : ec);
349 0 : w->delay = GNUNET_TIME_STD_BACKOFF (w->delay);
350 0 : break;
351 4 : case MHD_HTTP_OK:
352 : {
353 : enum GNUNET_DB_QueryStatus qs;
354 : char *exchange_url;
355 : struct TALER_WireTransferIdentifierRawP wtid;
356 :
357 4 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
358 : "Received wire transfer `%s' over %s\n",
359 : details->wire_subject,
360 : TALER_amount2s (&details->amount));
361 4 : w->found = true;
362 4 : if (GNUNET_OK !=
363 4 : parse_subject (details->wire_subject,
364 : &wtid,
365 : &exchange_url))
366 : {
367 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
368 : "Skipping transfer %llu (%s): not from exchange\n",
369 : (unsigned long long) serial_id,
370 : details->wire_subject);
371 0 : w->start_row = serial_id;
372 0 : return GNUNET_OK;
373 : }
374 : /* FIXME-Performance-Optimization: consider grouping multiple inserts
375 : into one bigger transaction with just one notify. */
376 4 : qs = db_plugin->insert_transfer (db_plugin->cls,
377 4 : w->instance_id,
378 : exchange_url,
379 : &wtid,
380 : &details->amount,
381 : details->credit_account_uri,
382 : serial_id);
383 4 : GNUNET_free (exchange_url);
384 4 : if (qs < 0)
385 : {
386 0 : GNUNET_break (0);
387 0 : GNUNET_SCHEDULER_shutdown ();
388 0 : w->hh = NULL;
389 0 : return GNUNET_SYSERR;
390 : }
391 : /* Success => reset back-off timer! */
392 4 : w->delay = GNUNET_TIME_UNIT_ZERO;
393 : {
394 4 : struct GNUNET_DB_EventHeaderP es = {
395 4 : .size = htons (sizeof (es)),
396 4 : .type = htons (TALER_DBEVENT_MERCHANT_WIRE_TRANSFER_CONFIRMED)
397 : };
398 :
399 4 : db_plugin->event_notify (db_plugin->cls,
400 : &es,
401 : NULL,
402 : 0);
403 : }
404 : }
405 4 : w->start_row = serial_id;
406 4 : return GNUNET_OK;
407 8 : case MHD_HTTP_NO_CONTENT:
408 8 : save (w);
409 : /* Delay artificially if server returned before long-poll timeout */
410 8 : if (! w->found)
411 4 : w->delay = GNUNET_TIME_absolute_get_remaining (w->long_poll_timeout);
412 8 : break;
413 0 : case MHD_HTTP_NOT_FOUND:
414 : /* configuration likely wrong, wait at least 1 minute, backoff up to 15 minutes! */
415 0 : w->delay = GNUNET_TIME_relative_max (GNUNET_TIME_UNIT_MINUTES,
416 : GNUNET_TIME_STD_BACKOFF (w->delay));
417 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
418 : "Bank claims account is unknown, waiting for %s before trying again\n",
419 : GNUNET_TIME_relative2s (w->delay,
420 : true));
421 0 : break;
422 0 : case MHD_HTTP_GATEWAY_TIMEOUT:
423 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
424 : "Gateway timeout, adjusting long polling threshold\n");
425 : /* Limit new timeout at request delay */
426 : w->bank_timeout
427 0 : = GNUNET_TIME_relative_min (GNUNET_TIME_absolute_get_duration (
428 : w->start_time),
429 : w->bank_timeout);
430 : /* set the timeout a bit earlier */
431 : w->bank_timeout
432 0 : = GNUNET_TIME_relative_subtract (w->bank_timeout,
433 : GNUNET_TIME_UNIT_SECONDS);
434 : /* do not allow it to go to zero */
435 : w->bank_timeout
436 0 : = GNUNET_TIME_relative_max (w->bank_timeout,
437 : GNUNET_TIME_UNIT_SECONDS);
438 0 : w->delay = GNUNET_TIME_STD_BACKOFF (w->delay);
439 0 : break;
440 0 : default:
441 : /* Something went wrong, try again, but with back-off */
442 0 : w->delay = GNUNET_TIME_STD_BACKOFF (w->delay);
443 0 : GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
444 : "Unexpected HTTP status code %u(%d) from bank\n",
445 : http_status,
446 : ec);
447 0 : break;
448 : }
449 8 : w->hh = NULL;
450 8 : if (test_mode && (! w->found))
451 : {
452 4 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
453 : "No transactions found and in test mode. Ending watch!\n");
454 4 : end_watch (w);
455 4 : if (NULL == w_head)
456 4 : GNUNET_SCHEDULER_shutdown ();
457 4 : return GNUNET_OK;
458 : }
459 4 : w->task = GNUNET_SCHEDULER_add_delayed (w->delay,
460 : &do_work,
461 : w);
462 4 : return GNUNET_OK;
463 : }
464 :
465 :
466 : static void
467 8 : do_work (void *cls)
468 : {
469 8 : struct Watch *w = cls;
470 :
471 8 : w->task = NULL;
472 8 : w->found = false;
473 : w->long_poll_timeout
474 8 : = GNUNET_TIME_relative_to_absolute (w->bank_timeout);
475 : w->start_time
476 8 : = GNUNET_TIME_absolute_get ();
477 8 : w->hh = TALER_MERCHANT_BANK_credit_history (ctx,
478 8 : &w->ad,
479 : w->start_row,
480 : batch_size,
481 : test_mode
482 8 : ? GNUNET_TIME_UNIT_ZERO
483 : : w->bank_timeout,
484 : &credit_cb,
485 : w);
486 8 : if (NULL == w->hh)
487 : {
488 0 : GNUNET_break (0);
489 0 : GNUNET_SCHEDULER_shutdown ();
490 0 : return;
491 : }
492 : }
493 :
494 :
495 : /**
496 : * Function called with information about a accounts
497 : * the wirewatcher should monitor.
498 : *
499 : * @param cls closure (NULL)
500 : * @param instance instance that owns the account
501 : * @param payto_uri account URI
502 : * @param credit_facade_url URL for the credit facade
503 : * @param credit_facade_credentials account access credentials
504 : * @param last_serial last transaction serial (inclusive) we have seen from this account
505 : */
506 : static void
507 4 : start_watch (
508 : void *cls,
509 : const char *instance,
510 : struct TALER_FullPayto payto_uri,
511 : const char *credit_facade_url,
512 : const json_t *credit_facade_credentials,
513 : uint64_t last_serial)
514 : {
515 4 : struct Watch *w = GNUNET_new (struct Watch);
516 :
517 : (void) cls;
518 4 : w->bank_timeout = BANK_TIMEOUT;
519 4 : if (GNUNET_OK !=
520 4 : TALER_MERCHANT_BANK_auth_parse_json (credit_facade_credentials,
521 : credit_facade_url,
522 : &w->ad))
523 : {
524 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
525 : "Failed to parse authentication data of `%s/%s'\n",
526 : instance,
527 : payto_uri.full_payto);
528 0 : GNUNET_free (w);
529 0 : GNUNET_SCHEDULER_shutdown ();
530 0 : global_ret = EXIT_NOTCONFIGURED;
531 0 : return;
532 : }
533 :
534 4 : GNUNET_CONTAINER_DLL_insert (w_head,
535 : w_tail,
536 : w);
537 4 : w->instance_id = GNUNET_strdup (instance);
538 4 : w->payto_uri.full_payto = GNUNET_strdup (payto_uri.full_payto);
539 4 : w->start_row = last_serial;
540 4 : w->task = GNUNET_SCHEDULER_add_now (&do_work,
541 : w);
542 : }
543 :
544 :
545 : /**
546 : * Function called on configuration change events received from Postgres. We
547 : * shutdown (and systemd should restart us).
548 : *
549 : * @param cls closure (NULL)
550 : * @param extra additional event data provided
551 : * @param extra_size number of bytes in @a extra
552 : */
553 : static void
554 0 : config_changed (void *cls,
555 : const void *extra,
556 : size_t extra_size)
557 : {
558 : (void) cls;
559 : (void) extra;
560 : (void) extra_size;
561 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
562 : "Configuration changed, %s\n",
563 : 0 == persist_mode
564 : ? "restarting"
565 : : "reinitializing");
566 0 : config_changed_flag = true;
567 0 : GNUNET_SCHEDULER_shutdown ();
568 0 : }
569 :
570 :
571 : /**
572 : * First task.
573 : *
574 : * @param cls closure, NULL
575 : * @param args remaining command-line arguments
576 : * @param cfgfile name of the configuration file used (for saving, can be NULL!)
577 : * @param c configuration
578 : */
579 : static void
580 4 : run (void *cls,
581 : char *const *args,
582 : const char *cfgfile,
583 : const struct GNUNET_CONFIGURATION_Handle *c)
584 : {
585 : (void) args;
586 : (void) cfgfile;
587 :
588 4 : cfg = c;
589 4 : GNUNET_SCHEDULER_add_shutdown (&shutdown_task,
590 : NULL);
591 4 : ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule,
592 : &rc);
593 4 : rc = GNUNET_CURL_gnunet_rc_create (ctx);
594 4 : if (NULL == ctx)
595 : {
596 0 : GNUNET_break (0);
597 0 : GNUNET_SCHEDULER_shutdown ();
598 0 : global_ret = EXIT_FAILURE;
599 0 : return;
600 : }
601 4 : if (NULL ==
602 4 : (db_plugin = TALER_MERCHANTDB_plugin_load (cfg)))
603 : {
604 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
605 : "Failed to initialize DB subsystem\n");
606 0 : GNUNET_SCHEDULER_shutdown ();
607 0 : global_ret = EXIT_NOTCONFIGURED;
608 0 : return;
609 : }
610 4 : if (GNUNET_OK !=
611 4 : db_plugin->connect (db_plugin->cls))
612 : {
613 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
614 : "Failed to connect to database. Consider running taler-merchant-dbinit!\n");
615 0 : GNUNET_SCHEDULER_shutdown ();
616 0 : global_ret = EXIT_FAILURE;
617 0 : return;
618 : }
619 : {
620 4 : struct GNUNET_DB_EventHeaderP es = {
621 4 : .size = htons (sizeof (es)),
622 4 : .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED)
623 : };
624 :
625 8 : eh = db_plugin->event_listen (db_plugin->cls,
626 : &es,
627 4 : GNUNET_TIME_UNIT_FOREVER_REL,
628 : &config_changed,
629 : NULL);
630 : }
631 : {
632 : enum GNUNET_DB_QueryStatus qs;
633 :
634 4 : qs = db_plugin->select_wirewatch_accounts (db_plugin->cls,
635 : &start_watch,
636 : NULL);
637 4 : if (qs < 0)
638 : {
639 0 : GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
640 : "Failed to obtain wirewatch accounts from database\n");
641 0 : GNUNET_SCHEDULER_shutdown ();
642 0 : global_ret = EXIT_NO_RESTART;
643 0 : return;
644 : }
645 4 : if ( (NULL == w_head) &&
646 0 : (GNUNET_YES == test_mode) )
647 : {
648 0 : GNUNET_log (GNUNET_ERROR_TYPE_INFO,
649 : "No active wirewatch accounts in database and in test mode. Exiting.\n");
650 0 : GNUNET_SCHEDULER_shutdown ();
651 0 : global_ret = EXIT_SUCCESS;
652 0 : return;
653 : }
654 : }
655 : }
656 :
657 :
658 : /**
659 : * The main function of taler-merchant-wirewatch
660 : *
661 : * @param argc number of arguments from the command line
662 : * @param argv command line arguments
663 : * @return 0 ok, 1 on error
664 : */
665 : int
666 4 : main (int argc,
667 : char *const *argv)
668 : {
669 4 : struct GNUNET_GETOPT_CommandLineOption options[] = {
670 4 : GNUNET_GETOPT_option_flag ('p',
671 : "persist",
672 : "run in persist mode and do not exit on configuration changes",
673 : &persist_mode),
674 4 : GNUNET_GETOPT_option_timetravel ('T',
675 : "timetravel"),
676 4 : GNUNET_GETOPT_option_flag ('t',
677 : "test",
678 : "run in test mode and exit when idle",
679 : &test_mode),
680 4 : GNUNET_GETOPT_option_version (VERSION "-" VCS_VERSION),
681 : GNUNET_GETOPT_OPTION_END
682 : };
683 : enum GNUNET_GenericReturnValue ret;
684 :
685 : do {
686 4 : config_changed_flag = false;
687 4 : ret = GNUNET_PROGRAM_run (
688 : TALER_MERCHANT_project_data (),
689 : argc, argv,
690 : "taler-merchant-wirewatch",
691 : gettext_noop (
692 : "background process that watches for incoming wire transfers to the merchant bank account"),
693 : options,
694 : &run, NULL);
695 4 : } while ( (1 == persist_mode) &&
696 : config_changed_flag);
697 4 : if (GNUNET_SYSERR == ret)
698 0 : return EXIT_INVALIDARGUMENT;
699 4 : if (GNUNET_NO == ret)
700 0 : return EXIT_SUCCESS;
701 4 : return global_ret;
702 : }
703 :
704 :
705 : /* end of taler-exchange-wirewatch.c */
|