/* This file is part of TALER Copyright (C) 2022 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version. TALER is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with TALER; see the file COPYING. If not, see */ /** * @file kyclogic_api.c * @brief server-side KYC API * @author Christian Grothoff */ #include "platform.h" #include "taler_kyclogic_lib.h" /** * Information about a KYC provider. */ struct TALER_KYCLOGIC_KycProvider; /** * Abstract representation of a KYC check. */ struct TALER_KYCLOGIC_KycCheck { /** * Human-readable name given to the KYC check. */ char *name; /** * Array of @e num_providers providers that offer this type of KYC check. */ struct TALER_KYCLOGIC_KycProvider **providers; /** * Length of the @e providers array. */ unsigned int num_providers; }; struct TALER_KYCLOGIC_KycProvider { /** * Name of the provider (configuration section name). */ const char *provider_section_name; /** * Array of @e num_checks checks performed by this provider. */ struct TALER_KYCLOGIC_KycCheck **provided_checks; /** * Logic to run for this provider. */ struct TALER_KYCLOGIC_Plugin *logic; /** * @e provider_section_name specific details to * pass to the @e logic functions. */ struct TALER_KYCLOGIC_ProviderDetails *pd; /** * Cost of running this provider's KYC. */ unsigned long long cost; /** * Length of the @e checks array. */ unsigned int num_checks; /** * Type of user this provider supports. */ enum TALER_KYCLOGIC_KycUserType user_type; }; /** * Condition that triggers a need to perform KYC. */ struct TALER_KYCLOGIC_KycTrigger { /** * Timeframe to consider for computing the amount * to compare against the @e limit. Zero for the * wallet balance trigger (as not applicable). */ struct GNUNET_TIME_Relative timeframe; /** * Maximum amount that can be transacted until * the rule triggers. */ struct TALER_Amount threshold; /** * Array of @e num_checks checks to apply on this trigger. */ struct TALER_KYCLOGIC_KycCheck **required_checks; /** * Length of the @e checks array. */ unsigned int num_checks; /** * What event is this trigger for? */ enum TALER_KYCLOGIC_KycTriggerEvent trigger; }; /** * Array of @e num_kyc_logics KYC logic plugins we have loaded. */ static struct TALER_KYCLOGIC_Plugin **kyc_logics; /** * Length of the #kyc_logics array. */ static unsigned int num_kyc_logics; /** * Array of @e num_kyc_checks known types of * KYC checks. */ static struct TALER_KYCLOGIC_KycCheck **kyc_checks; /** * Length of the #kyc_checks array. */ static unsigned int num_kyc_checks; /** * Array of configured triggers. */ static struct TALER_KYCLOGIC_KycTrigger **kyc_triggers; /** * Length of the #kyc_triggers array. */ static unsigned int num_kyc_triggers; /** * Array of configured providers. */ static struct TALER_KYCLOGIC_KycProvider **kyc_providers; /** * Length of the #kyc_providers array. */ static unsigned int num_kyc_providers; enum GNUNET_GenericReturnValue TALER_KYCLOGIC_kyc_trigger_from_string (const char *trigger_s, enum TALER_KYCLOGIC_KycTriggerEvent * trigger) { struct { const char *in; enum TALER_KYCLOGIC_KycTriggerEvent out; } map [] = { { "withdraw", TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW }, { "deposit", TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT }, { "merge", TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE }, { "balance", TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE }, { NULL, 0 } }; for (unsigned int i = 0; NULL != map[i].in; i++) if (0 == strcasecmp (map[i].in, trigger_s)) { *trigger = map[i].out; return GNUNET_OK; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Invalid KYC trigger `%s'\n", trigger_s); return GNUNET_SYSERR; } const char * TALER_KYCLOGIC_kyc_trigger2s (enum TALER_KYCLOGIC_KycTriggerEvent trigger) { switch (trigger) { case TALER_KYCLOGIC_KYC_TRIGGER_WITHDRAW: return "withdraw"; case TALER_KYCLOGIC_KYC_TRIGGER_DEPOSIT: return "deposit"; case TALER_KYCLOGIC_KYC_TRIGGER_P2P_RECEIVE: return "merge"; case TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE: return "balance"; } GNUNET_break (0); return NULL; } enum GNUNET_GenericReturnValue TALER_KYCLOGIC_kyc_user_type_from_string (const char *ut_s, enum TALER_KYCLOGIC_KycUserType *ut) { struct { const char *in; enum TALER_KYCLOGIC_KycTriggerEvent out; } map [] = { { "individual", TALER_KYCLOGIC_KYC_UT_INDIVIDUAL }, { "business", TALER_KYCLOGIC_KYC_UT_BUSINESS }, { NULL, 0 } }; for (unsigned int i = 0; NULL != map[i].in; i++) if (0 == strcasecmp (map[i].in, ut_s)) { *ut = map[i].out; return GNUNET_OK; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Invalid user type `%s'\n", ut_s); return GNUNET_SYSERR; } const char * TALER_KYCLOGIC_kyc_user_type2s (enum TALER_KYCLOGIC_KycUserType ut) { switch (ut) { case TALER_KYCLOGIC_KYC_UT_INDIVIDUAL: return "individual"; case TALER_KYCLOGIC_KYC_UT_BUSINESS: return "business"; } GNUNET_break (0); return NULL; } /** * Load KYC logic plugin. * * @param cfg configuration to use * @param name name of the plugin * @return NULL on error */ static struct TALER_KYCLOGIC_Plugin * load_logic (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *name) { char *lib_name; struct TALER_KYCLOGIC_Plugin *plugin; GNUNET_asprintf (&lib_name, "libtaler_plugin_kyclogic_%s", name); for (unsigned int i = 0; ilibrary_name)) { GNUNET_free (lib_name); return kyc_logics[i]; } plugin = GNUNET_PLUGIN_load (lib_name, (void *) cfg); if (NULL != plugin) plugin->library_name = lib_name; else GNUNET_free (lib_name); GNUNET_array_append (kyc_logics, num_kyc_logics, plugin); return plugin; } /** * Add check type to global array of checks. * First checks if the type already exists, otherwise * adds a new one. * * @param check name of the check * @return pointer into the global list */ static struct TALER_KYCLOGIC_KycCheck * add_check (const char *check) { struct TALER_KYCLOGIC_KycCheck *kc; for (unsigned int i = 0; iname)) return kyc_checks[i]; kc = GNUNET_new (struct TALER_KYCLOGIC_KycCheck); kc->name = GNUNET_strdup (check); GNUNET_array_append (kyc_checks, num_kyc_checks, kc); return kc; } /** * Parse list of checks from @a checks and build an * array of aliases into the global checks array * in @a provided_checks. * * @param[in,out] checks list of checks; clobbered * @param[out] p_checks where to put array of aliases * @param[out] num_p_checks set to length of @a p_checks array */ static void add_checks (char *checks, struct TALER_KYCLOGIC_KycCheck ***p_checks, unsigned int *num_p_checks) { char *sptr; struct TALER_KYCLOGIC_KycCheck **rchecks = NULL; unsigned int num_rchecks = 0; for (char *tok = strtok_r (checks, " ", &sptr); NULL != tok; tok = strtok_r (NULL, " ", &sptr)) { struct TALER_KYCLOGIC_KycCheck *kc; kc = add_check (tok); GNUNET_array_append (rchecks, num_rchecks, kc); } *p_checks = rchecks; *num_p_checks = num_rchecks; } /** * Parse configuration of a KYC provider. * * @param cfg configuration to parse * @param section name of the section to analyze * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue add_provider (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *section) { unsigned long long cost; char *logic; char *ut_s; enum TALER_KYCLOGIC_KycUserType ut; char *checks; struct TALER_KYCLOGIC_Plugin *lp; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_number (cfg, section, "COST", &cost)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "COST", "number required"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "USER_TYPE", &ut_s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "USER_TYPE"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_KYCLOGIC_kyc_user_type_from_string (ut_s, &ut)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "USER_TYPE", "valid user type required"); GNUNET_free (ut_s); return GNUNET_SYSERR; } GNUNET_free (ut_s); if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "LOGIC", &logic)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "LOGIC"); return GNUNET_SYSERR; } lp = load_logic (cfg, logic); if (NULL == lp) { GNUNET_free (logic); GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "LOGIC", "logic plugin could not be loaded"); return GNUNET_SYSERR; } GNUNET_free (logic); if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "PROVIDED_CHECKS", &checks)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "PROVIDED_CHECKS"); return GNUNET_SYSERR; } { struct TALER_KYCLOGIC_KycProvider *kp; kp = GNUNET_new (struct TALER_KYCLOGIC_KycProvider); kp->provider_section_name = section; kp->user_type = ut; kp->logic = lp; kp->cost = cost; add_checks (checks, &kp->provided_checks, &kp->num_checks); GNUNET_free (checks); kp->pd = lp->load_configuration (lp->cls, section); if (NULL == kp->pd) { GNUNET_free (kp); return GNUNET_SYSERR; } GNUNET_array_append (kyc_providers, num_kyc_providers, kp); for (unsigned int i = 0; inum_checks; i++) { struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[i]; GNUNET_array_append (kc->providers, kc->num_providers, kp); } } return GNUNET_OK; } /** * Parse configuration @a cfg in section @a section for * the specification of a KYC trigger. * * @param cfg configuration to parse * @param section configuration section to parse * @return #GNUNET_OK on success */ static enum GNUNET_GenericReturnValue add_trigger (const struct GNUNET_CONFIGURATION_Handle *cfg, const char *section) { char *ot_s; struct TALER_Amount threshold; struct GNUNET_TIME_Relative timeframe; char *checks; enum TALER_KYCLOGIC_KycTriggerEvent ot; if (GNUNET_OK != TALER_config_get_amount (cfg, section, "THRESHOLD", &threshold)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "THRESHOLD", "amount required"); return GNUNET_SYSERR; } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "OPERATION_TYPE", &ot_s)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "OPERATION_TYPE"); return GNUNET_SYSERR; } if (GNUNET_OK != TALER_KYCLOGIC_kyc_trigger_from_string (ot_s, &ot)) { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "OPERATION_TYPE", "valid trigger type required"); GNUNET_free (ot_s); return GNUNET_SYSERR; } GNUNET_free (ot_s); if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (cfg, section, "TIMEFRAME", &timeframe)) { if (TALER_KYCLOGIC_KYC_TRIGGER_WALLET_BALANCE == ot) { timeframe = GNUNET_TIME_UNIT_ZERO; } else { GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, section, "TIMEFRAME", "duration required"); return GNUNET_SYSERR; } } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (cfg, section, "REQUIRED_CHECKS", &checks)) { GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "REQUIRED_CHECKS"); return GNUNET_SYSERR; } { struct TALER_KYCLOGIC_KycTrigger *kt; kt = GNUNET_new (struct TALER_KYCLOGIC_KycTrigger); kt->timeframe = timeframe; kt->threshold = threshold; kt->trigger = ot; add_checks (checks, &kt->required_checks, &kt->num_checks); GNUNET_free (checks); GNUNET_array_append (kyc_triggers, num_kyc_triggers, kt); } return GNUNET_OK; } /** * Closure for #handle_section(). */ struct SectionContext { /** * Configuration to handle. */ const struct GNUNET_CONFIGURATION_Handle *cfg; /** * Result to return, set to false on failures. */ bool result; }; /** * Function to iterate over configuration sections. * * @param cls a `struct SectionContext *` * @param section name of the section */ static void handle_section (void *cls, const char *section) { struct SectionContext *sc = cls; if (0 == strncasecmp (section, "kyc-provider-", strlen ("kyc-provider-"))) { if (GNUNET_OK != add_provider (sc->cfg, section)) sc->result = false; return; } if (0 == strncasecmp (section, "kyc-legitimization-", strlen ("kyc-legitimization-"))) { if (GNUNET_OK != add_trigger (sc->cfg, section)) sc->result = false; return; } } /** * Comparator for qsort. Compares two triggers * by timeframe to sort triggers by time. * * @param p1 first trigger to compare * @param p2 second trigger to compare * @return -1 if p1 < p2, 0 if p1==p2, 1 if p1 > p2. */ static int sort_by_timeframe (const void *p1, const void *p2) { struct TALER_KYCLOGIC_KycTrigger **t1 = (struct TALER_KYCLOGIC_KycTrigger **) p1; struct TALER_KYCLOGIC_KycTrigger **t2 = (struct TALER_KYCLOGIC_KycTrigger **) p2; if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, <, (*t2)->timeframe)) return -1; if (GNUNET_TIME_relative_cmp ((*t1)->timeframe, >, (*t2)->timeframe)) return 1; return 0; } enum GNUNET_GenericReturnValue TALER_KYCLOGIC_kyc_init (const struct GNUNET_CONFIGURATION_Handle *cfg) { struct SectionContext sc = { .cfg = cfg, .result = true }; GNUNET_CONFIGURATION_iterate_sections (cfg, &handle_section, &sc); if (! sc.result) { TALER_KYCLOGIC_kyc_done (); return GNUNET_SYSERR; } /* sanity check: ensure at least one provider exists for any trigger and indidivual or business. */ for (unsigned int i = 0; inum_providers) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "No provider available for required KYC check `%s'\n", kyc_checks[i]->name); TALER_KYCLOGIC_kyc_done (); return GNUNET_SYSERR; } qsort (kyc_triggers, num_kyc_triggers, sizeof (struct TALER_KYCLOGIC_KycTrigger *), &sort_by_timeframe); return GNUNET_OK; } void TALER_KYCLOGIC_kyc_done (void) { for (unsigned int i = 0; irequired_checks, kt->num_checks, 0); GNUNET_free (kt); } GNUNET_array_grow (kyc_triggers, num_kyc_triggers, 0); for (unsigned int i = 0; ilogic->unload_configuration (kp->pd); GNUNET_array_grow (kp->provided_checks, kp->num_checks, 0); GNUNET_free (kp); } GNUNET_array_grow (kyc_providers, num_kyc_providers, 0); for (unsigned int i = 0; ilibrary_name; GNUNET_assert (NULL == GNUNET_PLUGIN_unload (lib_name, lp)); GNUNET_free (lib_name); } GNUNET_array_grow (kyc_logics, num_kyc_logics, 0); for (unsigned int i = 0; iname); GNUNET_free (kc); } GNUNET_array_grow (kyc_checks, num_kyc_checks, 0); } /** * Closure for the #eval_trigger(). */ struct ThresholdTestContext { /** * Total amount so far. */ struct TALER_Amount total; /** * Trigger event to evaluate triggers of. */ enum TALER_KYCLOGIC_KycTriggerEvent event; /** * Offset in the triggers array where we need to start * checking for triggers. All trigges below this * offset were already hit. */ unsigned int start; /** * Array of checks needed so far. */ struct TALER_KYCLOGIC_KycCheck **needed; /** * Pointer to number of entries used in @a needed. */ unsigned int *needed_cnt; /** * Has @e total been initialized yet? */ bool have_total; }; /** * Function called on each @a amount that was found to * be relevant for a KYC check. * * @param cls closure to allow the KYC module to * total up amounts and evaluate rules * @param amount encountered transaction amount * @param date when was the amount encountered * @return #GNUNET_OK to continue to iterate, * #GNUNET_NO to abort iteration * #GNUNET_SYSERR on internal error (also abort itaration) */ static enum GNUNET_GenericReturnValue eval_trigger (void *cls, const struct TALER_Amount *amount, struct GNUNET_TIME_Absolute date) { struct ThresholdTestContext *ttc = cls; struct GNUNET_TIME_Relative duration; bool bump = true; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check with new amount %s\n", TALER_amount2s (amount)); duration = GNUNET_TIME_absolute_get_duration (date); if (ttc->have_total) { if (0 > TALER_amount_add (&ttc->total, &ttc->total, amount)) { GNUNET_break (0); return GNUNET_SYSERR; } } else { ttc->total = *amount; ttc->have_total = true; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check: new total is %s\n", TALER_amount2s (&ttc->total)); for (unsigned int i = ttc->start; ievent != kt->trigger) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check #%u: trigger type does not match\n", i); continue; } duration = GNUNET_TIME_relative_max (duration, kt->timeframe); if (GNUNET_TIME_relative_cmp (kt->timeframe, >, duration)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check #%u: amount is beyond time limit\n", i); if (bump) ttc->start = i; return GNUNET_OK; } if (-1 == TALER_amount_cmp (&ttc->total, &kt->threshold)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check #%u: amount is below treshold\n", i); if (bump) ttc->start = i; bump = false; continue; /* amount too low to trigger */ } /* add check to list of required checks, unless already present... */ for (unsigned int j = 0; jnum_checks; j++) { struct TALER_KYCLOGIC_KycCheck *rc = kt->required_checks[j]; bool found = false; for (unsigned int k = 0; k<*ttc->needed_cnt; k++) if (ttc->needed[k] == rc) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC rule #%u already listed\n", j); found = true; break; } if (! found) { ttc->needed[*ttc->needed_cnt] = rc; (*ttc->needed_cnt)++; } } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "KYC check #%u (%s) is applicable, %u checks needed so far\n", i, ttc->needed[(*ttc->needed_cnt) - 1]->name, *ttc->needed_cnt); } if (bump) return GNUNET_NO; /* we hit all possible triggers! */ return GNUNET_OK; } /** * Closure for the #remove_satisfied(). */ struct RemoveContext { /** * Array of checks needed so far. */ struct TALER_KYCLOGIC_KycCheck **needed; /** * Pointer to number of entries used in @a needed. */ unsigned int *needed_cnt; }; /** * Remove all checks satisfied by @a provider_name from * our list of checks. * * @param cls a `struct RemoveContext` * @param provider_name section name of provider that was already run previously */ static void remove_satisfied (void *cls, const char *provider_name) { struct RemoveContext *rc = cls; for (unsigned int i = 0; iprovider_section_name)) continue; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Provider `%s' satisfied\n", provider_name); for (unsigned int j = 0; jnum_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Provider satisfies check `%s'\n", kc->name); for (unsigned int k = 0; k<*rc->needed_cnt; k++) if (kc == rc->needed[k]) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Removing check `%s' from list\n", kc->name); rc->needed[k] = rc->needed[*rc->needed_cnt - 1]; (*rc->needed_cnt)--; if (0 == *rc->needed_cnt) return; /* for sure finished */ break; } } break; } } const char * TALER_KYCLOGIC_kyc_test_required (enum TALER_KYCLOGIC_KycTriggerEvent event, const struct TALER_PaytoHashP *h_payto, TALER_KYCLOGIC_KycSatisfiedIterator ki, void *ki_cls, TALER_KYCLOGIC_KycAmountIterator ai, void *ai_cls) { struct TALER_KYCLOGIC_KycCheck *needed[num_kyc_checks]; unsigned int needed_cnt = 0; struct GNUNET_TIME_Relative timeframe; unsigned long long min_cost = ULONG_LONG_MAX; unsigned int max_checks = 0; const struct TALER_KYCLOGIC_KycProvider *kp_best = NULL; timeframe = GNUNET_TIME_UNIT_ZERO; for (unsigned int i = 0; itrigger) continue; timeframe = GNUNET_TIME_relative_max (timeframe, kt->timeframe); } { struct GNUNET_TIME_Absolute now; struct ThresholdTestContext ttc = { .event = event, .needed = needed, .needed_cnt = &needed_cnt }; now = GNUNET_TIME_absolute_get (); ai (ai_cls, GNUNET_TIME_absolute_subtract (now, timeframe), &eval_trigger, &ttc); } if (0 == needed_cnt) return NULL; { struct RemoveContext rc = { .needed = needed, .needed_cnt = &needed_cnt }; enum GNUNET_DB_QueryStatus qs; /* Check what provider checks are already satisfied for h_payto (with database), remove those from the 'needed' array. */ qs = ki (ki_cls, h_payto, &remove_satisfied, &rc); GNUNET_break (qs >= 0); // FIXME: handle DB failure more nicely? } if (0 == needed_cnt) return NULL; /* Count maximum number of remaining checks covered by any provider */ for (unsigned int i = 0; inum_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; for (unsigned int k = 0; knum_checks; j++) { const struct TALER_KYCLOGIC_KycCheck *kc = kp->provided_checks[j]; for (unsigned int k = 0; kcost < min_cost) ) { min_cost = kp->cost; kp_best = kp; } } GNUNET_assert (NULL != kp_best); return kp_best->provider_section_name; } enum GNUNET_GenericReturnValue TALER_KYCLOGIC_kyc_get_logic (const char *provider_section_name, struct TALER_KYCLOGIC_Plugin **plugin, struct TALER_KYCLOGIC_ProviderDetails **pd) { for (unsigned int i = 0; iprovider_section_name)) continue; *plugin = kp->logic; *pd = kp->pd; return GNUNET_OK; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Provider `%s' unknown\n", provider_section_name); return GNUNET_SYSERR; } void TALER_KYCLOGIC_kyc_iterate_thresholds ( enum TALER_KYCLOGIC_KycTriggerEvent event, TALER_KYCLOGIC_KycThresholdIterator it, void *it_cls) { for (unsigned int i = 0; itrigger) continue; it (it_cls, &kt->threshold); } } /* end of taler-exchange-httpd_kyc.c */