commit a72b7b392e06508fd32b01dd7ca6193ebe995584 parent 9ddfb38ea4cc9ad846d6fc835c24b7f0feabecbd Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com> Date: Mon, 14 Jul 2025 11:10:20 +0200 Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration Diffstat:
94 files changed, 3083 insertions(+), 1460 deletions(-)
diff --git a/configure.ac b/configure.ac @@ -18,7 +18,7 @@ # This configure file is in the public domain AC_PREREQ([2.69]) -AC_INIT([taler-merchant],[1.0.3],[taler-bug@gnunet.org]) +AC_INIT([taler-merchant],[1.0.4],[taler-bug@gnunet.org]) AC_CONFIG_SRCDIR([src/backend/taler-merchant-httpd.c]) AC_CONFIG_HEADERS([taler_merchant_config.h]) # support for non-recursive builds @@ -334,12 +334,12 @@ AS_IF([test $libtalermhd != 1], libtalerjson=0 AC_MSG_CHECKING([for libtalerjson]) AC_CHECK_HEADERS([taler/taler_json_lib.h], - [AC_CHECK_LIB([talerjson], [TALER_JSON_spec_otp_type], libtalerjson=1)]) + [AC_CHECK_LIB([talerjson], [TALER_JSON_currency_specs_to_json], libtalerjson=1)]) AM_CONDITIONAL(HAVE_TALERJSON, test x$libtalerjson = x1) AS_IF([test $libtalerjson != 1], [AC_MSG_ERROR([[ *** -*** You need libtalerjson >= 0.9.4 to build this program. +*** You need libtalerjson >= 1.1 (API v6) to build this program. *** This library is part of the GNU Taler exchange, available at *** https://taler.net *** ]])]) diff --git a/debian/changelog b/debian/changelog @@ -1,3 +1,9 @@ +taler-merchant (1.0.4) unstable; urgency=low + + * Release 1.0.4. + + -- Florian Dold <florian@dold.me> Mon, 23 Jun 2025 16:52:33 +0200 + taler-merchant (1.0.3) unstable; urgency=low * Release 1.0.3. diff --git a/debian/taler-merchant.postinst b/debian/taler-merchant.postinst @@ -79,6 +79,15 @@ configure) # Cleanup marker file rm -f "$MARKER" + + # Try to generate compressed versions of the SPA + for n in index.html index.css index.js lang.js + do + TDIR="/usr/share/taler-merchant/" + gzip --best - < "${TDIR}/spa/$n" > "${TDIR}/spa/$n.gz" || true + zstd -19 - < "${TDIR}/spa/$n" > "${TDIR}/spa/$n.zstd" || true + done + ;; abort-upgrade | abort-remove | abort-deconfigure) ;; diff --git a/doc/Makefile.am b/doc/Makefile.am @@ -2,6 +2,7 @@ SUBDIRS = . doxygen man_MANS = \ prebuilt/man/taler-merchant-benchmark.1 \ + prebuilt/man/taler-merchant.conf.5 \ prebuilt/man/taler-merchant-config.1 \ prebuilt/man/taler-merchant-dbconfig.1 \ prebuilt/man/taler-merchant-dbinit.1 \ diff --git a/doc/doxygen/taler.doxy b/doc/doxygen/taler.doxy @@ -5,7 +5,7 @@ #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "GNU Taler: Merchant" -PROJECT_NUMBER = 1.0.3 +PROJECT_NUMBER = 1.0.4 PROJECT_LOGO = logo.svg OUTPUT_DIRECTORY = . CREATE_SUBDIRS = YES diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -116,7 +116,9 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-get-instances-ID.h \ taler-merchant-httpd_private-get-instances-ID-kyc.c \ taler-merchant-httpd_private-get-instances-ID-kyc.h \ - taler-merchant-httpd_private-get-pos.c \ + taler-merchant-httpd_private-get-instances-ID-tokens.c \ + taler-merchant-httpd_private-get-instances-ID-tokens.h \ + taler-merchant-httpd_private-get-pos.c \ taler-merchant-httpd_private-get-pos.h \ taler-merchant-httpd_private-get-products.c \ taler-merchant-httpd_private-get-products.h \ @@ -130,6 +132,8 @@ taler_merchant_httpd_SOURCES = \ taler-merchant-httpd_private-get-otp-devices.h \ taler-merchant-httpd_private-get-otp-devices-ID.c \ taler-merchant-httpd_private-get-otp-devices-ID.h \ + taler-merchant-httpd_private-get-incoming.c \ + taler-merchant-httpd_private-get-incoming.h \ taler-merchant-httpd_private-get-transfers.c \ taler-merchant-httpd_private-get-transfers.h \ taler-merchant-httpd_private-get-templates.c \ @@ -279,6 +283,7 @@ taler_merchant_reconciliation_LDADD = \ -ltalerjson \ -ltalerutil \ -ltalerpq \ + -lgnunetpq \ -lgnunetjson \ -lgnunetcurl \ -lgnunetutil \ diff --git a/src/backend/taler-merchant-depositcheck.c b/src/backend/taler-merchant-depositcheck.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024, 2025 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 @@ -424,7 +424,6 @@ deposit_get_cb ( case MHD_HTTP_OK: { enum GNUNET_DB_QueryStatus qs; - bool cleared = false; GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange returned wire transfer over %s for deposited coin %s\n", @@ -433,25 +432,10 @@ deposit_get_cb ( qs = db_plugin->insert_deposit_to_transfer ( db_plugin->cls, w->deposit_serial, - &dr->details.ok, - &cleared); - if (qs < 0) - { - GNUNET_break (0); - GNUNET_SCHEDULER_shutdown (); - return; - } - if (! cleared) - { - qs = db_plugin->update_deposit_confirmation_status ( - db_plugin->cls, - w->deposit_serial, - true, /* this failed, wire_pending remains true */ - GNUNET_TIME_absolute_to_timestamp (future_retry), - w->retry_backoff, - "wire transfer unknown"); - } - if (qs < 0) + &w->h_wire, + exchange_url, + &dr->details.ok); + if (qs <= 0) { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); @@ -493,9 +477,10 @@ deposit_get_cb ( qs = db_plugin->update_deposit_confirmation_status ( db_plugin->cls, w->deposit_serial, - true, /* wire_pending is still true! */ + true, /* need to try again in the future! */ GNUNET_TIME_absolute_to_timestamp (future_retry), - w->retry_backoff, + MHD_HTTP_ACCEPTED, + TALER_EC_NONE, "Exchange reported 202 Accepted but no KYC block"); if (qs < 0) { @@ -517,9 +502,10 @@ deposit_get_cb ( qs = db_plugin->update_deposit_confirmation_status ( db_plugin->cls, w->deposit_serial, - true, + true /* need to try again in the future */, GNUNET_TIME_absolute_to_timestamp (future_retry), - w->retry_backoff, + MHD_HTTP_ACCEPTED, + TALER_EC_NONE, "Exchange reported 202 Accepted due to KYC/AML block"); if (qs < 0) { @@ -533,32 +519,50 @@ deposit_get_cb ( default: { enum GNUNET_DB_QueryStatus qs; - char *msg; + bool retry_needed; GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s returned tracking failure for deposited coin %s\n", + "Exchange %s returned tracking failure for deposited coin %s: %u\n", exchange_url, - TALER_B2S (&w->coin_pub)); - GNUNET_asprintf (&msg, - "Unexpected exchange status %u (#%d, %s)\n", - dr->hr.http_status, - (int) dr->hr.ec, - dr->hr.hint); + TALER_B2S (&w->coin_pub), + dr->hr.http_status); + /* rough classification by HTTP status group */ + switch (dr->hr.http_status / 100) + { + case 0: + /* timeout */ + retry_needed = true; + break; + case 1: + case 2: + case 3: + /* very strange */ + retry_needed = false; + break; + case 4: + /* likely fatal */ + retry_needed = false; + break; + case 5: + /* likely transient */ + retry_needed = true; + break; + } qs = db_plugin->update_deposit_confirmation_status ( db_plugin->cls, w->deposit_serial, - true, /* this failed, wire_pending remains true */ + retry_needed, GNUNET_TIME_absolute_to_timestamp (future_retry), - w->retry_backoff, - msg); - GNUNET_free (msg); + (uint32_t) dr->hr.http_status, + dr->hr.ec, + dr->hr.hint); if (qs < 0) { GNUNET_break (0); GNUNET_SCHEDULER_shutdown (); return; } - return; + break; } } /* end switch */ @@ -569,9 +573,14 @@ deposit_get_cb ( GNUNET_free (w->instance_id); GNUNET_free (w); GNUNET_assert (NULL != keys); - if ( (w_count < CONCURRENCY_LIMIT / 2) || - (0 == w_count) ) + if (0 == w_count) { + /* We only SELECT() again after having finished + all requests, as otherwise we'll most like + just SELECT() those again that are already + being requested; alternatively, we could + update the retry_time already on SELECT(), + but this should be easier on the DB. */ if (NULL != task) GNUNET_SCHEDULER_cancel (task); task = GNUNET_SCHEDULER_add_now (&select_work, @@ -590,7 +599,8 @@ deposit_get_cb ( * @param h_contract_terms hash of the contract terms * @param merchant_priv private key of the merchant * @param instance_id row ID of the instance - * @param h_wire hash of the merchant's wire account into * @param amount_with_fee amount the exchange will deposit for this coin + * @param h_wire hash of the merchant's wire account into + * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin which the deposit was made * @param coin_pub public key of the deposited coin */ @@ -599,7 +609,7 @@ pending_deposits_cb ( void *cls, uint64_t deposit_serial, struct GNUNET_TIME_Absolute wire_deadline, - struct GNUNET_TIME_Relative retry_backoff, + struct GNUNET_TIME_Absolute retry_time, const struct TALER_PrivateContractHashP *h_contract_terms, const struct TALER_MerchantPrivateKeyP *merchant_priv, const char *instance_id, @@ -609,20 +619,30 @@ pending_deposits_cb ( const struct TALER_CoinSpendPublicKeyP *coin_pub) { struct ExchangeInteraction *w; + struct GNUNET_TIME_Absolute mx + = GNUNET_TIME_absolute_max (wire_deadline, + retry_time); + struct GNUNET_TIME_Relative retry_backoff; (void) cls; - if (GNUNET_TIME_absolute_is_future (wire_deadline)) + if (GNUNET_TIME_absolute_is_future (mx)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Pending deposit has deadline in the future at %s\n", - GNUNET_TIME_absolute2s (wire_deadline)); - run_at (wire_deadline); + "Pending deposit should be checked next at %s\n", + GNUNET_TIME_absolute2s (mx)); + run_at (mx); return; } + if (GNUNET_TIME_absolute_is_zero (retry_time)) + retry_backoff = GNUNET_TIME_absolute_get_duration (wire_deadline); + else + retry_backoff = GNUNET_TIME_absolute_get_difference (wire_deadline, + retry_time); w = GNUNET_new (struct ExchangeInteraction); w->deposit_serial = deposit_serial; w->wire_deadline = wire_deadline; - w->retry_backoff = GNUNET_TIME_STD_BACKOFF (retry_backoff); + w->retry_backoff = GNUNET_TIME_randomized_backoff (retry_backoff, + GNUNET_TIME_UNIT_DAYS); w->h_contract_terms = *h_contract_terms; w->merchant_priv = *merchant_priv; w->h_wire = *h_wire; @@ -766,6 +786,14 @@ select_work (void *cls) static struct GNUNET_OS_Process * start_worker (const char *base_url) { + char toff[30]; + long long zo; + + zo = GNUNET_TIME_get_offset (); + GNUNET_snprintf (toff, + sizeof (toff), + "%lld", + zo); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Launching worker for exchange `%s' using `%s`\n", base_url, @@ -782,6 +810,7 @@ start_worker (const char *base_url) "taler-merchant-depositcheck", "-e", base_url, "-L", "INFO", + "-T", toff, test_mode ? "-t" : NULL, NULL); return GNUNET_OS_start_process ( @@ -794,6 +823,7 @@ start_worker (const char *base_url) "-c", cfg_filename, "-e", base_url, "-L", "INFO", + "-T", toff, test_mode ? "-t" : NULL, NULL); } diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -50,9 +50,11 @@ #include "taler-merchant-httpd_private-get-accounts-ID.h" #include "taler-merchant-httpd_private-get-categories.h" #include "taler-merchant-httpd_private-get-categories-ID.h" +#include "taler-merchant-httpd_private-get-incoming.h" #include "taler-merchant-httpd_private-get-instances.h" #include "taler-merchant-httpd_private-get-instances-ID.h" #include "taler-merchant-httpd_private-get-instances-ID-kyc.h" +#include "taler-merchant-httpd_private-get-instances-ID-tokens.h" #include "taler-merchant-httpd_private-get-pos.h" #include "taler-merchant-httpd_private-get-products.h" #include "taler-merchant-httpd_private-get-products-ID.h" @@ -164,6 +166,12 @@ struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map; int TMH_strict_v19; /** + * #GNUNET_YES if authentication is disabled (For testing only!!). + * (Default is #GNUNET_NO) + */ +int TMH_auth_disabled; + +/** * How long do we need to keep information on paid contracts on file for tax * or other legal reasons? Used to block deletions for younger transaction * data. @@ -250,14 +258,20 @@ struct ScopePermissionMap struct ScopePermissionMap scope_permissions[] = { /* Deprecated since v19 */ { - .as = TMH_AS_ADMIN, + .as = TMH_AS_ALL, .name = "write", .permissions = "*" }, - /* Full access */ + /* Full access for SPA */ + { + .as = TMH_AS_ALL, + .name = "all", + .permissions = "*" + }, + /* Full access for SPA */ { - .as = TMH_AS_ADMIN, - .name = "admin", + .as = TMH_AS_SPA, + .name = "spa", .permissions = "*" }, /* Read-only access */ @@ -336,8 +350,8 @@ permission_in_scope (const char *permission_required, { char *permissions; const char *perms_tmp; - bool is_read_perm; - bool is_write_perm; + bool is_read_perm = false; + bool is_write_perm = false; bool refreshable; const char *last_dash; @@ -358,10 +372,9 @@ permission_in_scope (const char *permission_required, "-read")); } - if (refreshable && - (0 == strcmp ("token-refresh", - permission_required)) ) - return true; + if (0 == strcmp ("token-refresh", + permission_required)) + return refreshable; permissions = GNUNET_strdup (perms_tmp); { const char *perm = strtok (permissions, @@ -473,6 +486,21 @@ TMH_get_scope_by_name (const char *name) } +const char* +TMH_get_name_by_scope (enum TMH_AuthScope scope, bool *refreshable) +{ + *refreshable = scope & TMH_AS_REFRESHABLE; + for (unsigned int i = 0; TMH_AS_NONE != scope_permissions[i].as; i++) + { + /* We ignore the TMH_AS_REFRESHABLE bit */ + if ( (scope & ~TMH_AS_REFRESHABLE) == + (scope_permissions[i].as & ~TMH_AS_REFRESHABLE) ) + return scope_permissions[i].name; + } + return NULL; +} + + enum GNUNET_GenericReturnValue TMH_check_auth (const char *password, struct TALER_MerchantAuthenticationSaltP *salt, @@ -1030,6 +1058,116 @@ full_url_track_callback (void *cls, /** + * Function used to process Basic authorization header value. + * Sets correct scope in the auth_scope parameter of the + * #TMH_HandlerContext. + * + * @param hc the handler context + * @param authn_s the value of the authorization header + */ +static void +process_basic_auth (struct TMH_HandlerContext *hc, const char*authn_s) +{ + /* Handle token endpoint slightly differently: Only allow + * instance password (Basic auth) to retrieve access token. + * We need to handle authorization with Basic auth here first + * The only time we need to handle authentication like this is + * for the token endpoint! + */ + if ( (0 != strncmp (hc->rh->url_prefix, + "/token", + strlen ("/token"))) || + (0 != strncmp (MHD_HTTP_METHOD_POST, + hc->rh->method, + strlen (MHD_HTTP_METHOD_POST))) || + (NULL == hc->instance)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Called endpoint `%s' with Basic authentication. Rejecting...\n", + hc->rh->url_prefix); + hc->auth_scope = TMH_AS_NONE; + return; + } + if (GNUNET_OK == + check_auth_instance (authn_s, + hc->instance)) + hc->auth_scope = TMH_AS_ALL; + else + hc->auth_scope = TMH_AS_NONE; +} + + +/** + * Function used to process Bearer authorization header value. + * Sets correct scope in the auth_scope parameter of the + * #TMH_HandlerContext.. + * + * @param hc the handler context + * @param authn_s the value of the authorization header + * @return TALER_EC_NONE on success. + */ +static enum TALER_ErrorCode +process_bearer_auth (struct TMH_HandlerContext *hc, const char*authn_s) +{ + if (NULL == hc->instance) + { + hc->auth_scope = TMH_AS_NONE; + return TALER_EC_NONE; + } + if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) + { + /* hash zero means no authentication for instance */ + hc->auth_scope = TMH_AS_ALL; + return TALER_EC_NONE; + } + { + enum TALER_ErrorCode ec; + + ec = TMH_check_token (authn_s, + hc->instance->settings.id, + &hc->auth_scope); + if (TALER_EC_NONE != ec) + { + char *dec; + size_t dec_len; + const char *token; + + /* NOTE: Deprecated, remove sometime after v1.1 */ + if (0 != strncasecmp (authn_s, + RFC_8959_PREFIX, + strlen (RFC_8959_PREFIX))) + { + GNUNET_break_op (0); + hc->auth_scope = TMH_AS_NONE; + return ec; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Trying deprecated secret-token:password API authN\n"); + token = authn_s + strlen (RFC_8959_PREFIX); + dec_len = GNUNET_STRINGS_urldecode (token, + strlen (token), + &dec); + if ( (0 == dec_len) || + (GNUNET_OK != + TMH_check_auth (dec, + &hc->instance->auth.auth_salt, + &hc->instance->auth.auth_hash)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Login failed\n"); + hc->auth_scope = TMH_AS_NONE; + GNUNET_free (dec); + return TALER_EC_NONE; + } + hc->auth_scope = TMH_AS_ALL; + GNUNET_free (dec); + } + } + return TALER_EC_NONE; +} + + +/** * A client has requested the given url using the given method * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT, * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc). The callback @@ -1422,6 +1560,14 @@ url_handler (void *cls, .allow_deleted_instance = true, .handler = &TMH_private_get_transfers }, + /* GET /incoming: */ + { + .url_prefix = "/incoming", + .permission = "transfers-read", + .method = MHD_HTTP_METHOD_GET, + .allow_deleted_instance = true, + .handler = &TMH_private_get_incoming + }, /* POST /otp-devices: */ { .url_prefix = "/otp-devices", @@ -1604,6 +1750,13 @@ url_handler (void *cls, .handler = &TMH_private_delete_account_ID, .have_id_segment = true }, + /* GET /tokens: */ + { + .url_prefix = "/tokens", + .permission = "tokens-read", + .method = MHD_HTTP_METHOD_GET, + .handler = &TMH_private_get_instances_ID_tokens, + }, /* POST /token: */ { .url_prefix = "/token", @@ -1613,6 +1766,14 @@ url_handler (void *cls, /* Body should be tiny. */ .max_upload = 1024 }, + /* DELETE /tokens/$SERIAL: */ + { + .url_prefix = "/tokens/", + .permission = "tokens-write", + .method = MHD_HTTP_METHOD_DELETE, + .handler = &TMH_private_delete_instances_ID_token_SERIAL, + .have_id_segment = true + }, /* DELETE /token: */ { .url_prefix = "/token", @@ -2222,12 +2383,6 @@ url_handler (void *cls, if (NULL != auth) { - /* We _only_ complain about malformed auth headers if - authorization was truly required (#6737). This helps - in case authorization was disabled in the backend - because some reverse proxy is already doing it, and - then that reverse proxy may forward malformed auth - headers to the backend. */ extract_auth (&auth, &is_basic_auth); if (NULL == auth) @@ -2236,108 +2391,33 @@ url_handler (void *cls, } /* If we have zero configured instances (not even ones that have been - purged), THEN we accept anything (no access - control), as we then also have no data to protect. */ - if (0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) + purged) or explicitly disabled authentication, THEN we accept anything + (no access control), as we then also have no data to protect. */ + if ((0 == GNUNET_CONTAINER_multihashmap_size (TMH_by_id_map)) || + (GNUNET_YES == TMH_auth_disabled)) { - hc->auth_scope = TMH_AS_ADMIN; + hc->auth_scope = TMH_AS_ALL; } else if (is_basic_auth) { - /* Handle token endpoint slightly differently: Only allow - * instance password (Basic auth) to retrieve access token. - * We need to handle authorization with Basic auth here first - * The only time we need to handle authentication like this is - * for the token endpoint! - */ - if ( (0 != strncmp (hc->rh->url_prefix, - "/token", - strlen ("/token"))) || - (0 != strncmp (MHD_HTTP_METHOD_POST, - hc->rh->method, - strlen (MHD_HTTP_METHOD_POST))) || - (NULL == hc->instance)) - { - // FIXME this should never happen, but according to the comment below, - // We must not error out here for some reason that has to do with - // disabled authZ behind reverse proxy...? - hc->auth_scope = TMH_AS_NONE; - } - else - { - if (GNUNET_OK == - check_auth_instance (auth, - hc->instance)) - hc->auth_scope = TMH_AS_ADMIN; - else - hc->auth_scope = TMH_AS_NONE; - } + process_basic_auth (hc, auth); } else /* Check bearer token */ { - if (NULL != hc->instance) + enum TALER_ErrorCode ec; + ec = process_bearer_auth (hc, auth); + if (TALER_EC_NONE != ec) { - if (GNUNET_is_zero (&hc->instance->auth.auth_hash)) - { - /* hash zero means no authentication for instance */ - hc->auth_scope = TMH_AS_ADMIN; - } - else - { - enum TALER_ErrorCode ec; - - ec = TMH_check_token (auth, - hc->instance->settings.id, - &hc->auth_scope); - if (TALER_EC_NONE != ec) - { - char *dec; - size_t dec_len; - const char *token; - - /* NOTE: Deprecated, remove sometime after v1.1 */ - if (0 != strncasecmp (auth, - RFC_8959_PREFIX, - strlen (RFC_8959_PREFIX))) - { - GNUNET_break_op (0); - return TALER_MHD_reply_with_ec (connection, - ec, - NULL); - } - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Trying deprecated secret-token:password API authN\n"); - token = auth + strlen (RFC_8959_PREFIX); - dec_len = GNUNET_STRINGS_urldecode (token, - strlen (token), - &dec); - if ( (0 == dec_len) || - (GNUNET_OK != - TMH_check_auth (dec, - &hc->instance->auth.auth_salt, - &hc->instance->auth.auth_hash)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Login failed\n"); - hc->auth_scope = TMH_AS_NONE; - } - else - { - hc->auth_scope = TMH_AS_ADMIN; - } - GNUNET_free (dec); - } - } - } - else - { - hc->auth_scope = TMH_AS_NONE; + return TALER_MHD_reply_with_ec (connection, + ec, + NULL); } } /* We grant access if: - - scope is 'all' - - rh has an explicit non-NONE scope that matches - - scope is 'read only' and we have a GET request */ + - Endpoint does not require permissions + - Authorization scope of bearer token contains permissions + required by endpoint. + */ if ( (NULL != hc->rh->permission) && (! permission_in_scope (hc->rh->permission, hc->auth_scope))) @@ -2683,7 +2763,7 @@ run (void *cls, "merchant", &TMH_currency)) { - + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2693,11 +2773,12 @@ run (void *cls, &TMH_num_cspecs, &TMH_cspecs)) { + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } - if (GNUNET_SYSERR != + if (GNUNET_SYSERR == (TMH_strict_v19 = GNUNET_CONFIGURATION_get_value_yesno (cfg, "merchant", "STRICT_PROTOCOL_V19"))) @@ -2707,6 +2788,18 @@ run (void *cls, "STRICT_PROTOCOL_V19"); TMH_strict_v19 = GNUNET_NO; } + if (GNUNET_SYSERR == + (TMH_auth_disabled = GNUNET_CONFIGURATION_get_value_yesno (cfg, + "merchant", + "DISABLE_AUTHENTICATION"))) + { + TMH_auth_disabled = GNUNET_NO; + } + if (GNUNET_YES == TMH_auth_disabled) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "DANGEROUS: Endpoint Authentication disabled!"); + } if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_time (cfg, "merchant", @@ -2716,6 +2809,7 @@ run (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, "merchant", "LEGAL_PRESERVATION"); + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2731,6 +2825,7 @@ run (void *cls, "merchant", "BASE_URL", "Needs to start with 'http://' or 'https://'"); + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2745,6 +2840,7 @@ run (void *cls, { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to setup templates\n"); + global_ret = EXIT_NOTINSTALLED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2753,6 +2849,7 @@ run (void *cls, { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to load single page app\n"); + global_ret = EXIT_NOTINSTALLED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2762,12 +2859,14 @@ run (void *cls, (TMH_by_id_map = GNUNET_CONTAINER_multihashmap_create (4, GNUNET_YES))) { + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } if (NULL == (TMH_db = TALER_MERCHANTDB_plugin_load (cfg))) { + global_ret = EXIT_NOTINSTALLED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2776,12 +2875,14 @@ run (void *cls, { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to connect to database. Consider running taler-merchant-dbinit!\n"); + global_ret = EXIT_FAILURE; GNUNET_SCHEDULER_shutdown (); return; } elen = TMH_EXCHANGES_init (config); if (GNUNET_SYSERR == elen) { + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } @@ -2789,6 +2890,7 @@ run (void *cls, { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Fatal: no trusted exchanges configured. Exiting.\n"); + global_ret = EXIT_NOTCONFIGURED; GNUNET_SCHEDULER_shutdown (); return; } diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -451,8 +451,15 @@ enum TMH_AuthScope /** * Full access is granted to everything. + * We want to deprecate and remove this! + * Old scope "write" */ - TMH_AS_ADMIN = 7 | 1 << 30, + TMH_AS_ALL = 7 | 1 << 30, + + /** + * Full access is granted to everything. + */ + TMH_AS_SPA = 8, /** * /login access to renew the token is OK. @@ -750,6 +757,18 @@ extern struct GNUNET_CONTAINER_MultiHashMap *TMH_by_id_map; */ extern struct GNUNET_TIME_Relative TMH_legal_expiration; +/** + * #GNUNET_YES if protocol version 19 is strictly enforced. + * (Default is #GNUNET_NO) + */ +extern int TMH_strict_v19; + +/** + * #GNUNET_YES if authentication is disabled (For testing only!!). + * (Default is #GNUNET_NO) + */ +extern int TMH_auth_disabled; + /** * Callback that frees an instances removing @@ -863,4 +882,14 @@ TMH_scope_is_subset (enum TMH_AuthScope as, enum TMH_AuthScope candidate); enum TMH_AuthScope TMH_get_scope_by_name (const char *name); +/** + * Return the name corresponding to @a scop. + * + * @param scope the scope to look for + * @param[out] refreshable outputs if scope value was refreshable + * @return the name corresponding to the scope, or NULL. + */ +const char* +TMH_get_name_by_scope (enum TMH_AuthScope scope, bool *refreshable); + #endif diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "19:0:16" +#define MERCHANT_PROTOCOL_VERSION "20:0:17" /** @@ -104,8 +104,8 @@ MH_handler_config (const struct TMH_RequestHandler *rh, json_object_set_new ( specs, cspec->currency, - TALER_CONFIG_currency_specs_to_json - (cspec))); + TALER_JSON_currency_specs_to_json ( + cspec))); } response = TALER_MHD_MAKE_JSON_PACK ( GNUNET_JSON_pack_string ("currency", diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -607,8 +607,13 @@ TMH_check_auth_config (struct MHD_Connection *connection, { GNUNET_break_op (0); } - else if (0 == strcmp (auth_method, - "external")) + else if ((GNUNET_YES != TMH_strict_v19) && + (0 == strcmp (auth_method, + "external"))) + { + auth_wellformed = true; + } + else if (GNUNET_YES == TMH_auth_disabled) { auth_wellformed = true; } diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -996,7 +996,10 @@ batch_deposit_transaction (const struct ExchangeGroup *eg, &dc->cdd.coin_sig, &dc->cdd.amount, &dc->deposit_fee, - &dc->refund_fee); + &dc->refund_fee, + GNUNET_TIME_absolute_add ( + pc->check_contract.contract_terms->wire_deadline.abs_time, + GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES))); if (qs < 0) return qs; GNUNET_break (qs > 0); diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.c @@ -29,6 +29,60 @@ MHD_RESULT +TMH_private_delete_instances_ID_token_SERIAL (const struct TMH_RequestHandler * + rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TMH_MerchantInstance *mi = hc->instance; + enum GNUNET_DB_QueryStatus qs; + unsigned long long serial; + char dummy; + + GNUNET_assert (NULL != mi); + GNUNET_assert (NULL != hc->infix); + if (1 != sscanf (hc->infix, + "%llu%c", + &serial, + &dummy)) + { + GNUNET_break_op (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "serial must be a number"); + } + + + qs = TMH_db->delete_login_token_serial (TMH_db->cls, + mi->settings.id, + serial); + switch (qs) + { + case GNUNET_DB_STATUS_HARD_ERROR: + case GNUNET_DB_STATUS_SOFT_ERROR: + GNUNET_break (0); + return TALER_MHD_reply_with_ec (connection, + TALER_EC_GENERIC_DB_STORE_FAILED, + "delete_login_token_by_serial"); + case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_CATEGORY_UNKNOWN, + hc->infix); + case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: + return TALER_MHD_reply_static (connection, + MHD_HTTP_NO_CONTENT, + NULL, + NULL, + 0); + } + GNUNET_break (0); + return MHD_NO; +} + + +MHD_RESULT TMH_private_delete_instances_ID_token (const struct TMH_RequestHandler *rh, struct MHD_Connection *connection, struct TMH_HandlerContext *hc) @@ -90,11 +144,11 @@ TMH_private_delete_instances_ID_token (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_DB_STORE_FAILED, "delete_login_token"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* No 404, as the login token must have existed - when we got the request as it was accepted as - valid. So we can only get here due to concurrent - modification, and then the client should still - simply see the success. Hence, fall-through */ + /* No 404, as the login token must have existed + when we got the request as it was accepted as + valid. So we can only get here due to concurrent + modification, and then the client should still + simply see the success. Hence, fall-through */ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, diff --git a/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.h b/src/backend/taler-merchant-httpd_private-delete-instances-ID-token.h @@ -26,6 +26,20 @@ #define TALER_MERCHANT_HTTPD_PRIVATE_DELETE_INSTANCES_ID_TOKEN_H #include "taler-merchant-httpd.h" +/** + * Delete login token for an instance by serial. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_delete_instances_ID_token_SERIAL ( + const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + /** * Delete login token for an instance. diff --git a/src/backend/taler-merchant-httpd_private-get-incoming.c b/src/backend/taler-merchant-httpd_private-get-incoming.c @@ -0,0 +1,193 @@ +/* + This file is part of TALER + (C) 2025 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-incoming.c + * @brief implement API for obtaining a list of expected incoming wire transfers + * @author Christian Grothoff + */ +#include "platform.h" +#include <jansson.h> +#include <taler/taler_json_lib.h> +#include "taler-merchant-httpd_private-get-incoming.h" + + +/** + * Function called with information about a wire transfer. + * Generate a response (array entry) based on the given arguments. + * + * @param cls closure with a `json_t *` array to build up the response + * @param expected_credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown + * @param wtid wire transfer identifier + * @param payto_uri target account that received the wire transfer + * @param exchange_url base URL of the exchange that made the wire transfer + * @param transfer_serial_id serial number identifying the transfer in the backend + * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS + * if it did not yet happen + * @param confirmed true if the merchant acknowledged the wire transfer reception + * @param validated true if the reconciliation succeeded + * @param last_http_status HTTP status of our last request to the exchange for this transfer + * @param last_ec last error code we got back (otherwise #TALER_EC_NONE) + * @param last_error_detail last detail we got back (or NULL for none) + */ +static void +incoming_cb (void *cls, + const struct TALER_Amount *expected_credit_amount, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + uint64_t expected_transfer_serial_id, + struct GNUNET_TIME_Timestamp execution_time, + bool confirmed, + bool validated, + unsigned int last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_error_detail) +{ + json_t *ja = cls; + json_t *r; + + r = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("expected_credit_amount", + expected_credit_amount)), + GNUNET_JSON_pack_data_auto ("wtid", + wtid), + TALER_JSON_pack_full_payto ("payto_uri", + payto_uri), + GNUNET_JSON_pack_string ("exchange_url", + exchange_url), + GNUNET_JSON_pack_uint64 ("expected_transfer_serial_id", + expected_transfer_serial_id), + GNUNET_JSON_pack_timestamp ("execution_time", + execution_time), + GNUNET_JSON_pack_bool ("validated", + validated), + GNUNET_JSON_pack_bool ("confirmed", + confirmed), + GNUNET_JSON_pack_uint64 ("last_http_status", + last_http_status), + GNUNET_JSON_pack_uint64 ("last_ec", + last_ec), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("last_error_detail", + last_error_detail))); + GNUNET_assert (0 == + json_array_append_new (ja, + r)); +} + + +/** + * Manages a GET /private/incoming call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_incoming (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + struct TALER_FullPayto payto_uri = { + .full_payto = NULL + }; + struct GNUNET_TIME_Timestamp before = GNUNET_TIME_UNIT_FOREVER_TS; + struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; + int64_t limit = -20; + uint64_t offset; + enum TALER_EXCHANGE_YesNoAll confirmed; + enum TALER_EXCHANGE_YesNoAll verified; + + (void) rh; + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + TALER_MHD_parse_request_yna (connection, + "verified", + TALER_EXCHANGE_YNA_ALL, + &verified); + TALER_MHD_parse_request_yna (connection, + "confirmed", + TALER_EXCHANGE_YNA_ALL, + &confirmed); + TALER_MHD_parse_request_timestamp (connection, + "before", + &before); + TALER_MHD_parse_request_timestamp (connection, + "after", + &after); + { + const char *esc_payto; + + esc_payto = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "payto_uri"); + if (NULL != esc_payto) + { + payto_uri.full_payto + = GNUNET_strdup (esc_payto); + (void) MHD_http_unescape (payto_uri.full_payto); + } + } + TMH_db->preflight (TMH_db->cls); + { + json_t *ja; + enum GNUNET_DB_QueryStatus qs; + + ja = json_array (); + GNUNET_assert (NULL != ja); + qs = TMH_db->lookup_expected_transfers (TMH_db->cls, + hc->instance->settings.id, + payto_uri, + before, + after, + limit, + offset, + confirmed, + verified, + &incoming_cb, + ja); + GNUNET_free (payto_uri.full_payto); + if (0 > qs) + { + /* Simple select queries should not cause serialization issues */ + GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs); + /* Always report on hard error as well to enable diagnostics */ + GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "incoming"); + } + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("incoming", + ja)); + } +} + + +/* end of taler-merchant-httpd_private-get-incoming.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-incoming.h b/src/backend/taler-merchant-httpd_private-get-incoming.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-incoming.h + * @brief headers for GET /incoming handler + * @author Christian Grothoff + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INCOMING_H +#include <microhttpd.h> +#include "taler-merchant-httpd.h" + + +/** + * Manages a GET /private/incoming call. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_incoming (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + + +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -375,12 +375,14 @@ kyc_change_cb (void *cls, { struct KycContext *kc = cls; - GNUNET_assert (GNUNET_YES == kc->suspended); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Resuming KYC with gateway timeout\n"); - kc->suspended = GNUNET_NO; - MHD_resume_connection (kc->connection); - TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + if (GNUNET_YES == kc->suspended) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Resuming KYC with gateway timeout\n"); + kc->suspended = GNUNET_NO; + MHD_resume_connection (kc->connection); + TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */ + } } diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.c @@ -0,0 +1,114 @@ +/* + This file is part of TALER + (C) 2025 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-instances-ID-tokens.c + * @brief implement GET /tokens + * @author Martin Schanzenbach + */ +#include "platform.h" +#include "taler-merchant-httpd_private-get-instances-ID-tokens.h" + + +/** + * Add token details to our JSON array. + * + * @param cls a `json_t *` JSON array to build + * @param product_serial serial (row) number of the product in the database + * @param product_id ID of the product + */ +static void +add_token (void *cls, + struct GNUNET_TIME_Timestamp creation_time, + struct GNUNET_TIME_Timestamp expiration_time, + uint32_t scope, + const char *description, + uint64_t serial) +{ + json_t *pa = cls; + bool refreshable; + const char*as; + + as = TMH_get_name_by_scope (scope, &refreshable); + if (NULL == as) + { + GNUNET_break (0); + return; + } + GNUNET_assert (0 == + json_array_append_new ( + pa, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("creation_time", + creation_time), + GNUNET_JSON_pack_timestamp ("expiration_time", + expiration_time), + GNUNET_JSON_pack_string ("scope", + as), + GNUNET_JSON_pack_bool ("refreshable", + refreshable), + GNUNET_JSON_pack_string ("description", + description), + GNUNET_JSON_pack_uint64 ("serial", + serial)))); +} + + +MHD_RESULT +TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc) +{ + json_t *ta; + enum GNUNET_DB_QueryStatus qs; + int64_t limit; + uint64_t offset; + + limit = -20; /* default */ + TALER_MHD_parse_request_snumber (connection, + "limit", + &limit); + if (limit < 0) + offset = INT64_MAX; + else + offset = 0; + TALER_MHD_parse_request_number (connection, + "offset", + &offset); + ta = json_array (); + GNUNET_assert (NULL != ta); + qs = TMH_db->lookup_login_tokens (TMH_db->cls, + hc->instance->settings.id, + offset, + limit, + &add_token, + ta); + if (0 > qs) + { + GNUNET_break (0); + json_decref (ta); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL); + } + return TALER_MHD_REPLY_JSON_PACK (connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_array_steal ("tokens", + ta)); +} + + +/* end of taler-merchant-httpd_private-get-products.c */ diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.h b/src/backend/taler-merchant-httpd_private-get-instances-ID-tokens.h @@ -0,0 +1,41 @@ +/* + This file is part of TALER + (C) 2025 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file taler-merchant-httpd_private-get-instances-ID-tokens.h + * @brief implement GET /tokens + * @author Martin Schanzenbach + */ +#ifndef TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H +#define TALER_MERCHANT_HTTPD_PRIVATE_GET_INSTANCES_ID_TOKENS_H + +#include "taler-merchant-httpd.h" + + +/** + * Handle a GET "/tokens" request. + * + * @param rh context of the handler + * @param connection the MHD connection to handle + * @param[in,out] hc context with further information about the request + * @return MHD result code + */ +MHD_RESULT +TMH_private_get_instances_ID_tokens (const struct TMH_RequestHandler *rh, + struct MHD_Connection *connection, + struct TMH_HandlerContext *hc); + +/* end of taler-merchant-httpd_private-get-instances-ID-tokens.h */ +#endif diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID.c b/src/backend/taler-merchant-httpd_private-get-instances-ID.c @@ -64,7 +64,12 @@ get_instances_ID (struct TMH_MerchantInstance *mi, GNUNET_JSON_pack_bool ("active", wm->active)))); } - + if (GNUNET_YES == TMH_strict_v19) + { + // When pre v19 is deprecated this if guard can be removed + // and the code below should never return "external" + GNUNET_assert (! GNUNET_is_zero (&mi->auth.auth_hash)); + } auth = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("method", GNUNET_is_zero (&mi->auth.auth_hash) diff --git a/src/backend/taler-merchant-httpd_private-get-orders.c b/src/backend/taler-merchant-httpd_private-get-orders.c @@ -723,7 +723,7 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "delta"); + "limit"); } { const char *date_s_str; @@ -786,7 +786,7 @@ TMH_private_get_orders (const struct TMH_RequestHandler *rh, return TALER_MHD_reply_with_error (connection, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_PARAMETER_MALFORMED, - "start"); + "offset"); } po->of.session_id = MHD_lookup_connection_value (connection, diff --git a/src/backend/taler-merchant-httpd_private-get-pos.c b/src/backend/taler-merchant-httpd_private-get-pos.c @@ -117,6 +117,8 @@ add_product (void *cls, json_array_append_new ( pa, GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_name", + pd->product_name), GNUNET_JSON_pack_string ("description", pd->description), GNUNET_JSON_pack_object_incref ("description_i18n", diff --git a/src/backend/taler-merchant-httpd_private-get-products-ID.c b/src/backend/taler-merchant-httpd_private-get-products-ID.c @@ -82,6 +82,8 @@ TMH_private_get_products_ID ( ret = TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, + GNUNET_JSON_pack_string ("product_name", + pd.product_name), GNUNET_JSON_pack_string ("description", pd.description), GNUNET_JSON_pack_object_steal ("description_i18n", diff --git a/src/backend/taler-merchant-httpd_private-get-transfers.c b/src/backend/taler-merchant-httpd_private-get-transfers.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2024 Taler Systems SA + (C) 2014-2025 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 @@ -30,16 +30,13 @@ * Generate a response (array entry) based on the given arguments. * * @param cls closure with a `json_t *` array to build up the response - * @param credit_amount how much was wired to the merchant (minus fees) + * @param expected_credit_amount amount expected to be wired to the merchant (minus fees), NULL if unknown * @param wtid wire transfer identifier * @param payto_uri target account that received the wire transfer * @param exchange_url base URL of the exchange that made the wire transfer * @param transfer_serial_id serial number identifying the transfer in the backend * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS * if it did not yet happen - * @param verified YES if we checked the exchange's answer and liked it, - * NO if we checked the exchange's answer and it is problematic, - * ALL if we did not yet check * @param confirmed true if the merchant acknowledged the wire transfer reception */ static void @@ -49,9 +46,8 @@ transfer_cb (void *cls, struct TALER_FullPayto payto_uri, const char *exchange_url, uint64_t transfer_serial_id, - struct GNUNET_TIME_Timestamp execution_time, - bool verified, - bool confirmed) + struct GNUNET_TIME_Absolute execution_time, + bool expected) { json_t *ja = cls; json_t *r; @@ -67,16 +63,17 @@ transfer_cb (void *cls, exchange_url), GNUNET_JSON_pack_uint64 ("transfer_serial_id", transfer_serial_id), + // FIXME: protocol breaking to remove... GNUNET_JSON_pack_bool ("verified", - verified), + false), + // FIXME: protocol breaking to remove... GNUNET_JSON_pack_bool ("confirmed", - confirmed), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ( - "execution_time", - GNUNET_TIME_absolute_is_never (execution_time.abs_time) - ? GNUNET_TIME_UNIT_ZERO_TS /* => field omitted */ - : execution_time)) ); + true), + GNUNET_JSON_pack_bool ("expected", + expected), + GNUNET_JSON_pack_timestamp ( + "execution_time", + GNUNET_TIME_absolute_to_timestamp (execution_time))); GNUNET_assert (0 == json_array_append_new (ja, r)); @@ -103,60 +100,9 @@ TMH_private_get_transfers (const struct TMH_RequestHandler *rh, struct GNUNET_TIME_Timestamp after = GNUNET_TIME_UNIT_ZERO_TS; int64_t limit = -20; uint64_t offset; - enum TALER_EXCHANGE_YesNoAll verified; + enum TALER_EXCHANGE_YesNoAll expected; (void) rh; - { - const char *esc_payto; - - esc_payto = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "payto_uri"); - if (NULL != esc_payto) - { - payto_uri.full_payto - = GNUNET_strdup (esc_payto); - (void) MHD_http_unescape (payto_uri.full_payto); - } - } - { - const char *before_s; - - before_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "before"); - if ( (NULL != before_s) && - (GNUNET_OK != - GNUNET_STRINGS_fancy_time_to_timestamp (before_s, - &before)) ) - { - GNUNET_break_op (0); - GNUNET_free (payto_uri.full_payto); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "before"); - } - } - { - const char *after_s; - - after_s = MHD_lookup_connection_value (connection, - MHD_GET_ARGUMENT_KIND, - "after"); - if ( (NULL != after_s) && - (GNUNET_OK != - GNUNET_STRINGS_fancy_time_to_timestamp (after_s, - &after)) ) - { - GNUNET_break_op (0); - GNUNET_free (payto_uri.full_payto); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "after"); - } - } TALER_MHD_parse_request_snumber (connection, "limit", &limit); @@ -167,17 +113,28 @@ TMH_private_get_transfers (const struct TMH_RequestHandler *rh, TALER_MHD_parse_request_number (connection, "offset", &offset); - if (! (TALER_MHD_arg_to_yna (connection, - "verified", + TALER_MHD_parse_request_yna (connection, + "expected", TALER_EXCHANGE_YNA_ALL, - &verified)) ) + &expected); + TALER_MHD_parse_request_timestamp (connection, + "before", + &before); + TALER_MHD_parse_request_timestamp (connection, + "after", + &after); { - GNUNET_break_op (0); - GNUNET_free (payto_uri.full_payto); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "verified"); + const char *esc_payto; + + esc_payto = MHD_lookup_connection_value (connection, + MHD_GET_ARGUMENT_KIND, + "payto_uri"); + if (NULL != esc_payto) + { + payto_uri.full_payto + = GNUNET_strdup (esc_payto); + (void) MHD_http_unescape (payto_uri.full_payto); + } } TMH_db->preflight (TMH_db->cls); { @@ -193,7 +150,7 @@ TMH_private_get_transfers (const struct TMH_RequestHandler *rh, after, limit, offset, - verified, + expected, &transfer_cb, ja); GNUNET_free (payto_uri.full_payto); diff --git a/src/backend/taler-merchant-httpd_private-patch-products-ID.c b/src/backend/taler-merchant-httpd_private-patch-products-ID.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020-2024 Taler Systems SA + (C) 2020--2025 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 @@ -49,6 +49,11 @@ TMH_private_patch_products_ID ( int64_t total_stock; enum GNUNET_DB_QueryStatus qs; struct GNUNET_JSON_Specification spec[] = { + /* new in protocol v20, thus optional for backwards-compatibility */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("product_name", + (const char **) &pd.product_name), + NULL), GNUNET_JSON_spec_string ("description", (const char **) &pd.description), GNUNET_JSON_spec_mark_optional ( @@ -114,6 +119,10 @@ TMH_private_patch_products_ID ( return (GNUNET_NO == res) ? MHD_YES : MHD_NO; + /* For pre-v20 clients, we use the description given as the + product name; remove once we make product_name mandatory. */ + if (NULL == pd.product_name) + pd.product_name = pd.description; } if (total_stock < -1) { diff --git a/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c b/src/backend/taler-merchant-httpd_private-post-instances-ID-token.c @@ -41,6 +41,7 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, struct TMH_MerchantInstance *mi = hc->instance; json_t *jtoken = hc->request_body; const char *scope; + const char *description; uint32_t iscope = TMH_AS_NONE; bool refreshable = false; struct TALER_MERCHANTDB_LoginTokenP btoken; @@ -58,6 +59,10 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, GNUNET_JSON_spec_bool ("refreshable", &refreshable), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("description", + &description), + NULL), GNUNET_JSON_spec_end () }; enum GNUNET_DB_QueryStatus qs; @@ -107,12 +112,17 @@ TMH_private_post_instances_ID_token (const struct TMH_RequestHandler *rh, TALER_EC_GENERIC_TOKEN_PERMISSION_INSUFFICIENT, NULL); } + if (NULL == description) + { + description = ""; + } qs = TMH_db->insert_login_token (TMH_db->cls, mi->settings.id, &btoken, GNUNET_TIME_timestamp_get (), expiration_time, - iscope); + iscope, + description); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -3780,6 +3780,8 @@ merge_inventory (struct OrderContext *oc) json_t *p; p = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("product_name", + pd.product_name), GNUNET_JSON_pack_string ("description", pd.description), GNUNET_JSON_pack_object_steal ("description_i18n", diff --git a/src/backend/taler-merchant-httpd_private-post-products.c b/src/backend/taler-merchant-httpd_private-post-products.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2020-2024 Taler Systems SA + (C) 2020-2025 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 @@ -41,6 +41,11 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ("product_id", &product_id), + /* new in protocol v20, thus optional for backwards-compatibility */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("product_name", + (const char **) &pd.product_name), + NULL), GNUNET_JSON_spec_string ("description", (const char **) &pd.description), GNUNET_JSON_spec_mark_optional ( @@ -101,6 +106,10 @@ TMH_private_post_products (const struct TMH_RequestHandler *rh, ? MHD_YES : MHD_NO; } + /* For pre-v20 clients, we use the description given as the + product name; remove once we make product_name mandatory. */ + if (NULL == pd.product_name) + pd.product_name = pd.description; } if (total_stock < -1) { diff --git a/src/backend/taler-merchant-httpd_private-post-transfers.c b/src/backend/taler-merchant-httpd_private-post-transfers.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014-2023 Taler Systems SA + (C) 2014-2023, 2025 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 @@ -32,7 +32,7 @@ /** * How often do we retry the simple INSERT database transaction? */ -#define MAX_RETRIES 3 +#define MAX_RETRIES 5 MHD_RESULT @@ -91,13 +91,7 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, &wtid, &amount, payto_uri, - true /* confirmed! */); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - qs = TMH_db->set_transfer_status_to_confirmed (TMH_db->cls, - hc->instance->settings.id, - exchange_url, - &wtid, - &amount); + 0 /* no bank serial known! */); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -111,13 +105,13 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, TMH_db->rollback (TMH_db->cls); continue; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - /* Could not set to confirmed, must differ by amount! */ + /* Must mean the bank account is unknown! */ TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_SUBMISSION, - NULL); + payto_uri.full_payto); case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; } @@ -151,6 +145,7 @@ TMH_private_post_transfers (const struct TMH_RequestHandler *rh, "post-transfer committed successfully\n"); break; } + break; } return TALER_MHD_reply_static (connection, MHD_HTTP_NO_CONTENT, diff --git a/src/backend/taler-merchant-reconciliation.c b/src/backend/taler-merchant-reconciliation.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023-2024 Taler Systems SA + Copyright (C) 2023-2025 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 @@ -154,11 +154,6 @@ struct Inquiry struct TALER_WireTransferIdentifierRawP wtid; /** - * Amount of the wire transfer. - */ - struct TALER_Amount total; - - /** * Row of the wire transfer in our database. */ uint64_t rowid; @@ -282,28 +277,29 @@ launch_inquiries_at_exchange (struct Exchange *e) * * @param w inquiry to update status for * @param next_attempt when should we retry @a w (if ever) + * @param http_status HTTP status of the response * @param ec error code to use (if any) - * @param failed failure status (if ultimately failed) - * @param verified success status (if ultimately successful) + * @param last_hint hint delivered with the response (if any, possibly NULL) + * @param needs_retry true if we should try the HTTP request again */ static void update_transaction_status (const struct Inquiry *w, struct GNUNET_TIME_Absolute next_attempt, + unsigned int http_status, enum TALER_ErrorCode ec, - bool failed, - bool verified) + const char *last_hint, + bool needs_retry) { enum GNUNET_DB_QueryStatus qs; - if (failed) - found_problem = true; qs = db_plugin->update_transfer_status (db_plugin->cls, w->exchange->exchange_url, &w->wtid, next_attempt, + http_status, ec, - failed, - verified); + last_hint, + needs_retry); if (qs < 0) { GNUNET_break (0); @@ -725,18 +721,13 @@ wire_transfer_cb (void *cls, break; case MHD_HTTP_BAD_REQUEST: case MHD_HTTP_FORBIDDEN: - update_transaction_status (w, - GNUNET_TIME_UNIT_FOREVER_ABS, - TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_HARD_FAILURE, - true, - false); - end_inquiry (w); - return; case MHD_HTTP_NOT_FOUND: + found_problem = true; update_transaction_status (w, GNUNET_TIME_UNIT_FOREVER_ABS, - TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_FATAL_NOT_FOUND, - true, + tgr->hr.http_status, + tgr->hr.ec, + tgr->hr.hint, false); end_inquiry (w); return; @@ -747,12 +738,14 @@ wire_transfer_cb (void *cls, update_transaction_status (w, GNUNET_TIME_relative_to_absolute ( e->transfer_delay), - TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE, - false, - false); + tgr->hr.http_status, + tgr->hr.ec, + tgr->hr.hint, + true); end_inquiry (w); return; default: + found_problem = true; e->transfer_delay = GNUNET_TIME_STD_BACKOFF (e->transfer_delay); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Unexpected HTTP status %u\n", @@ -760,9 +753,10 @@ wire_transfer_cb (void *cls, update_transaction_status (w, GNUNET_TIME_relative_to_absolute ( e->transfer_delay), - TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE, - false, - false); + tgr->hr.http_status, + tgr->hr.ec, + tgr->hr.hint, + true); end_inquiry (w); return; } @@ -785,6 +779,11 @@ wire_transfer_cb (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + // FIXME: insert_transfer_details has more complex + // error possibilities inside, expose them here + // and persist them with the transaction status + // if they arise (especially no_account, no_exchange, conflict) + // -- not sure how no_instance could happen... if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -863,9 +862,10 @@ wire_transfer_cb (void *cls, ? GNUNET_TIME_UNIT_FOREVER_ABS : GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_UNIT_MINUTES), + MHD_HTTP_OK, ctc.ec, - ctc.failure, - false); + NULL /* no hint */, + ! ctc.failure); end_inquiry (w); return; } @@ -879,41 +879,33 @@ wire_transfer_cb (void *cls, GNUNET_break_op (0); update_transaction_status (w, GNUNET_TIME_UNIT_FOREVER_ABS, + MHD_HTTP_OK, TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_BAD_WIRE_FEE, - true, + TALER_amount2s (&td->wire_fee), false); end_inquiry (w); return; } - if ( (GNUNET_OK != - TALER_amount_cmp_currency (&td->total_amount, - &w->total)) || - (0 != - TALER_amount_cmp (&td->total_amount, - &w->total)) ) { - GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Wire transfer total value was %s\n", - TALER_amount2s (&w->total)); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Exchange claimed total value to be %s\n", - TALER_amount2s (&td->total_amount)); - update_transaction_status (w, - GNUNET_TIME_UNIT_FOREVER_ABS, - TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_CONFLICTING_TRANSFERS, - true, - false); - end_inquiry (w); - return; + enum GNUNET_DB_QueryStatus qs; + + qs = db_plugin->finalize_transfer_status (db_plugin->cls, + w->exchange->exchange_url, + &w->wtid, + &td->h_details, + &td->total_amount, + &td->wire_fee, + &td->exchange_pub, + &td->exchange_sig); + if (qs < 0) + { + GNUNET_break (0); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } } - /* set transaction to successful */ - update_transaction_status (w, - GNUNET_TIME_UNIT_FOREVER_ABS, - TALER_EC_NONE, - false, - true); end_inquiry (w); } @@ -947,9 +939,10 @@ exchange_request (void *cls) update_transaction_status (w, GNUNET_TIME_relative_to_absolute ( e->transfer_delay), + 0 /* failed to begin */, TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_TRANSIENT_FAILURE, - false, - false); + "Failed to initiate GET request at exchange", + true); end_inquiry (w); return; } @@ -957,9 +950,10 @@ exchange_request (void *cls) update_transaction_status (w, GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_UNIT_MINUTES), + 0 /* timeout */, TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_AWAITING_LIST, - false, - false); + "Initiated GET with exchange", + true); } @@ -973,7 +967,6 @@ exchange_request (void *cls) * @param exchange_url base URL of the exchange that initiated the transfer * @param payto_uri account of the merchant that received the transfer * @param wtid wire transfer subject identifying the aggregation - * @param total total amount that was wired * @param next_attempt when should we next try to interact with the exchange */ static void @@ -984,7 +977,6 @@ start_inquiry ( const char *exchange_url, struct TALER_FullPayto payto_uri, const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *total, struct GNUNET_TIME_Absolute next_attempt) { struct Exchange *e; @@ -999,6 +991,8 @@ start_inquiry ( NULL); return; } + active_inquiries++; + e = find_exchange (exchange_url); for (w = e->w_head; NULL != w; w = w->next) { @@ -1012,13 +1006,11 @@ start_inquiry ( } } - active_inquiries++; w = GNUNET_new (struct Inquiry); w->payto_uri.full_payto = GNUNET_strdup (payto_uri.full_payto); w->instance_id = GNUNET_strdup (instance_id); w->rowid = rowid; w->wtid = *wtid; - w->total = *total; GNUNET_CONTAINER_DLL_insert (e->w_head, e->w_tail, w); @@ -1030,9 +1022,10 @@ start_inquiry ( update_transaction_status (w, GNUNET_TIME_relative_to_absolute ( GNUNET_TIME_UNIT_MINUTES), + 0 /* timeout */, TALER_EC_MERCHANT_EXCHANGE_TRANSFERS_AWAITING_KEYS, - false, - false); + exchange_url, + true); } @@ -1204,7 +1197,7 @@ run (void *cls, { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_WIRE_TRANSFER_CONFIRMED) + .type = htons (TALER_DBEVENT_MERCHANT_WIRE_TRANSFER_EXPECTED) }; eh = db_plugin->event_listen (db_plugin->cls, diff --git a/src/backend/taler-merchant-wirewatch.c b/src/backend/taler-merchant-wirewatch.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 2025 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 @@ -379,52 +379,7 @@ credit_cb ( &wtid, &details->amount, details->credit_account_uri, - true /* confirmed */); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - struct TALER_Amount total; - struct TALER_Amount wfee; - struct TALER_Amount eamount; - struct GNUNET_TIME_Timestamp timestamp; - bool have_esig; - bool verified; - - qs = db_plugin->lookup_transfer (db_plugin->cls, - w->instance_id, - exchange_url, - &wtid, - &total, - &wfee, - &eamount, - ×tamp, - &have_esig, - &verified); - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Inserting transfer for %s into database failed. Is the credit account %s configured correctly?\n", - w->instance_id, - details->credit_account_uri.full_payto); - } - if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) - { - if (0 != - TALER_amount_cmp (&total, - &details->amount)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Inserting transfer for %s into database failed. An entry exists for a different transfer amount (%s)!\n", - w->instance_id, - TALER_amount2s (&total)); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Inserting transfer for %s into database failed. An equivalent entry already exists.\n", - w->instance_id); - } - } - } + serial_id); GNUNET_free (exchange_url); if (qs < 0) { diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -143,8 +143,10 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_deposits.h pg_lookup_deposits.c \ pg_lookup_deposits_by_contract_and_coin.h pg_lookup_deposits_by_contract_and_coin.c \ pg_lookup_deposits_by_order.h pg_lookup_deposits_by_order.c \ + pg_lookup_expected_transfers.h pg_lookup_expected_transfers.c \ pg_lookup_instance_auth.h pg_lookup_instance_auth.c \ pg_lookup_instances.h pg_lookup_instances.c \ + pg_lookup_login_tokens.h pg_lookup_login_tokens.c \ pg_lookup_order.h pg_lookup_order.c \ pg_lookup_order_by_fulfillment.h pg_lookup_order_by_fulfillment.c \ pg_lookup_order_status.h pg_lookup_order_status.c \ @@ -166,7 +168,6 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_lookup_token_family.h pg_lookup_token_family.c \ pg_lookup_token_family_key.h pg_lookup_token_family_key.c \ pg_lookup_token_family_keys.h pg_lookup_token_family_keys.c \ - pg_lookup_transfer.h pg_lookup_transfer.c \ pg_lookup_transfer_details.h pg_lookup_transfer_details.c \ pg_lookup_transfer_details_by_order.h pg_lookup_transfer_details_by_order.c \ pg_lookup_transfer_summary.h pg_lookup_transfer_summary.c \ @@ -191,7 +192,6 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_select_otp.h pg_select_otp.c \ pg_select_otp_serial.h pg_select_otp_serial.c \ pg_select_wirewatch_accounts.h pg_select_wirewatch_accounts.c \ - pg_set_transfer_status_to_confirmed.h pg_set_transfer_status_to_confirmed.c \ pg_store_wire_fee_by_exchange.h pg_store_wire_fee_by_exchange.c \ pg_unlock_inventory.h pg_unlock_inventory.c \ pg_update_account.h pg_update_account.c \ @@ -206,6 +206,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \ pg_update_template.h pg_update_template.c \ pg_update_token_family.h pg_update_token_family.c \ pg_update_transfer_status.h pg_update_transfer_status.c \ + pg_finalize_transfer_status.h pg_finalize_transfer_status.c \ pg_update_webhook.h pg_update_webhook.c \ pg_update_wirewatch_progress.h pg_update_wirewatch_progress.c \ pg_lookup_statistics_counter_by_bucket.h pg_lookup_statistics_counter_by_bucket.c \ diff --git a/src/backenddb/merchant-0019.sql b/src/backenddb/merchant-0019.sql @@ -0,0 +1,42 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- @file merchant-0019.sql +-- @brief Tables for statistics +-- @author Christian Grothoff + + +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('merchant-0019', NULL, NULL); + +SET search_path TO merchant; + +ALTER TABLE merchant_inventory + ADD product_name TEXT; +-- We use existing description as the product name for +-- products that existed in the inventory prior to the migration. +UPDATE merchant_inventory + SET product_name=description; +ALTER TABLE merchant_inventory + ALTER COLUMN product_name SET NOT NULL; + + +COMMENT ON COLUMN merchant_inventory.product_name + IS 'Name of the product'; + +COMMIT; diff --git a/src/backenddb/merchant-0020.sql b/src/backenddb/merchant-0020.sql @@ -0,0 +1,38 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- @file merchant-0020.sql +-- @brief Add token descriptions +-- @author Martin Schanzenbach + + +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('merchant-0020', NULL, NULL); + +SET search_path TO merchant; + +ALTER TABLE merchant_login_tokens + ADD description TEXT NOT NULL; + +COMMENT ON COLUMN merchant_login_tokens.description + IS 'Description of the login token'; + +ALTER TABLE merchant_login_tokens +ADD COLUMN serial BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY; + +COMMIT; diff --git a/src/backenddb/merchant-0021.sql b/src/backenddb/merchant-0021.sql @@ -0,0 +1,295 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2025 Taler Systems SA +-- +-- TALER is free software; you can redistribute it and/or modify it under the +-- terms of the GNU 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 General Public License for more details. +-- +-- You should have received a copy of the GNU General Public License along with +-- TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +-- + +-- @file merchant-0021.sql +-- @brief Tables for statistics +-- @author Christian Grothoff + + +BEGIN; + +-- Check patch versioning is in place. +SELECT _v.register_patch('merchant-0021', NULL, NULL); + +SET search_path TO merchant; + +COMMENT ON TABLE merchant_transfers + IS 'table represents confirmed incoming wire transfers'; +COMMENT ON COLUMN merchant_transfers.credit_amount + IS 'actual value of the confirmed wire transfer'; + +CREATE TABLE merchant_expected_transfers + (expected_credit_serial INT8 GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY + ,exchange_url TEXT NOT NULL + ,wtid BYTEA NOT NULL CHECK (LENGTH(wtid)=32) + ,expected_credit_amount taler_amount_currency + ,wire_fee taler_amount_currency + ,account_serial INT8 NOT NULL + REFERENCES merchant_accounts (account_serial) ON DELETE CASCADE + ,expected_time INT8 NOT NULL + ,retry_time INT8 NOT NULL DEFAULT (0) + ,last_http_status INT4 DEFAULT NULL + ,last_ec INT4 DEFAULT NULL + ,last_detail TEXT DEFAULT NULL + ,retry_needed BOOLEAN NOT NULL DEFAULT TRUE + ,signkey_serial BIGINT + REFERENCES merchant_exchange_signing_keys (signkey_serial) + ON DELETE CASCADE + ,exchange_sig BYTEA CHECK (LENGTH(exchange_sig)=64) DEFAULT NULL + ,h_details BYTEA CHECK (LENGTH(h_details)=64) DEFAULT NULL + ,confirmed BOOLEAN NOT NULL DEFAULT FALSE + ,UNIQUE (wtid, exchange_url, account_serial) + ); +COMMENT ON TABLE merchant_expected_transfers + IS 'expected incoming wire transfers'; +COMMENT ON COLUMN merchant_expected_transfers.expected_credit_serial + IS 'Unique identifier for this expected wire transfer in this backend'; +COMMENT ON COLUMN merchant_expected_transfers.exchange_url + IS 'Base URL of the exchange that originated the wire transfer as extracted from the wire transfer subject'; +COMMENT ON COLUMN merchant_expected_transfers.wtid + IS 'Unique wire transfer identifier (or at least, should be unique by protocol) as selected by the exchange and extracted from the wire transfer subject'; +COMMENT ON COLUMN merchant_expected_transfers.expected_credit_amount + IS 'expected actual value of the (aggregated) wire transfer, excluding the wire fee; NULL if unknown'; +COMMENT ON COLUMN merchant_expected_transfers.wire_fee + IS 'wire fee the exchange claims to have charged us; NULL if unknown'; +COMMENT ON COLUMN merchant_expected_transfers.account_serial + IS 'Merchant bank account that should receive this wire transfer; also implies the merchant instance implicated by the wire transfer'; +COMMENT ON COLUMN merchant_expected_transfers.expected_time + IS 'Time when we should expect the exchange do do the wire transfer'; +COMMENT ON COLUMN merchant_expected_transfers.retry_time + IS 'Time when we should next inquire at the exchange about this wire transfer; used by taler-merchant-reconciliation to limit retries with the exchange in case of failures'; +COMMENT ON COLUMN merchant_expected_transfers.last_http_status + IS 'HTTP status of the last request to the exchange, 0 on timeout or if there was no request (200 on success)'; +COMMENT ON COLUMN merchant_expected_transfers.last_ec + IS 'Taler error code from the last request to the exchange, 0 on success or if there was no request'; +COMMENT ON COLUMN merchant_expected_transfers.last_detail + IS 'Taler error detail from the last request to the exchange, NULL on success or if there was no request'; +COMMENT ON COLUMN merchant_expected_transfers.signkey_serial + IS 'Identifies the online signing key of the exchange used to make the exchange_sig'; +COMMENT ON COLUMN merchant_expected_transfers.exchange_sig + IS 'Signature over the aggregation response from the exchange, or NULL on error or if we did not yet make that request'; +COMMENT ON COLUMN merchant_expected_transfers.confirmed + IS 'true once the merchant confirmed that this transfer was received and a matching transfer exists in the merchant_transfers table; set automatically via INSERT TRIGGER merchant_expected_transfers_insert_trigger'; +COMMENT ON COLUMN merchant_expected_transfers.retry_needed + IS 'true if we need to retry the HTTP request to the exchange (never did it, or transient failure)'; +COMMENT ON COLUMN merchant_expected_transfers.h_details + IS 'Hash over the aggregation details returned by the exchange, provided here for fast exchange_sig validation'; + +CREATE INDEX merchant_expected_transfers_by_open + ON merchant_expected_transfers + (retry_time ASC) + WHERE NOT confirmed OR retry_needed; +COMMENT ON INDEX merchant_expected_transfers_by_open + IS 'For select_open_transfers'; + +-- Migrate data. The backend will just re-do all of the +-- reconciliation work, so we only preserve confirmed transfers. +-- However, we must put those also into the new "merchant_expected_transfers" +-- table already. +DELETE FROM merchant_transfers + WHERE NOT confirmed; + +-- This index was replaced by merchant_expected_transfers_by_open. +DROP INDEX merchant_transfers_by_open; + +-- These columns will be in the new merchant_expected_transfers table. +ALTER TABLE merchant_transfers + ADD COLUMN bank_serial_id INT8, + ADD COLUMN expected BOOL DEFAULT FALSE, + ADD COLUMN execution_time INT8 DEFAULT (0), + DROP COLUMN ready_time, + DROP COLUMN confirmed, + DROP COLUMN failed, + DROP COLUMN verified, + DROP COLUMN validation_status; + +COMMENT ON COLUMN merchant_transfers.expected + IS 'True if this wire transfer was expected (has matching entry in merchant_expected_transfers); set automatically via INSERT TRIGGER merchant_transfers_insert_trigger'; +COMMENT ON COLUMN merchant_transfers.bank_serial_id + IS 'Row ID of the wire transfer from the automated import; NULL if not available (like when a human manually imported the transfer)'; +COMMENT ON COLUMN merchant_transfers.execution_time + IS 'Time when the merchant transfer was added and thus roughly received in our bank account'; + +-- Note: if the bank_serial_id is NULL (manual import), we always +-- consider confirmed transfers to be 'UNIQUE'; thus we do +-- NOT use "NULLS NOT DISTINCT" here. + +ALTER TABLE merchant_transfers + DROP CONSTRAINT merchant_transfers_wtid_exchange_url_account_serial_key, + ADD CONSTRAINT merchant_transfers_unique + UNIQUE (wtid, exchange_url, account_serial, bank_serial_id); + + +-- Create triggers to set confirmed/expected status on INSERT. +CREATE FUNCTION merchant_expected_transfers_insert_trigger() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE merchant_transfers + SET expected = TRUE + WHERE wtid = NEW.wtid + AND exchange_url = NEW.exchange_url + AND credit_amount = NEW.expected_credit_amount; + NEW.confirmed = FOUND; + RETURN NEW; +END $$; +COMMENT ON FUNCTION merchant_expected_transfers_insert_trigger + IS 'Sets "confirmed" to TRUE for the new record if the expected transfer was already confirmed, and updates the already confirmed transfer to "expected"'; + +-- Whenever an expected transfer is added, check if it was already confirmed +CREATE TRIGGER merchant_expected_transfers_on_insert + BEFORE INSERT + ON merchant.merchant_expected_transfers + FOR EACH ROW EXECUTE FUNCTION merchant_expected_transfers_insert_trigger(); + + +CREATE FUNCTION merchant_transfers_insert_trigger() +RETURNS trigger +LANGUAGE plpgsql +AS $$ +BEGIN + UPDATE merchant_expected_transfers + SET confirmed = TRUE + WHERE wtid = NEW.wtid + AND exchange_url = NEW.exchange_url + AND expected_credit_amount = NEW.credit_amount; + NEW.expected = FOUND; + RETURN NEW; +END $$; +COMMENT ON FUNCTION merchant_transfers_insert_trigger + IS 'Sets "expected" to TRUE for the new record if the transfer was already expected, and updates the already confirmed transfer to "confirmed"'; + +-- Whenever a transfer is addeded, check if it was already expected +CREATE TRIGGER merchant_transfers_on_insert + BEFORE INSERT + ON merchant.merchant_transfers + FOR EACH ROW EXECUTE FUNCTION merchant_transfers_insert_trigger(); + + +-- Adjust contract terms table. +ALTER TABLE merchant_deposits + ADD COLUMN settlement_retry_needed BOOL DEFAULT TRUE, + ADD COLUMN settlement_retry_time INT8 DEFAULT (0), + ADD COLUMN settlement_last_http_status INT4 DEFAULT NULL, + ADD COLUMN settlement_last_ec INT4 DEFAULT NULL, + ADD COLUMN settlement_last_detail TEXT DEFAULT NULL, + ADD COLUMN settlement_wtid BYTEA CHECK (LENGTH(settlement_wtid)=32) DEFAULT NULL, + ADD COLUMN settlement_coin_contribution taler_amount_currency DEFAULT NULL, + ADD COLUMN settlement_expected_credit_serial INT8 DEFAULT NULL + REFERENCES merchant_expected_transfers (expected_credit_serial), + ADD COLUMN signkey_serial INT8 DEFAULT NULL + REFERENCES merchant_exchange_signing_keys (signkey_serial) + ON DELETE CASCADE, + ADD COLUMN settlement_exchange_sig BYTEA + DEFAULT NULL CHECK (LENGTH(settlement_exchange_sig)=64); + +COMMENT ON COLUMN merchant_deposits.settlement_retry_needed + IS 'True if we should ask the exchange in the future about the settlement'; +COMMENT ON COLUMN merchant_deposits.settlement_retry_time + IS 'When should we next ask the exchange about the settlement wire transfer for this coin, initially set to the wire transfer deadline plus a bit of slack'; +COMMENT ON COLUMN merchant_deposits.settlement_last_http_status + IS 'HTTP status of our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on timeout'; +COMMENT ON COLUMN merchant_deposits.settlement_last_ec + IS 'Taler error code for our last inquiry with the exchange for this deposit, NULL if we never inquired, 0 on success'; +COMMENT ON COLUMN merchant_deposits.settlement_last_detail + IS 'Taler error detail for our last inquiry with the exchange for this deposit, NULL if we never inquired or on success'; +COMMENT ON COLUMN merchant_deposits.settlement_coin_contribution + IS 'Contribution of this coin to the overall wire transfer made by the exchange as claimed by exchange_sig; should match amount_with_fee minus deposit_fee, NULL if we did not get a reply from the exchange'; +COMMENT ON COLUMN merchant_deposits.settlement_expected_credit_serial + IS 'Identifies the expected wire transfer from the exchange to the merchant that settled the deposit of coin, NULL if unknown'; +COMMENT ON COLUMN merchant_deposits.signkey_serial + IS 'Identifies the online signing key of the exchange used to make the exchange_sig, NULL for none'; +COMMENT ON COLUMN merchant_deposits.settlement_exchange_sig + IS 'Exchange signature of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE, NULL if we did not get such an exchange signature'; + +CREATE INDEX merchant_deposits_by_settlement_open + ON merchant_deposits + (settlement_retry_time ASC) + WHERE settlement_retry_needed; +COMMENT ON INDEX merchant_deposits_by_settlement_open + IS 'For select_open_deposit_settlements'; + +CREATE INDEX merchant_deposits_by_deposit_confirmation + ON merchant_deposits + (deposit_confirmation_serial); + + +-- No 1:n mapping necessary, integrated into merchant_deposits table above. +DROP TABLE merchant_deposit_to_transfer; + +-- We need to fully re-do the merchant_transfer_to_coin table, +-- and data should be re-constructed, so drop and re-build. +DROP TABLE merchant_transfer_to_coin; +CREATE TABLE merchant_expected_transfer_to_coin + (deposit_serial BIGINT UNIQUE NOT NULL + REFERENCES merchant_deposits (deposit_serial) ON DELETE CASCADE + ,expected_credit_serial BIGINT NOT NULL + REFERENCES merchant_expected_transfers (expected_credit_serial) ON DELETE CASCADE + ,offset_in_exchange_list INT8 NOT NULL + ,exchange_deposit_value taler_amount_currency NOT NULL + ,exchange_deposit_fee taler_amount_currency NOT NULL + ); +CREATE INDEX IF NOT EXISTS merchant_transfers_by_credit + ON merchant_expected_transfer_to_coin + (expected_credit_serial); +COMMENT ON TABLE merchant_expected_transfer_to_coin + IS 'Mapping of (credit) transfers to (deposited) coins'; +COMMENT ON COLUMN merchant_expected_transfer_to_coin.deposit_serial + IS 'Identifies the deposited coin that the wire transfer presumably settles'; +COMMENT ON COLUMN merchant_expected_transfer_to_coin.expected_credit_serial + IS 'Identifies the expected wire transfer that settles the given deposited coin'; +COMMENT ON COLUMN merchant_expected_transfer_to_coin.offset_in_exchange_list + IS 'The exchange settlement data includes an array of the settled coins; this is the index of the coin in that list, useful to reconstruct the correct sequence of coins as needed to check the exchange signature'; +COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_value + IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits minus refunds'; +COMMENT ON COLUMN merchant_expected_transfer_to_coin.exchange_deposit_fee + IS 'Deposit value as claimed by the exchange, should match our values in merchant_deposits'; + + +-- We need to fully re-do the merchant_transfer_signatures table, +-- and data should be re-constructed, so drop and re-build. + +DROP TABLE merchant_transfer_signatures; +CREATE TABLE merchant_transfer_signatures + (expected_credit_serial BIGINT PRIMARY KEY + REFERENCES merchant_expected_transfers (expected_credit_serial) + ON DELETE CASCADE + ,signkey_serial BIGINT NOT NULL + REFERENCES merchant_exchange_signing_keys (signkey_serial) + ON DELETE CASCADE + ,wire_fee taler_amount_currency NOT NULL + ,credit_amount taler_amount_currency NOT NULL + ,execution_time INT8 NOT NULL + ,exchange_sig BYTEA NOT NULL CHECK (LENGTH(exchange_sig)=64) + ); +COMMENT ON TABLE merchant_transfer_signatures + IS 'table represents the main information returned from the /transfer request to the exchange.'; +COMMENT ON COLUMN merchant_transfer_signatures.expected_credit_serial + IS 'expected wire transfer this signature is about'; +COMMENT ON COLUMN merchant_transfer_signatures.signkey_serial + IS 'Online signing key by the exchange that was used for the exchange_sig signature'; +COMMENT ON COLUMN merchant_transfer_signatures.wire_fee + IS 'wire fee charged by the exchange for this transfer'; +COMMENT ON COLUMN merchant_transfer_signatures.exchange_sig + IS 'signature by the exchange of purpose TALER_SIGNATURE_EXCHANGE_CONFIRM_WIRE_DEPOSIT'; +COMMENT ON COLUMN merchant_transfer_signatures.execution_time + IS 'Execution time as claimed by the exchange, roughly matches time seen by merchant'; +COMMENT ON COLUMN merchant_transfer_signatures.credit_amount + IS 'actual value of the (aggregated) wire transfer, excluding the wire fee, according to the exchange'; + + +COMMIT; diff --git a/src/backenddb/pg_delete_login_token.c b/src/backenddb/pg_delete_login_token.c @@ -25,6 +25,33 @@ #include "pg_delete_login_token.h" #include "pg_helper.h" +enum GNUNET_DB_QueryStatus +TMH_PG_delete_login_token_serial ( + void *cls, + const char *id, + uint64_t serial) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (id), + GNUNET_PQ_query_param_uint64 (&serial), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + PREPARE (pg, + "delete_login_token_serial", + "DELETE FROM merchant_login_tokens" + " WHERE serial=$2" + " AND merchant_serial=" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1)"); + return GNUNET_PQ_eval_prepared_non_select (pg->conn, + "delete_login_token_serial", + params); +} + enum GNUNET_DB_QueryStatus TMH_PG_delete_login_token ( @@ -52,4 +79,3 @@ TMH_PG_delete_login_token ( "delete_login_token", params); } - diff --git a/src/backenddb/pg_delete_login_token.h b/src/backenddb/pg_delete_login_token.h @@ -25,6 +25,20 @@ #include <taler/taler_json_lib.h> #include "taler_merchantdb_plugin.h" +/** + * Delete login token from database by serial. + * + * @param cls closure + * @param id identifier of the instance + * @param serial serial of the token + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_delete_login_token_serial ( + void *cls, + const char *id, + uint64_t serial); + /** * Delete login token from database. diff --git a/src/backenddb/pg_finalize_transfer_status.c b/src/backenddb/pg_finalize_transfer_status.c @@ -0,0 +1,80 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_finalize_transfer_status.c + * @brief Implementation of the finalize_transfer_status function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_finalize_transfer_status.h" +#include "pg_helper.h" + + +enum GNUNET_DB_QueryStatus +TMH_PG_finalize_transfer_status ( + void *cls, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct GNUNET_HashCode *h_details, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_auto_from_type (wtid), + GNUNET_PQ_query_param_string (exchange_url), + TALER_PQ_query_param_amount_with_currency (pg->conn, + total_amount), + TALER_PQ_query_param_amount_with_currency (pg->conn, + wire_fee), + GNUNET_PQ_query_param_auto_from_type (h_details), + GNUNET_PQ_query_param_auto_from_type (exchange_pub), + GNUNET_PQ_query_param_auto_from_type (exchange_sig), + GNUNET_PQ_query_param_end + }; + + check_connection (pg); + PREPARE (pg, + "finalize_transfer_status", + "WITH subquery AS (" + " SELECT signkey_serial" + " FROM merchant_exchange_signing_keys" + " WHERE exchange_pub=$6" + ")" + "UPDATE merchant_expected_transfers SET" + " last_http_status=200" + ",last_ec=0" + ",last_detail=NULL" + ",retry_needed=FALSE" + ",retry_time=0" + ",expected_credit_amount=$3" + ",wire_fee=$4" + ",h_details=$5" + ",signkey_serial=subquery.signkey_serial" + ",exchange_sig=$7" + " FROM subquery" + " WHERE wtid=$1" + " AND exchange_url=$2"); + return GNUNET_PQ_eval_prepared_non_select ( + pg->conn, + "finalize_transfer_status", + params); +} diff --git a/src/backenddb/pg_finalize_transfer_status.h b/src/backenddb/pg_finalize_transfer_status.h @@ -0,0 +1,54 @@ +/* + 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_finalize_transfer_status.h + * @brief implementation of the finalize_transfer_status function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_FINALIZE_TRANSFER_STATUS_H +#define PG_FINALIZE_TRANSFER_STATUS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Finalize transfer status. + * + * @param cls closure + * @param exchange_url the exchange that made the transfer + * @param wtid wire transfer subject + * @param h_details hash over all of the aggregated deposits + * @param total_amount total amount exchange claimed to have transferred + * @param wire_fee wire fee charged by the exchange + * @param exchange_pub key used to make @e exchange_sig + * @param exchange_sig signature of the exchange over reconciliation data + * @return database transaction status + */ +enum GNUNET_DB_QueryStatus +TMH_PG_finalize_transfer_status ( + void *cls, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct GNUNET_HashCode *h_details, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig); + + +#endif diff --git a/src/backenddb/pg_insert_deposit.c b/src/backenddb/pg_insert_deposit.c @@ -36,7 +36,8 @@ TMH_PG_insert_deposit ( const struct TALER_CoinSpendSignatureP *coin_sig, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee) + const struct TALER_Amount *refund_fee, + struct GNUNET_TIME_Absolute check_time) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -50,6 +51,7 @@ TMH_PG_insert_deposit ( deposit_fee), TALER_PQ_query_param_amount_with_currency (pg->conn, refund_fee), + GNUNET_PQ_query_param_absolute_time (&check_time), GNUNET_PQ_query_param_end }; @@ -69,7 +71,8 @@ TMH_PG_insert_deposit ( ",amount_with_fee" ",deposit_fee" ",refund_fee" - ") VALUES ($1, $2, $3, $4, $5, $6, $7)"); + ",settlement_retry_time" + ") VALUES ($1,$2,$3,$4,$5,$6,$7,$8)"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_deposit", params); diff --git a/src/backenddb/pg_insert_deposit.h b/src/backenddb/pg_insert_deposit.h @@ -36,6 +36,8 @@ * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange charges to refund this coin + * @param check_time at what time should we check the deposit status + * with the exchange (for settlement) * @return transaction status */ enum GNUNET_DB_QueryStatus @@ -47,6 +49,7 @@ TMH_PG_insert_deposit ( const struct TALER_CoinSpendSignatureP *coin_sig, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee); + const struct TALER_Amount *refund_fee, + struct GNUNET_TIME_Absolute check_time); #endif diff --git a/src/backenddb/pg_insert_deposit_to_transfer.c b/src/backenddb/pg_insert_deposit_to_transfer.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -30,8 +30,9 @@ enum GNUNET_DB_QueryStatus TMH_PG_insert_deposit_to_transfer ( void *cls, uint64_t deposit_serial, - const struct TALER_EXCHANGE_DepositData *dd, - bool *wpc) + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + const struct TALER_EXCHANGE_DepositData *dd) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -39,46 +40,29 @@ TMH_PG_insert_deposit_to_transfer ( TALER_PQ_query_param_amount_with_currency (pg->conn, &dd->coin_contribution), GNUNET_PQ_query_param_timestamp (&dd->execution_time), + GNUNET_PQ_query_param_string (exchange_url), + GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_auto_from_type (&dd->exchange_sig), GNUNET_PQ_query_param_auto_from_type (&dd->exchange_pub), GNUNET_PQ_query_param_auto_from_type (&dd->wtid), GNUNET_PQ_query_param_end }; - bool conflict; - bool no_exchange_pub; + bool dummy; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_bool ("out_wire_pending_cleared", - wpc), - GNUNET_PQ_result_spec_bool ("out_conflict", - &conflict), - GNUNET_PQ_result_spec_bool ("out_no_exchange_pub", - &no_exchange_pub), + GNUNET_PQ_result_spec_bool ("out_dummy", + &dummy), GNUNET_PQ_result_spec_end }; - enum GNUNET_DB_QueryStatus qs; - *wpc = false; PREPARE (pg, "insert_deposit_to_transfer", "SELECT" - " out_wire_pending_cleared" - " ,out_conflict" - " ,out_no_exchange_pub" + " out_dummy" " FROM merchant_insert_deposit_to_transfer" - " ($1,$2,$3,$4,$5,$6);"); - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "insert_deposit_to_transfer", - params, - rs); - if (qs <= 0) - return qs; - if (no_exchange_pub) - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Exchange public key unknown (bug!?)\n"); - if (*wpc) - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Wire pending flag cleared (good!)\n"); - if (conflict) - return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; - return qs; + " ($1,$2,$3,$4,$5,$6,$7,$8);"); + return GNUNET_PQ_eval_prepared_singleton_select ( + pg->conn, + "insert_deposit_to_transfer", + params, + rs); } diff --git a/src/backenddb/pg_insert_deposit_to_transfer.h b/src/backenddb/pg_insert_deposit_to_transfer.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -31,16 +31,18 @@ * * @param cls closure * @param deposit_serial serial number of the deposit + * @param h_wire hash of the merchant's account that should receive the deposit + * @param exchange_url URL of the exchange that is making the deposit * @param dd deposit transfer data from the exchange to store - * @param[out] wpc set to true if the wire_pending flag was cleared * @return transaction status */ enum GNUNET_DB_QueryStatus TMH_PG_insert_deposit_to_transfer ( void *cls, uint64_t deposit_serial, - const struct TALER_EXCHANGE_DepositData *dd, - bool *wpc); + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + const struct TALER_EXCHANGE_DepositData *dd); #endif diff --git a/src/backenddb/pg_insert_deposit_to_transfer.sql b/src/backenddb/pg_insert_deposit_to_transfer.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- Copyright (C) 2024, 2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -15,31 +15,30 @@ -- -CREATE OR REPLACE FUNCTION merchant_insert_deposit_to_transfer ( +DROP FUNCTION IF EXISTS merchant_insert_deposit_to_transfer; +CREATE FUNCTION merchant_insert_deposit_to_transfer ( IN in_deposit_serial INT8, - IN in_amount_with_fee taler_amount_currency, + IN in_coin_contribution taler_amount_currency, IN in_execution_time INT8, + IN in_exchange_url TEXT, + IN in_h_wire BYTEA, IN in_exchange_sig BYTEA, IN in_exchange_pub BYTEA, IN in_wtid BYTEA, - OUT out_wire_pending_cleared BOOL, - OUT out_conflict BOOL, - OUT out_no_exchange_pub BOOL) + OUT out_dummy BOOL) LANGUAGE plpgsql AS $$ DECLARE my_signkey_serial INT8; -DECLARE - my_confirmed BOOL; -DECLARE + my_account_serial INT8; my_decose INT8; -DECLARE my_order_serial INT8; -DECLARE - my_merchant_serial INT8; -DECLARE + my_expected_credit_serial INT8; + my_wire_pending_cleared BOOL; my_order_id TEXT; BEGIN + -- Just to return something (for now). + out_dummy=FALSE; -- Find exchange sign key SELECT signkey_serial @@ -51,137 +50,89 @@ SELECT signkey_serial IF NOT FOUND THEN - out_no_exchange_pub=TRUE; - out_conflict=FALSE; - out_wire_pending_cleared=FALSE; + -- Maybe 'keys' is outdated, try again in 8 hours. + UPDATE merchant_deposits + SET settlement_last_ec=2029 -- MERCHANT_EXCHANGE_SIGN_PUB_UNKNOWN + ,settlement_last_http_status=200 + ,settlement_last_detail=ENCODE(in_exchange_pub, 'hex') + ,settlement_wtid=in_wtid + ,settlement_retry_needed=TRUE + ,settlement_retry_time=(EXTRACT(epoch FROM (CURRENT_TIME + interval '8 hours')) * 1000000)::INT8 + WHERE deposit_serial=in_deposit_serial; RETURN; END IF; -out_no_exchange_pub=FALSE; - --- Try to insert new wire transfer -INSERT INTO merchant_deposit_to_transfer - (deposit_serial - ,coin_contribution_value - ,wtid - ,execution_time - ,signkey_serial - ,exchange_sig - ) - VALUES - (in_deposit_serial - ,in_amount_with_fee - ,in_wtid - ,in_execution_time - ,my_signkey_serial - ,in_exchange_sig - ) - ON CONFLICT DO NOTHING; +-- Find deposit confirmation +SELECT deposit_confirmation_serial + INTO my_decose + FROM merchant_deposits + WHERE deposit_serial=in_deposit_serial; -IF NOT FOUND -THEN - PERFORM FROM merchant_deposit_to_transfer - WHERE deposit_serial=in_deposit_serial - AND wtid=in_wtid - AND signkey_serial=my_signkey_serial - AND exchange_sig=in_exchange_sig; -END IF; +-- Find merchant account +SELECT account_serial + INTO my_account_serial + FROM merchant_deposit_confirmations mdc + JOIN merchant_accounts ma + USING (account_serial) + WHERE mdc.deposit_confirmation_serial=my_decose + AND ma.h_wire=in_h_wire; IF NOT FOUND THEN - -- Conflicting (!) wire transfer existed in the table already - out_conflict=TRUE; - out_wire_pending_cleared=FALSE; + -- Merchant account referenced in exchange response is unknown to us. + -- Remember fatal error and do not try again. + UPDATE merchant_deposits + SET settlement_last_ec=2558 -- MERCHANT_EXCHANGE_TRANSFERS_TARGET_ACCOUNT_UNKNOWN + ,settlement_last_http_status=200 + ,settlement_last_detail=ENCODE(in_h_wire, 'hex') + ,settlement_wtid=in_wtid + ,settlement_retry_needed=FALSE + ,settlement_coin_contribution=in_coin_contribution + ,signkey_serial=my_signkey_serial + ,settlement_exchange_sig=in_exchange_sig + WHERE deposit_serial=in_deposit_serial; RETURN; END IF; -out_conflict=FALSE; --- Check if we already imported the (confirmed) --- wire transfer *and* if it is mapped to this deposit. -PERFORM - FROM merchant_transfers mt - JOIN merchant_transfer_to_coin mtc - USING (credit_serial) - WHERE mt.wtid=in_wtid - AND mt.confirmed - AND mtc.deposit_serial=in_deposit_serial; +-- Make sure wire transfer is expected. +SELECT expected_credit_serial + INTO my_expected_credit_serial + FROM merchant_expected_transfers + WHERE wtid=in_wtid + AND exchange_url=in_exchange_url + AND account_serial=my_account_serial; IF NOT FOUND THEN - out_wire_pending_cleared=FALSE; - RETURN; + INSERT INTO merchant_expected_transfers + (exchange_url + ,wtid + ,account_serial + ,expected_time) + VALUES + (in_exchange_url + ,in_wtid + ,my_account_serial + ,in_execution_time) + RETURNING expected_credit_serial + INTO my_expected_credit_serial; END IF; - -RAISE NOTICE 'checking affected deposit confirmation for completion'; - -SELECT deposit_confirmation_serial - INTO my_decose - FROM merchant_deposits +-- Finally, update merchant_deposits so we do not try again. +UPDATE merchant_deposits + SET settlement_last_ec=0 + ,settlement_last_http_status=200 + ,settlement_last_detail=NULL + ,settlement_wtid=in_wtid + ,settlement_retry_needed=FALSE + ,settlement_coin_contribution=in_coin_contribution + ,settlement_expected_credit_serial=my_expected_credit_serial + ,signkey_serial=my_signkey_serial + ,settlement_exchange_sig=in_exchange_sig WHERE deposit_serial=in_deposit_serial; --- we made a change, check about clearing wire_pending --- for the entire deposit confirmation -UPDATE merchant_deposit_confirmations - SET wire_pending=FALSE - WHERE (deposit_confirmation_serial=my_decose) - AND NOT EXISTS - (SELECT 1 - FROM merchant_deposits md - LEFT JOIN merchant_deposit_to_transfer mdtt - USING (deposit_serial) - WHERE md.deposit_confirmation_serial=my_decose - AND mdtt.signkey_serial IS NULL); --- credit_serial will be NULL due to LEFT JOIN --- if we do not have an entry in mdtt for the deposit --- and thus some entry in md was not yet wired. +-- MERCHANT_WIRE_TRANSFER_EXPECTED +NOTIFY XR6849FMRD2AJFY1E2YY0GWA8GN0YT407Z66WHJB0SAKJWF8G2Q60; -IF NOT FOUND -THEN - out_wire_pending_cleared=FALSE; - RETURN; -END IF; -out_wire_pending_cleared=TRUE; - - -RAISE NOTICE 'checking affected contracts for completion'; - --- Check if all deposit confirmations of the same --- contract are now wired. -SELECT deposit_confirmation_serial - INTO my_order_serial - FROM merchant_deposit_confirmations - WHERE deposit_confirmation_serial=my_decose; --- The above MUST succeed by invariants. - --- Check about setting 'wired' for the contract term. --- Note: the same contract may be paid from --- multiple exchanges, so we need to check if --- payments were wired from all of them! -UPDATE merchant_contract_terms - SET wired=TRUE - WHERE (order_serial=my_order_serial) - AND NOT EXISTS - (SELECT 1 - FROM merchant_deposit_confirmations mdc - WHERE mdc.wire_pending - AND mdc.order_serial=my_order_serial); - --- POSSIBLE LOCATION FOR THE WIRE WEBHOOK OF ORDER --- --- INSERT INTO merchant_pending_webhooks --- (merchant_serial --- ,webhook_serial --- ,url --- ,http_method --- ,body) --- SELECT mw.merchant_serial --- ,mw.webhook_serial --- ,mw.url --- ,mw.http_method --- ,json_build_object('order_id', my_order_id)::TEXT --- FROM merchant_webhook mw --- WHERE mw.event_type = 'order_settled' --- AND mw.merchant_serial = my_merchant_serial; END $$; diff --git a/src/backenddb/pg_insert_login_token.c b/src/backenddb/pg_insert_login_token.c @@ -33,7 +33,8 @@ TMH_PG_insert_login_token ( const struct TALER_MERCHANTDB_LoginTokenP *token, struct GNUNET_TIME_Timestamp creation_time, struct GNUNET_TIME_Timestamp expiration_time, - uint32_t validity_scope) + uint32_t validity_scope, + const char *description) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -42,6 +43,7 @@ TMH_PG_insert_login_token ( GNUNET_PQ_query_param_timestamp (&creation_time), GNUNET_PQ_query_param_timestamp (&expiration_time), GNUNET_PQ_query_param_uint32 (&validity_scope), + GNUNET_PQ_query_param_string (description), GNUNET_PQ_query_param_end }; @@ -53,9 +55,10 @@ TMH_PG_insert_login_token ( ",creation_time" ",expiration_time" ",validity_scope" + ",description" ",merchant_serial" ")" - "SELECT $2, $3, $4, $5, merchant_serial" + "SELECT $2, $3, $4, $5, $6, merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, diff --git a/src/backenddb/pg_insert_login_token.h b/src/backenddb/pg_insert_login_token.h @@ -44,7 +44,8 @@ TMH_PG_insert_login_token ( const struct TALER_MERCHANTDB_LoginTokenP *token, struct GNUNET_TIME_Timestamp creation_time, struct GNUNET_TIME_Timestamp expiration_time, - uint32_t validity_scope); + uint32_t validity_scope, + const char*description); #endif diff --git a/src/backenddb/pg_insert_product.c b/src/backenddb/pg_insert_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022-2024 Taler Systems SA + Copyright (C) 2022-2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -26,6 +26,7 @@ #include "pg_insert_product.h" #include "pg_helper.h" + enum GNUNET_DB_QueryStatus TMH_PG_insert_product (void *cls, const char *instance_id, @@ -55,6 +56,7 @@ TMH_PG_insert_product (void *cls, GNUNET_PQ_query_param_array_uint64 (num_cats, cats, pg->conn), + GNUNET_PQ_query_param_string (pd->product_name), GNUNET_PQ_query_param_end }; uint64_t ncat; @@ -80,7 +82,7 @@ TMH_PG_insert_product (void *cls, ",out_no_cat AS no_cat" ",out_no_instance AS no_instance" " FROM merchant_do_insert_product" - "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);"); + "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "insert_product", params, diff --git a/src/backenddb/pg_insert_product.sql b/src/backenddb/pg_insert_product.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- Copyright (C) 2024, 2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -15,7 +15,8 @@ -- -CREATE OR REPLACE FUNCTION merchant_do_insert_product ( +DROP FUNCTION IF EXISTS merchant_do_insert_product; +CREATE FUNCTION merchant_do_insert_product ( IN in_instance_id TEXT, IN in_product_id TEXT, IN in_description TEXT, @@ -29,6 +30,7 @@ CREATE OR REPLACE FUNCTION merchant_do_insert_product ( IN in_next_restock INT8, IN in_minimum_age INT4, IN ina_categories INT8[], + IN in_product_name TEXT, OUT out_no_instance BOOL, OUT out_conflict BOOL, OUT out_no_cat INT8) @@ -59,6 +61,7 @@ out_no_instance=FALSE; INSERT INTO merchant_inventory (merchant_serial ,product_id + ,product_name ,description ,description_i18n ,unit @@ -72,6 +75,7 @@ INSERT INTO merchant_inventory ) VALUES ( my_merchant_id ,in_product_id + ,in_product_name ,in_description ,in_description_i18n ,in_unit @@ -95,6 +99,7 @@ THEN FROM merchant_inventory WHERE merchant_serial=my_merchant_id AND product_id=in_product_id + AND product_name=in_product_name AND description=in_description AND description_i18n=in_description_i18n AND unit=in_unit diff --git a/src/backenddb/pg_insert_transfer.c b/src/backenddb/pg_insert_transfer.c @@ -34,21 +34,30 @@ TMH_PG_insert_transfer ( const struct TALER_WireTransferIdentifierRawP *wtid, const struct TALER_Amount *credit_amount, struct TALER_FullPayto payto_uri, - bool confirmed) + uint64_t bank_serial_id) { struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (exchange_url), GNUNET_PQ_query_param_auto_from_type (wtid), TALER_PQ_query_param_amount_with_currency (pg->conn, credit_amount), GNUNET_PQ_query_param_string (payto_uri.full_payto), - GNUNET_PQ_query_param_bool (confirmed), GNUNET_PQ_query_param_string (instance_id), + 0 == bank_serial_id + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_uint64 (&bank_serial_id), + GNUNET_PQ_query_param_absolute_time (&now), GNUNET_PQ_query_param_end }; check_connection (pg); + // FIXME-#10176: if transfer with matching exchange_url, wtid and credit_amount + // and account_serial exists already AND where bank_serial_id is NULL + // and if our bank_serial_id is NOT NULL, then maybe UPDATE instead? + // (user may have switched from manual import to automatic import, + // and now we may be duplicating all the records, which would be bad). PREPARE (pg, "insert_transfer", "INSERT INTO merchant_transfers" @@ -56,16 +65,17 @@ TMH_PG_insert_transfer ( ",wtid" ",credit_amount" ",account_serial" - ",confirmed)" + ",bank_serial_id" + ",execution_time)" "SELECT" - " $1, $2, $3, account_serial, $5" + " $1, $2, $3, account_serial, $6, $7" " FROM merchant_accounts" " WHERE REGEXP_REPLACE(payto_uri,'\\?.*','')" " =REGEXP_REPLACE($4,'\\?.*','')" " AND merchant_serial=" " (SELECT merchant_serial" " FROM merchant_instances" - " WHERE merchant_id=$6)" + " WHERE merchant_id=$5)" " ON CONFLICT DO NOTHING;"); return GNUNET_PQ_eval_prepared_non_select (pg->conn, "insert_transfer", diff --git a/src/backenddb/pg_insert_transfer.h b/src/backenddb/pg_insert_transfer.h @@ -34,8 +34,8 @@ * @param wtid identifier of the wire transfer * @param credit_amount how much did we receive * @param payto_uri what is the merchant's bank account that received the transfer - * @param confirmed whether the transfer was confirmed by the merchant or - * was merely claimed by the exchange at this point + * @param bank_serial_id unique ID for the wire transfer at the bank, + * 0 for "NULL" if none is known due to manual import * @return transaction status */ enum GNUNET_DB_QueryStatus @@ -46,7 +46,7 @@ TMH_PG_insert_transfer ( const struct TALER_WireTransferIdentifierRawP *wtid, const struct TALER_Amount *credit_amount, struct TALER_FullPayto payto_uri, - bool confirmed); + uint64_t bank_serial_id); #endif diff --git a/src/backenddb/pg_insert_transfer_details.c b/src/backenddb/pg_insert_transfer_details.c @@ -48,6 +48,7 @@ TMH_PG_insert_transfer_details ( const struct TALER_CoinSpendPublicKeyP *coin_pubs[GNUNET_NZL (len)]; const struct TALER_PrivateContractHashP *contract_terms[GNUNET_NZL (len)]; enum GNUNET_DB_QueryStatus qs; + bool duplicate; for (unsigned int i = 0; i<len; i++) { @@ -117,7 +118,6 @@ TMH_PG_insert_transfer_details ( bool no_instance; bool no_account; bool no_exchange; - bool duplicate; bool conflict; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_bool ("out_no_instance", @@ -167,5 +167,7 @@ TMH_PG_insert_transfer_details ( if (GNUNET_DB_STATUS_SOFT_ERROR != qs) break; } + if (duplicate) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; return qs; } diff --git a/src/backenddb/pg_insert_transfer_details.sql b/src/backenddb/pg_insert_transfer_details.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- Copyright (C) 2024, 2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -15,7 +15,8 @@ -- -CREATE OR REPLACE FUNCTION merchant_do_insert_transfer_details ( +DROP FUNCTION IF EXISTS merchant_do_insert_transfer_details; +CREATE FUNCTION merchant_do_insert_transfer_details ( IN in_instance_id TEXT, IN in_exchange_url TEXT, IN in_payto_uri TEXT, @@ -39,9 +40,10 @@ AS $$ DECLARE my_merchant_id INT8; my_signkey_serial INT8; - my_credit_serial INT8; + my_expected_credit_serial INT8; my_affected_orders RECORD; my_merchant_serial INT8; + my_decose INT8; my_order_id TEXT; i INT8; curs CURSOR (arg_coin_pub BYTEA) FOR @@ -75,9 +77,9 @@ END IF; out_no_instance=FALSE; -- Determine account that was credited. -SELECT credit_serial - INTO my_credit_serial - FROM merchant_transfers +SELECT expected_credit_serial + INTO my_expected_credit_serial + FROM merchant_expected_transfers WHERE exchange_url=in_exchange_url AND wtid=in_wtid AND account_serial= @@ -116,14 +118,14 @@ out_no_exchange=FALSE; -- Add signature first, check for idempotent request INSERT INTO merchant_transfer_signatures - (credit_serial + (expected_credit_serial ,signkey_serial ,credit_amount ,wire_fee ,execution_time ,exchange_sig) VALUES - (my_credit_serial + (my_expected_credit_serial ,my_signkey_serial ,in_total_amount ,in_wire_fee @@ -135,7 +137,7 @@ IF NOT FOUND THEN PERFORM 1 FROM merchant_transfer_signatures - WHERE credit_serial=my_credit_serial + WHERE expected_credit_serial=my_expected_credit_serial AND signkey_serial=my_signkey_serial AND credit_amount=in_total_amount AND wire_fee=in_wire_fee @@ -165,15 +167,15 @@ LOOP ini_coin_pub=ina_coin_pubs[i]; ini_contract_term=ina_contract_terms[i]; - INSERT INTO merchant_transfer_to_coin + INSERT INTO merchant_expected_transfer_to_coin (deposit_serial - ,credit_serial + ,expected_credit_serial ,offset_in_exchange_list ,exchange_deposit_value ,exchange_deposit_fee) SELECT dep.deposit_serial - ,my_credit_serial + ,my_expected_credit_serial ,i ,ini_coin_value ,ini_deposit_fee @@ -194,61 +196,62 @@ LOOP RAISE NOTICE 'checking affected order for completion'; - -- First, check if deposit confirmation is done. - UPDATE merchant_deposit_confirmations - SET wire_pending=FALSE - WHERE (deposit_confirmation_serial=my_affected_orders.deposit_confirmation_serial) - AND NOT EXISTS - (SELECT 1 - FROM merchant_deposits md - LEFT JOIN merchant_deposit_to_transfer mdtt - USING (deposit_serial) - WHERE md.deposit_confirmation_serial=my_affected_orders.deposit_confirmation_serial - AND mdtt.wtid IS NULL); - -- wtid will be NULL due to LEFT JOIN - -- if we do not have an entry in mdtt for the deposit - -- and thus some entry in md was not yet wired. + my_decose=my_affected_orders.deposit_confirmation_serial; - IF FOUND + PERFORM FROM merchant_deposits md + WHERE md.deposit_confirmation_serial=my_decose + AND settlement_retry_needed + OR settlement_wtid IS NULL; + IF NOT FOUND THEN - -- Also update contract terms, if all (other) associated - -- deposit_confirmations are also done. + -- must be all done, clear flag + UPDATE merchant_deposit_confirmations + SET wire_pending=FALSE + WHERE (deposit_confirmation_serial=my_decose); - UPDATE merchant_contract_terms - SET wired=TRUE - WHERE (order_serial=my_affected_orders.order_serial) - AND NOT EXISTS - (SELECT 1 - FROM merchant_deposit_confirmations mdc - WHERE mdc.wire_pending - AND mdc.order_serial=my_affected_orders.order_serial); + IF FOUND + THEN + -- Also update contract terms, if all (other) associated + -- deposit_confirmations are also done. - -- Select merchant_serial and order_id for webhook - SELECT merchant_serial, order_id - INTO my_merchant_serial, my_order_id - FROM merchant_contract_terms - WHERE order_serial=my_affected_orders.order_serial; + RAISE NOTICE 'checking affected contract for completion'; + PERFORM FROM merchant_deposit_confirmations mdc + WHERE mdc.wire_pending + AND mdc.order_serial=my_affected_orders.order_serial; + IF NOT FOUND + THEN - -- Insert pending webhook if it exists - INSERT INTO merchant_pending_webhooks - (merchant_serial - ,webhook_serial - ,url - ,http_method - ,body) - SELECT mw.merchant_serial - ,mw.webhook_serial - ,mw.url - ,mw.http_method - ,replace_placeholder( - replace_placeholder(mw.body_template, 'order_id', my_order_id), - 'wtid', encode(in_wtid, 'hex') - )::TEXT - FROM merchant_webhook mw - WHERE mw.event_type = 'order_settled' - AND mw.merchant_serial = my_merchant_serial; + UPDATE merchant_contract_terms + SET wired=TRUE + WHERE (order_serial=my_affected_orders.order_serial); - END IF; + -- Select merchant_serial and order_id for webhook + SELECT merchant_serial, order_id + INTO my_merchant_serial, my_order_id + FROM merchant_contract_terms + WHERE order_serial=my_affected_orders.order_serial; + + -- Insert pending webhook if it exists + INSERT INTO merchant_pending_webhooks + (merchant_serial + ,webhook_serial + ,url + ,http_method + ,body) + SELECT mw.merchant_serial + ,mw.webhook_serial + ,mw.url + ,mw.http_method + ,replace_placeholder( + replace_placeholder(mw.body_template, 'order_id', my_order_id), + 'wtid', encode(in_wtid, 'hex') + )::TEXT + FROM merchant_webhook mw + WHERE mw.event_type = 'order_settled' + AND mw.merchant_serial = my_merchant_serial; + END IF; -- no more merchant_deposits waiting for wire_pending + END IF; -- did clear wire_pending flag for deposit confirmation + END IF; -- no more merchant_deposits wait for settlement END LOOP; -- END curs LOOP CLOSE curs; diff --git a/src/backenddb/pg_lookup_all_products.c b/src/backenddb/pg_lookup_all_products.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2024 Taler Systems SA + Copyright (C) 2024, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -80,6 +80,8 @@ lookup_products_cb (void *cls, &product_id), GNUNET_PQ_result_spec_uint64 ("product_serial", &product_serial), + GNUNET_PQ_result_spec_string ("product_name", + &pd.product_name), GNUNET_PQ_result_spec_string ("description", &pd.description), TALER_PQ_result_spec_json ("description_i18n", @@ -157,6 +159,7 @@ TMH_PG_lookup_all_products (void *cls, "SELECT" " description" ",description_i18n" + ",product_name" ",unit" ",price" ",taxes" diff --git a/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c b/src/backenddb/pg_lookup_deposits_by_contract_and_coin.c @@ -306,7 +306,7 @@ TMH_PG_lookup_deposits_by_contract_and_coin ( " JOIN merchant_deposits dep" " USING (deposit_confirmation_serial)" " JOIN merchant_exchange_signing_keys msig" - " USING (signkey_serial)" + " ON (mcon.signkey_serial=msig.signkey_serial)" " JOIN merchant_accounts acc" " USING (account_serial)" " WHERE h_contract_terms=$2" diff --git a/src/backenddb/pg_lookup_expected_transfers.c b/src/backenddb/pg_lookup_expected_transfers.c @@ -0,0 +1,272 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_expected_transfers.c + * @brief Implementation of the lookup_expected_transfers function for Postgres + * @author Christian Grothoff + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_expected_transfers.h" +#include "pg_helper.h" +#include <microhttpd.h> /* for HTTP status codes */ + +/** + * Closure for #lookup_expected_transfers_cb(). + */ +struct LookupExpectedTransfersContext +{ + /** + * Function to call on results. + */ + TALER_MERCHANTDB_IncomingCallback cb; + + /** + * Closure for @e cb. + */ + void *cb_cls; + + /** + * Postgres context. + */ + struct PostgresClosure *pg; + + /** + * Transaction status (set). + */ + enum GNUNET_DB_QueryStatus qs; + +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results. + * + * @param cls of type `struct LookupExpectedTransfersContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_expected_transfers_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupExpectedTransfersContext *ltc = cls; + + for (unsigned int i = 0; i<num_results; i++) + { + struct TALER_Amount expected_credit_amount; + struct TALER_WireTransferIdentifierRawP wtid; + struct TALER_FullPayto payto_uri; + char *exchange_url; + uint64_t expected_transfer_serial_id; + struct GNUNET_TIME_Timestamp execution_time; + bool confirmed; + bool validated; + char *last_detail = NULL; + uint32_t last_http_status = 0; + uint32_t last_ec = TALER_EC_NONE; + struct GNUNET_PQ_ResultSpec rs[] = { + TALER_PQ_result_spec_amount_with_currency ("expected_credit_amount", + &expected_credit_amount), + GNUNET_PQ_result_spec_auto_from_type ("wtid", + &wtid), + GNUNET_PQ_result_spec_string ("payto_uri", + &payto_uri.full_payto), + GNUNET_PQ_result_spec_string ("exchange_url", + &exchange_url), + GNUNET_PQ_result_spec_uint64 ("expected_credit_serial", + &expected_transfer_serial_id), + GNUNET_PQ_result_spec_timestamp ("execution_time", + &execution_time), + GNUNET_PQ_result_spec_bool ("confirmed", + &confirmed), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint32 ("last_http_status", + &last_http_status), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_uint32 ("last_ec", + &last_ec), + NULL), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("last_detail", + &last_detail), + NULL), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + ltc->qs = GNUNET_DB_STATUS_HARD_ERROR; + return; + } + validated = ( (MHD_HTTP_OK == last_http_status) && + (TALER_EC_NONE == last_ec) ); + ltc->cb (ltc->cb_cls, + &expected_credit_amount, + &wtid, + payto_uri, + exchange_url, + expected_transfer_serial_id, + execution_time, + confirmed, + validated, + last_http_status, + last_ec, + last_detail); + GNUNET_PQ_cleanup_result (rs); + } + ltc->qs = num_results; +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_expected_transfers ( + void *cls, + const char *instance_id, + struct TALER_FullPayto payto_uri, + struct GNUNET_TIME_Timestamp before, + struct GNUNET_TIME_Timestamp after, + int64_t limit, + uint64_t offset, + enum TALER_EXCHANGE_YesNoAll confirmed, + enum TALER_EXCHANGE_YesNoAll verified, + TALER_MERCHANTDB_IncomingCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + bool by_time = ( (! GNUNET_TIME_absolute_is_never (before.abs_time)) || + (! GNUNET_TIME_absolute_is_zero (after.abs_time)) ); + struct LookupExpectedTransfersContext ltc = { + .cb = cb, + .cb_cls = cb_cls, + .pg = pg + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_timestamp (&before), + GNUNET_PQ_query_param_timestamp (&after), + GNUNET_PQ_query_param_uint64 (&offset), + GNUNET_PQ_query_param_uint64 (&plimit), + NULL == payto_uri.full_payto + ? GNUNET_PQ_query_param_null () /* NULL: do not filter by payto URI */ + : GNUNET_PQ_query_param_string (payto_uri.full_payto), + GNUNET_PQ_query_param_bool (! by_time), /* $7: filter by time? */ + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == confirmed), /* filter by confirmed? */ + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == confirmed), + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == verified), /* filter by verified? */ + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == verified), + + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_expected_transfers_asc", + "SELECT" + " met.expected_credit_amount" + ",met.wtid" + ",mac.payto_uri" + ",met.exchange_url" + ",met.expected_credit_serial" + ",mts.execution_time" + ",met.confirmed" + ",met.last_http_status" + ",met.last_ec" + ",met.last_detail" + " FROM merchant_expected_transfers met" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_transfer_signatures mts" + " USING (expected_credit_serial)" + " WHERE ( $7 OR " + " (mts.execution_time IS NOT NULL AND" + " mts.execution_time < $2 AND" + " mts.execution_time >= $3) )" + " AND ( (CAST($6 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($6,'\\?.*','')) )" + " AND ( $8 OR " + " (mt.confirmed = $9) )" + " AND ( $10 OR " + " ($11 = (200=mt.last_http_status) AND" + " (0=mt.last_ec) ) )" + " AND merchant_serial =" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1)" + " AND (met.expected_credit_serial > $4)" + " ORDER BY met.expected_credit_serial ASC" + " LIMIT $5"); + PREPARE (pg, + "lookup_expected_transfers_desc", + "SELECT" + " met.expected_credit_amount" + ",met.wtid" + ",mac.payto_uri" + ",met.exchange_url" + ",met.expected_credit_serial" + ",mts.execution_time" + ",met.confirmed" + ",met.last_http_status" + ",met.last_ec" + ",met.last_detail" + " FROM merchant_expected_transfers met" + " JOIN merchant_accounts mac" + " USING (account_serial)" + " LEFT JOIN merchant_transfer_signatures mts" + " USING (expected_credit_serial)" + " WHERE ( $7 OR " + " (mts.execution_time IS NOT NULL AND" + " mts.execution_time < $2 AND" + " mts.execution_time >= $3) )" + " AND ( (CAST($6 AS TEXT) IS NULL) OR " + " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" + " =REGEXP_REPLACE($6,'\\?.*','')) )" + " AND ( $8 OR " + " (mt.expected = $9) )" + " AND ( $10 OR " + " ($11 = (200=mt.last_http_status) AND" + " (0=mt.last_ec) ) )" + " AND merchant_serial =" + " (SELECT merchant_serial" + " FROM merchant_instances" + " WHERE merchant_id=$1)" + " AND (met.expected_credit_serial < $4)" + " ORDER BY met.expected_credit_serial DESC" + " LIMIT $5"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + (limit > 0) + ? "lookup_expected_transfers_asc" + : "lookup_expected_transfers_desc", + params, + &lookup_expected_transfers_cb, + <c); + if (0 >= qs) + return qs; + return ltc.qs; +} diff --git a/src/backenddb/pg_lookup_expected_transfers.h b/src/backenddb/pg_lookup_expected_transfers.h @@ -0,0 +1,61 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_expected_transfers.h + * @brief implementation of the lookup_expected_transfers function for Postgres + * @author Christian Grothoff + */ +#ifndef PG_LOOKUP_EXPECTED_TRANSFERS_H +#define PG_LOOKUP_EXPECTED_TRANSFERS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + + +/** + * Lookup expected incoming transfers. + * + * @param cls closure + * @param instance_id instance to lookup payments for + * @param payto_uri account that we are interested in transfers to + * @param before timestamp for the earliest transfer we care about + * @param after timestamp for the last transfer we care about + * @param limit number of entries to return, negative for descending in execution time, + * positive for ascending in execution time + * @param offset expected_transfer_serial number of the transfer we want to offset from + * @param confirmed filter by confirmation status + * @param verified filter by verification status + * @param cb function to call with detailed transfer data + * @param cb_cls closure for @a cb + * @return transaction status + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_expected_transfers ( + void *cls, + const char *instance_id, + struct TALER_FullPayto payto_uri, + struct GNUNET_TIME_Timestamp before, + struct GNUNET_TIME_Timestamp after, + int64_t limit, + uint64_t offset, + enum TALER_EXCHANGE_YesNoAll confirmed, + enum TALER_EXCHANGE_YesNoAll verified, + TALER_MERCHANTDB_IncomingCallback cb, + void *cb_cls); + + +#endif diff --git a/src/backenddb/pg_lookup_login_tokens.c b/src/backenddb/pg_lookup_login_tokens.c @@ -0,0 +1,182 @@ +/* + 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 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_products.c + * @brief Implementation of the lookup_products function for Postgres + * @author Iván Ávalos + */ +#include "platform.h" +#include <taler/taler_error_codes.h> +#include <taler/taler_dbevents.h> +#include <taler/taler_pq_lib.h> +#include "pg_lookup_login_tokens.h" +#include "pg_helper.h" + +/** + * Context used for TMH_PG_lookup_products(). + */ +struct LookupLoginTokensContext +{ + /** + * Function to call with the results. + */ + TALER_MERCHANTDB_LoginTokensCallback cb; + + /** + * Closure for @a cb. + */ + void *cb_cls; + + /** + * Did database result extraction fail? + */ + bool extract_failed; +}; + + +/** + * Function to be called with the results of a SELECT statement + * that has returned @a num_results results about products. + * + * @param[in,out] cls of type `struct LookupProductsContext *` + * @param result the postgres result + * @param num_results the number of results in @a result + */ +static void +lookup_login_tokens_cb (void *cls, + PGresult *result, + unsigned int num_results) +{ + struct LookupLoginTokensContext *plc = cls; + + for (unsigned int i = 0; i < num_results; i++) + { + uint32_t validity_scope; + uint64_t serial; + struct GNUNET_TIME_Timestamp expiration_time; + struct GNUNET_TIME_Timestamp creation_time; + char *description; + struct TALER_MERCHANTDB_LoginTokenP token; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_auto_from_type ("token", + &token), + GNUNET_PQ_result_spec_uint32 ("validity_scope", + &validity_scope), + GNUNET_PQ_result_spec_string ("description", + &description), + GNUNET_PQ_result_spec_timestamp ("creation_time", + &creation_time), + GNUNET_PQ_result_spec_timestamp ("expiration_time", + &expiration_time), + GNUNET_PQ_result_spec_uint64 ("serial", + &serial), + GNUNET_PQ_result_spec_end + }; + + if (GNUNET_OK != + GNUNET_PQ_extract_result (result, + rs, + i)) + { + GNUNET_break (0); + plc->extract_failed = true; + return; + } + plc->cb (plc->cb_cls, + creation_time, + expiration_time, + validity_scope, + description, + serial); + GNUNET_PQ_cleanup_result (rs); + } +} + + +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_login_tokens (void *cls, + const char *instance_id, + uint64_t offset, + int64_t limit, + TALER_MERCHANTDB_LoginTokensCallback cb, + void *cb_cls) +{ + struct PostgresClosure *pg = cls; + struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); + uint64_t plimit = (uint64_t) ((limit < 0) ? -limit : limit); + struct LookupLoginTokensContext plc = { + .cb = cb, + .cb_cls = cb_cls, + /* Can be overwritten by the lookup_products_cb */ + .extract_failed = false, + }; + struct GNUNET_PQ_QueryParam params[] = { + GNUNET_PQ_query_param_string (instance_id), + GNUNET_PQ_query_param_timestamp (&now), + GNUNET_PQ_query_param_uint64 (&offset), + GNUNET_PQ_query_param_uint64 (&plimit), + GNUNET_PQ_query_param_end + }; + enum GNUNET_DB_QueryStatus qs; + + check_connection (pg); + PREPARE (pg, + "lookup_login_tokens_asc", + "SELECT" + " token" + ",serial" + ",creation_time" + ",expiration_time" + ",validity_scope" + ",description" + " FROM merchant_login_tokens" + " JOIN merchant_instances" + " USING (merchant_serial)" + " WHERE merchant_instances.merchant_id=$1" + " AND expiration_time > $2" + " ORDER BY serial ASC" + " OFFSET $3" + " LIMIT $4"); + PREPARE (pg, + "lookup_login_tokens_desc", + "SELECT" + " token" + ",serial" + ",creation_time" + ",expiration_time" + ",validity_scope" + ",description" + " FROM merchant_login_tokens" + " JOIN merchant_instances" + " USING (merchant_serial)" + " WHERE merchant_instances.merchant_id=$1" + " AND expiration_time > $2" + " ORDER BY serial DESC" + " OFFSET $3" + " LIMIT $4"); + qs = GNUNET_PQ_eval_prepared_multi_select ( + pg->conn, + (limit > 0) + ? "lookup_login_tokens_asc" + : "lookup_login_tokens_desc", + params, + &lookup_login_tokens_cb, + &plc); + /* If there was an error inside lookup_products_cb, return a hard error. */ + if (plc.extract_failed) + return GNUNET_DB_STATUS_HARD_ERROR; + return qs; +} diff --git a/src/backenddb/pg_lookup_login_tokens.h b/src/backenddb/pg_lookup_login_tokens.h @@ -0,0 +1,48 @@ +/* + This file is part of TALER + Copyright (C) 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU 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 General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ +/** + * @file backenddb/pg_lookup_login_tokens.h + * @brief implementation of the lookup_login_tokens function for Postgres + * @author Martin Schanzenbach + */ +#ifndef PG_LOOKUP_LOGIN_TOKENS_H +#define PG_LOOKUP_LOGIN_TOKENS_H + +#include <taler/taler_util.h> +#include <taler/taler_json_lib.h> +#include "taler_merchantdb_plugin.h" + +/** + * Lookup all of the tokens the given instance has configured. + * + * @param cls closure + * @param instance_id instance to lookup tokens for + * @param offset transfer_serial number of the transfer we want to offset from + * @param limit number of entries to return, negative for descending, + * positive for ascending + * @param cb function to call on all products found + * @param cb_cls closure for @a cb + * @return database result code + */ +enum GNUNET_DB_QueryStatus +TMH_PG_lookup_login_tokens (void *cls, + const char *instance_id, + uint64_t offset, + int64_t limit, + TALER_MERCHANTDB_LoginTokensCallback cb, + void *cb_cls); + +#endif diff --git a/src/backenddb/pg_lookup_pending_deposits.c b/src/backenddb/pg_lookup_pending_deposits.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -73,7 +73,7 @@ lookup_deposits_cb (void *cls, { uint64_t deposit_serial; struct GNUNET_TIME_Absolute wire_deadline; - struct GNUNET_TIME_Relative retry_backoff; + struct GNUNET_TIME_Absolute retry_time; struct TALER_PrivateContractHashP h_contract_terms; struct TALER_MerchantPrivateKeyP merchant_priv; char *instance_id; @@ -92,8 +92,8 @@ lookup_deposits_cb (void *cls, &instance_id), GNUNET_PQ_result_spec_absolute_time ("wire_transfer_deadline", &wire_deadline), - GNUNET_PQ_result_spec_relative_time ("retry_backoff", - &retry_backoff), + GNUNET_PQ_result_spec_absolute_time ("retry_time", + &retry_time), GNUNET_PQ_result_spec_auto_from_type ("h_wire", &h_wire), TALER_PQ_result_spec_amount_with_currency ("amount_with_fee", @@ -117,7 +117,7 @@ lookup_deposits_cb (void *cls, ldc->cb (ldc->cb_cls, deposit_serial, wire_deadline, - retry_backoff, + retry_time, &h_contract_terms, &merchant_priv, instance_id, @@ -165,29 +165,29 @@ TMH_PG_lookup_pending_deposits ( ",mk.merchant_priv" ",mi.merchant_id" ",mdc.wire_transfer_deadline" + ",md.settlement_retry_time AS retry_time" ",ma.h_wire" ",md.amount_with_fee" ",md.deposit_fee" ",md.coin_pub" - ",mdc.retry_backoff" - " FROM merchant_deposit_confirmations mdc" + " FROM merchant_deposits md" + " JOIN merchant_deposit_confirmations mdc" + " USING (deposit_confirmation_serial)" " JOIN merchant_contract_terms mct" - " USING (order_serial)" + " ON (mct.order_serial=mdc.order_serial)" " JOIN merchant_accounts ma" - " USING (account_serial)" + " ON (mdc.account_serial=ma.account_serial)" " LEFT JOIN merchant_kyc kyc" - " ON (ma.account_serial=kyc.account_serial)" + " ON (mdc.account_serial=kyc.account_serial)" " JOIN merchant_instances mi" " ON (mct.merchant_serial=mi.merchant_serial)" " JOIN merchant_keys mk" " ON (mi.merchant_serial=mk.merchant_serial)" - " JOIN merchant_deposits md" - " USING (deposit_confirmation_serial)" - " WHERE mdc.wire_pending" - " AND (mdc.exchange_url=$1)" - " AND ($4 OR (mdc.wire_transfer_deadline < $2))" + " WHERE (mdc.exchange_url=$1)" + " AND md.settlement_retry_needed" + " AND ($4 OR (md.settlement_retry_time < $2))" " AND ( (kyc.kyc_ok IS NULL) OR kyc.kyc_ok)" - " ORDER BY mdc.wire_transfer_deadline ASC" + " ORDER BY md.settlement_retry_time ASC" " LIMIT $3"); qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn, "lookup_pending_deposits", diff --git a/src/backenddb/pg_lookup_product.c b/src/backenddb/pg_lookup_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -46,6 +46,7 @@ TMH_PG_lookup_product (void *cls, "SELECT" " mi.description" ",mi.description_i18n" + ",mi.product_name" ",mi.unit" ",mi.price" ",mi.taxes" @@ -84,6 +85,7 @@ TMH_PG_lookup_product (void *cls, } else { + char *my_name = NULL; char *my_description = NULL; json_t *my_description_i18n = NULL; char *my_unit = NULL; @@ -96,6 +98,8 @@ TMH_PG_lookup_product (void *cls, &my_description), TALER_PQ_result_spec_json ("description_i18n", &my_description_i18n), + GNUNET_PQ_result_spec_string ("product_name", + &my_name), GNUNET_PQ_result_spec_string ("unit", &my_unit), TALER_PQ_result_spec_amount_with_currency ("price", @@ -129,6 +133,7 @@ TMH_PG_lookup_product (void *cls, "lookup_product", params, rs); + pd->product_name = my_name; pd->description = my_description; pd->description_i18n = my_description_i18n; pd->unit = my_unit; @@ -137,6 +142,7 @@ TMH_PG_lookup_product (void *cls, pd->address = my_address; *categories = my_categories; /* Clear original pointers to that cleanup_result doesn't squash them */ + my_name = NULL; my_description = NULL; my_description_i18n = NULL; my_unit = NULL; diff --git a/src/backenddb/pg_lookup_transfer.c b/src/backenddb/pg_lookup_transfer.c @@ -1,125 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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 General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file backenddb/pg_lookup_transfer.c - * @brief Implementation of the lookup_transfer function for Postgres - * @author Iván Ávalos - */ -#include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_pq_lib.h> -#include "pg_lookup_transfer.h" -#include "pg_helper.h" - -enum GNUNET_DB_QueryStatus -TMH_PG_lookup_transfer (void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_Amount *total_amount, - struct TALER_Amount *wire_fee, - struct TALER_Amount *exchange_amount, - struct GNUNET_TIME_Timestamp *execution_time, - bool *have_exchange_sig, - bool *verified) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (exchange_url), - GNUNET_PQ_query_param_auto_from_type (wtid), - GNUNET_PQ_query_param_string (instance_id), - GNUNET_PQ_query_param_end - }; - uint8_t verified8; - /** Amount we got actually credited, _excludes_ the wire fee */ - bool no_sig; - struct TALER_Amount credit_amount; - struct GNUNET_PQ_ResultSpec rs[] = { - TALER_PQ_result_spec_amount_with_currency ("credit_amount", - &credit_amount), - GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_amount_with_currency ("wire_fee", - wire_fee), - &no_sig), - GNUNET_PQ_result_spec_allow_null ( - TALER_PQ_result_spec_amount_with_currency ("exchange_amount", - exchange_amount), - NULL), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_timestamp ("execution_time", - execution_time), - NULL), - GNUNET_PQ_result_spec_auto_from_type ("verified", - &verified8), - GNUNET_PQ_result_spec_end - }; - enum GNUNET_DB_QueryStatus qs; - - check_connection (pg); - *execution_time = GNUNET_TIME_UNIT_ZERO_TS; - - PREPARE (pg, - "lookup_transfer", - "SELECT" - " mt.credit_amount AS credit_amount" - ",mts.credit_amount AS exchange_amount" - ",wire_fee" - ",execution_time" - ",verified" - " FROM merchant_transfers mt" - " JOIN merchant_accounts USING (account_serial)" - " JOIN merchant_instances USING (merchant_serial)" - " LEFT JOIN merchant_transfer_signatures mts USING (credit_serial)" - " WHERE wtid=$2" - " AND exchange_url=$1" - " AND merchant_id=$3;"); - - qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, - "lookup_transfer", - params, - rs); - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Lookup transfer returned %d\n", - qs); - if (qs > 0) - { - *have_exchange_sig = ! no_sig; - *verified = (0 != verified8); - if (GNUNET_OK != - TALER_amount_cmp_currency (&credit_amount, - wire_fee)) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - if ( (! no_sig) && - (0 > - TALER_amount_add (total_amount, - &credit_amount, - wire_fee)) ) - { - GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; - } - } - else - { - *verified = false; - *have_exchange_sig = false; - } - return qs; -} diff --git a/src/backenddb/pg_lookup_transfer.h b/src/backenddb/pg_lookup_transfer.h @@ -1,57 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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 General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file backenddb/pg_lookup_transfer.h - * @brief implementation of the lookup_transfer function for Postgres - * @author Iván Ávalos - */ -#ifndef PG_LOOKUP_TRANSFER_H -#define PG_LOOKUP_TRANSFER_H - -#include <taler/taler_util.h> -#include <taler/taler_json_lib.h> -#include "taler_merchantdb_plugin.h" - -/** - * Lookup transfer status. - * - * @param cls closure - * @param instance_id at which instance should we resolve the transfer - * @param exchange_url the exchange that made the transfer - * @param wtid wire transfer subject - * @param[out] total_amount amount that was debited from our - * aggregate balance at the exchange (in total, sum of - * the wire transfer amount and the @a wire_fee) - * @param[out] wire_fee the wire fee the exchange charged (only set if @a have_exchange_sig is true) - * @param[out] exchange_amount the amount the exchange claims was transferred (only set if @a have_exchange_sig is true) - * @param[out] execution_time when the transfer was executed by the exchange (only set if @a have_exchange_sig is true) - * @param[out] have_exchange_sig do we have a response from the exchange about this transfer - * @param[out] verified did we confirm the transfer was OK - * @return transaction status - */ -enum GNUNET_DB_QueryStatus -TMH_PG_lookup_transfer (void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_Amount *total_amount, - struct TALER_Amount *wire_fee, - struct TALER_Amount *exchange_amount, - struct GNUNET_TIME_Timestamp *execution_time, - bool *have_exchange_sig, - bool *verified); - -#endif diff --git a/src/backenddb/pg_lookup_transfer_details.c b/src/backenddb/pg_lookup_transfer_details.c @@ -102,10 +102,12 @@ lookup_transfer_details_cb (void *cls, ltdc->qs = num_results; } + enum GNUNET_DB_QueryStatus TMH_PG_lookup_transfer_details (void *cls, const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_WireTransferIdentifierRawP * + wtid, TALER_MERCHANTDB_TransferDetailsCallback cb, void *cb_cls) { @@ -131,17 +133,17 @@ TMH_PG_lookup_transfer_details (void *cls, ",dep.coin_pub" ",mtcoin.exchange_deposit_value" ",mtcoin.exchange_deposit_fee" - " FROM merchant_transfer_to_coin mtcoin" + " FROM merchant_expected_transfer_to_coin mtcoin" " JOIN merchant_deposits dep" " USING (deposit_serial)" " JOIN merchant_deposit_confirmations mcon" " USING (deposit_confirmation_serial)" " JOIN merchant_contract_terms mterm" " USING (order_serial)" - " JOIN merchant_transfers mtr" - " USING (credit_serial)" - " WHERE mtr.wtid=$2" - " AND mtr.exchange_url=$1"); + " JOIN merchant_expected_transfers met" + " USING (expected_credit_serial)" + " WHERE met.wtid=$2" + " AND met.exchange_url=$1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, diff --git a/src/backenddb/pg_lookup_transfer_details_by_order.c b/src/backenddb/pg_lookup_transfer_details_by_order.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -76,7 +76,7 @@ lookup_transfer_details_by_order_cb (void *cls, struct GNUNET_TIME_Timestamp execution_time; struct TALER_Amount deposit_value; struct TALER_Amount deposit_fee; - uint8_t transfer_confirmed; + bool confirmed; struct GNUNET_PQ_ResultSpec rs[] = { GNUNET_PQ_result_spec_uint64 ("deposit_serial", &deposit_serial), @@ -84,14 +84,14 @@ lookup_transfer_details_by_order_cb (void *cls, &execution_time), GNUNET_PQ_result_spec_string ("exchange_url", &exchange_url), + GNUNET_PQ_result_spec_bool ("confirmed", + &confirmed), GNUNET_PQ_result_spec_auto_from_type ("wtid", &wtid), TALER_PQ_result_spec_amount_with_currency ("exchange_deposit_value", &deposit_value), TALER_PQ_result_spec_amount_with_currency ("exchange_deposit_fee", &deposit_fee), - GNUNET_PQ_result_spec_auto_from_type ("transfer_confirmed", - &transfer_confirmed), GNUNET_PQ_result_spec_end }; @@ -110,7 +110,7 @@ lookup_transfer_details_by_order_cb (void *cls, execution_time, &deposit_value, &deposit_fee, - (0 != transfer_confirmed)); + confirmed); GNUNET_PQ_cleanup_result (rs); /* technically useless here */ } ltdo->qs = num_results; @@ -118,10 +118,11 @@ lookup_transfer_details_by_order_cb (void *cls, enum GNUNET_DB_QueryStatus -TMH_PG_lookup_transfer_details_by_order (void *cls, - uint64_t order_serial, - TALER_MERCHANTDB_OrderTransferDetailsCallback cb, - void *cb_cls) +TMH_PG_lookup_transfer_details_by_order ( + void *cls, + uint64_t order_serial, + TALER_MERCHANTDB_OrderTransferDetailsCallback cb, + void *cb_cls) { struct PostgresClosure *pg = cls; struct LookupTransferDetailsByOrderContext ltdo = { @@ -141,20 +142,20 @@ TMH_PG_lookup_transfer_details_by_order (void *cls, "SELECT" " md.deposit_serial" ",mcon.exchange_url" - ",mt.wtid" + ",met.wtid" ",mtc.exchange_deposit_value" ",mtc.exchange_deposit_fee" ",mcon.deposit_timestamp" - ",mt.confirmed AS transfer_confirmed" - " FROM merchant_transfer_to_coin mtc" + ",met.confirmed" + " FROM merchant_expected_transfer_to_coin mtc" " JOIN merchant_deposits md" " USING (deposit_serial)" " JOIN merchant_deposit_confirmations mcon" " USING (deposit_confirmation_serial)" - " JOIN merchant_transfers mt" - " USING (credit_serial)" + " JOIN merchant_expected_transfers met" + " USING (expected_credit_serial)" " JOIN merchant_accounts acc" - " ON (acc.account_serial = mt.account_serial)" + " ON (acc.account_serial = met.account_serial)" /* Check that all this is for the same instance */ " JOIN merchant_contract_terms contracts" " USING (merchant_serial, order_serial)" diff --git a/src/backenddb/pg_lookup_transfer_summary.c b/src/backenddb/pg_lookup_transfer_summary.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2023 Taler Systems SA + Copyright (C) 2023, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -104,7 +104,8 @@ lookup_transfer_summary_cb (void *cls, enum GNUNET_DB_QueryStatus TMH_PG_lookup_transfer_summary (void *cls, const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, + const struct TALER_WireTransferIdentifierRawP * + wtid, TALER_MERCHANTDB_TransferSummaryCallback cb, void *cb_cls) { @@ -128,17 +129,17 @@ TMH_PG_lookup_transfer_summary (void *cls, " mct.order_id" ",mtc.exchange_deposit_value" ",mtc.exchange_deposit_fee" - " FROM merchant_transfers mtr" - " JOIN merchant_transfer_to_coin mtc" - " USING (credit_serial)" + " FROM merchant_expected_transfers met" + " JOIN merchant_expected_transfer_to_coin mtc" + " USING (expected_credit_serial)" " JOIN merchant_deposits dep" " USING (deposit_serial)" " JOIN merchant_deposit_confirmations mcon" " USING (deposit_confirmation_serial)" " JOIN merchant_contract_terms mct" " USING (order_serial)" - " WHERE mtr.wtid=$2" - " AND mtr.exchange_url=$1"); + " WHERE met.wtid=$2" + " AND met.exchange_url=$1"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, diff --git a/src/backenddb/pg_lookup_transfers.c b/src/backenddb/pg_lookup_transfers.c @@ -76,9 +76,8 @@ lookup_transfers_cb (void *cls, struct TALER_FullPayto payto_uri; char *exchange_url; uint64_t transfer_serial_id; - struct GNUNET_TIME_Timestamp execution_time = GNUNET_TIME_UNIT_FOREVER_TS; - bool verified; - bool confirmed; + struct GNUNET_TIME_Absolute execution_time; + bool expected; struct GNUNET_PQ_ResultSpec rs[] = { TALER_PQ_result_spec_amount_with_currency ("credit_amount", &credit_amount), @@ -90,14 +89,10 @@ lookup_transfers_cb (void *cls, &exchange_url), GNUNET_PQ_result_spec_uint64 ("credit_serial", &transfer_serial_id), - GNUNET_PQ_result_spec_allow_null ( - GNUNET_PQ_result_spec_timestamp ("execution_time", - &execution_time), - NULL), - GNUNET_PQ_result_spec_bool ("verified", - &verified), - GNUNET_PQ_result_spec_bool ("confirmed", - &confirmed), + GNUNET_PQ_result_spec_absolute_time ("execution_time", + &execution_time), + GNUNET_PQ_result_spec_bool ("expected", + &expected), GNUNET_PQ_result_spec_end }; @@ -117,8 +112,7 @@ lookup_transfers_cb (void *cls, exchange_url, transfer_serial_id, execution_time, - verified, - confirmed); + expected); GNUNET_PQ_cleanup_result (rs); } ltc->qs = num_results; @@ -133,7 +127,7 @@ TMH_PG_lookup_transfers (void *cls, struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll verified, + enum TALER_EXCHANGE_YesNoAll expected, TALER_MERCHANTDB_TransferCallback cb, void *cb_cls) { @@ -156,8 +150,9 @@ TMH_PG_lookup_transfers (void *cls, ? GNUNET_PQ_query_param_null () /* NULL: do not filter by payto URI */ : GNUNET_PQ_query_param_string (payto_uri.full_payto), GNUNET_PQ_query_param_bool (! by_time), /* $7: filter by time? */ - GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == verified), /* filter by verified? */ - GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == verified), + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_ALL == expected), /* filter by expected? */ + GNUNET_PQ_query_param_bool (TALER_EXCHANGE_YNA_YES == expected), + GNUNET_PQ_query_param_end }; enum GNUNET_DB_QueryStatus qs; @@ -167,65 +162,57 @@ TMH_PG_lookup_transfers (void *cls, "lookup_transfers_asc", "SELECT" " mt.credit_amount" - ",wtid" + ",mt.wtid" ",mac.payto_uri" - ",exchange_url" - ",credit_serial" - ",mts.execution_time" - ",verified" - ",confirmed" + ",mt.exchange_url" + ",mt.credit_serial" + ",mt.execution_time" + ",mt.expected" " FROM merchant_transfers mt" " JOIN merchant_accounts mac" " USING (account_serial)" - " LEFT JOIN merchant_transfer_signatures mts" - " USING (credit_serial)" " WHERE ( $7 OR " - " (mts.execution_time IS NOT NULL AND" - " mts.execution_time < $2 AND" - " mts.execution_time >= $3) )" - " AND ( $8 OR " - " (verified = $9) )" + " (mt.execution_time < $2 AND" + " mt.execution_time >= $3) )" " AND ( (CAST($6 AS TEXT) IS NULL) OR " " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" " =REGEXP_REPLACE($6,'\\?.*','')) )" + " AND ( $8 OR " + " (mt.expected = $9) )" " AND merchant_serial =" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)" - " AND (credit_serial > $4)" - " ORDER BY credit_serial ASC" + " AND (mt.credit_serial > $4)" + " ORDER BY mt.credit_serial ASC" " LIMIT $5"); PREPARE (pg, "lookup_transfers_desc", "SELECT" " mt.credit_amount" - ",wtid" + ",mt.wtid" ",mac.payto_uri" - ",exchange_url" - ",credit_serial" - ",mts.execution_time" - ",verified" - ",confirmed" + ",mt.exchange_url" + ",mt.credit_serial" + ",mt.execution_time" + ",mt.expected" " FROM merchant_transfers mt" " JOIN merchant_accounts mac" " USING (account_serial)" - " LEFT JOIN merchant_transfer_signatures mts" - " USING (credit_serial)" " WHERE ( $7 OR " - " (mts.execution_time IS NOT NULL AND" - " mts.execution_time < $2 AND" - " mts.execution_time >= $3) )" - " AND ( $8 OR " - " (verified = $9) )" + " (mt.execution_time < $2 AND" + " mt.execution_time >= $3) )" " AND ( (CAST($6 AS TEXT) IS NULL) OR " " (REGEXP_REPLACE(mac.payto_uri,'\\?.*','')" " =REGEXP_REPLACE($6,'\\?.*','')) )" + " AND ( $8 OR " + " (mt.expected = $9) )" " AND merchant_serial =" " (SELECT merchant_serial" " FROM merchant_instances" " WHERE merchant_id=$1)" - " AND (credit_serial < $4)" - " ORDER BY credit_serial DESC" + " AND (mt.credit_serial < $4)" + " ORDER BY mt.credit_serial DESC" " LIMIT $5"); qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, diff --git a/src/backenddb/pg_lookup_transfers.h b/src/backenddb/pg_lookup_transfers.h @@ -40,7 +40,7 @@ * @param limit number of entries to return, negative for descending in execution time, * positive for ascending in execution time * @param offset transfer_serial number of the transfer we want to offset from - * @param verified filter transfers by verification status + * @param expected filter for transfers that were expected * @param cb function to call with detailed transfer data * @param cb_cls closure for @a cb * @return transaction status @@ -53,7 +53,7 @@ TMH_PG_lookup_transfers (void *cls, struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll verified, + enum TALER_EXCHANGE_YesNoAll expected, TALER_MERCHANTDB_TransferCallback cb, void *cb_cls); diff --git a/src/backenddb/pg_select_open_transfers.c b/src/backenddb/pg_select_open_transfers.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -75,10 +75,9 @@ open_transfers_cb (void *cls, char *exchange_url; struct TALER_FullPayto payto_uri; struct TALER_WireTransferIdentifierRawP wtid; - struct TALER_Amount total; - struct GNUNET_TIME_Absolute next_attempt; + struct GNUNET_TIME_Absolute retry_time; struct GNUNET_PQ_ResultSpec rs[] = { - GNUNET_PQ_result_spec_uint64 ("credit_serial", + GNUNET_PQ_result_spec_uint64 ("expected_credit_serial", &rowid), GNUNET_PQ_result_spec_string ("instance_id", &instance_id), @@ -88,10 +87,8 @@ open_transfers_cb (void *cls, &payto_uri.full_payto), GNUNET_PQ_result_spec_auto_from_type ("wtid", &wtid), - TALER_PQ_result_spec_amount_with_currency ("credit_amount", - &total), - GNUNET_PQ_result_spec_absolute_time ("next_attempt", - &next_attempt), + GNUNET_PQ_result_spec_absolute_time ("retry_time", + &retry_time), GNUNET_PQ_result_spec_end }; @@ -110,8 +107,7 @@ open_transfers_cb (void *cls, exchange_url, payto_uri, &wtid, - &total, - next_attempt); + retry_time); GNUNET_PQ_cleanup_result (rs); } } @@ -138,23 +134,20 @@ TMH_PG_select_open_transfers (void *cls, PREPARE (pg, "select_open_transfers", "SELECT" - " credit_serial" - ",merchant_id AS instance_id" - ",exchange_url" - ",payto_uri" - ",wtid" - ",credit_amount" - ",ready_time AS next_attempt" - " FROM merchant_transfers" - " JOIN merchant_accounts" + " met.expected_credit_serial" + ",mi.merchant_id AS instance_id" + ",met.exchange_url" + ",ma.payto_uri" + ",met.wtid" + ",met.retry_time" + " FROM merchant_expected_transfers met" + " JOIN merchant_accounts ma" " USING (account_serial)" - " JOIN merchant_instances" - " USING (merchant_serial)" - " WHERE confirmed AND" - " NOT (failed OR verified)" - " ORDER BY ready_time ASC" + " JOIN merchant_instances mi" + " ON (ma.merchant_serial=mi.merchant_serial)" + " WHERE retry_needed" + " ORDER BY retry_time ASC" " LIMIT $1;"); - qs = GNUNET_PQ_eval_prepared_multi_select ( pg->conn, "select_open_transfers", diff --git a/src/backenddb/pg_set_transfer_status_to_confirmed.c b/src/backenddb/pg_set_transfer_status_to_confirmed.c @@ -1,66 +0,0 @@ -/* - This file is part of TALER - Copyright (C) 2023 Taler Systems SA - - TALER is free software; you can redistribute it and/or modify it under the - terms of the GNU 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 General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file backenddb/pg_set_transfer_status_to_confirmed.c - * @brief Implementation of the set_transfer_status_to_confirmed function for Postgres - * @author Christian Grothoff - */ -#include "platform.h" -#include <taler/taler_error_codes.h> -#include <taler/taler_dbevents.h> -#include <taler/taler_pq_lib.h> -#include "pg_set_transfer_status_to_confirmed.h" -#include "pg_helper.h" - - -enum GNUNET_DB_QueryStatus -TMH_PG_set_transfer_status_to_confirmed ( - void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *amount) -{ - struct PostgresClosure *pg = cls; - struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (instance_id), - GNUNET_PQ_query_param_auto_from_type (wtid), - GNUNET_PQ_query_param_string (exchange_url), - TALER_PQ_query_param_amount_with_currency (pg->conn, - amount), - GNUNET_PQ_query_param_end - }; - - check_connection (pg); - PREPARE (pg, - "set_transfer_status_to_confirmed", - "UPDATE merchant_transfers SET" - " confirmed=TRUE" - " WHERE wtid=$2" - " AND credit_amount=cast($4 AS taler_amount_currency)" - " AND exchange_url=$3" - " AND account_serial IN" - " (SELECT account_serial" - " FROM merchant_accounts" - " WHERE merchant_serial =" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1));"); - return GNUNET_PQ_eval_prepared_non_select ( - pg->conn, - "set_transfer_status_to_confirmed", - params); -} diff --git a/src/backenddb/pg_set_transfer_status_to_confirmed.h b/src/backenddb/pg_set_transfer_status_to_confirmed.h @@ -1,48 +0,0 @@ -/* - 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 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 General Public License for more details. - - You should have received a copy of the GNU General Public License along with - TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> - */ -/** - * @file backenddb/pg_set_transfer_status_to_confirmed.h - * @brief implementation of the set_transfer_status_to_confirmed function for Postgres - * @author Christian Grothoff - */ -#ifndef PG_SET_TRANSFER_STATUS_TO_CONFIRMED_H -#define PG_SET_TRANSFER_STATUS_TO_CONFIRMED_H - -#include <taler/taler_util.h> -#include <taler/taler_json_lib.h> -#include "taler_merchantdb_plugin.h" - - -/** - * Set transfer status to confirmed. - * - * @param cls closure - * @param instance_id merchant instance with the update - * @param exchange_url the exchange that made the transfer - * @param wtid wire transfer subject - * @param amount confirmed amount of the wire transfer - * @return transaction status - */ -enum GNUNET_DB_QueryStatus -TMH_PG_set_transfer_status_to_confirmed ( - void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *amount); - - -#endif diff --git a/src/backenddb/pg_update_deposit_confirmation_status.c b/src/backenddb/pg_update_deposit_confirmation_status.c @@ -30,35 +30,36 @@ enum GNUNET_DB_QueryStatus TMH_PG_update_deposit_confirmation_status ( void *cls, uint64_t deposit_serial, - bool wire_pending, - struct GNUNET_TIME_Timestamp future_retry, - struct GNUNET_TIME_Relative retry_backoff, - const char *emsg) + bool retry_needed, + struct GNUNET_TIME_Timestamp retry_time, + uint32_t last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_detail) { struct PostgresClosure *pg = cls; + uint32_t ec32 = (uint32_t) last_ec; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_uint64 (&deposit_serial), - GNUNET_PQ_query_param_timestamp (&future_retry), - NULL == emsg + GNUNET_PQ_query_param_bool (retry_needed), + GNUNET_PQ_query_param_timestamp (&retry_time), + GNUNET_PQ_query_param_uint32 (&last_http_status), + GNUNET_PQ_query_param_uint32 (&ec32), + NULL == last_detail ? GNUNET_PQ_query_param_null () - : GNUNET_PQ_query_param_string (emsg), - GNUNET_PQ_query_param_relative_time (&retry_backoff), - GNUNET_PQ_query_param_bool (wire_pending), + : GNUNET_PQ_query_param_string (last_detail), GNUNET_PQ_query_param_end }; check_connection (pg); PREPARE (pg, "update_deposit_confirmation_status", - "UPDATE merchant_deposit_confirmations SET" - " wire_transfer_deadline=$2" - ",exchange_failure=$3" - ",retry_backoff=$4" - ",wire_pending=$5 AND wire_pending" - " WHERE deposit_confirmation_serial=" - " (SELECT deposit_confirmation_serial" - " FROM merchant_deposits" - " WHERE deposit_serial=$1);"); + "UPDATE merchant_deposits SET" + " settlement_retry_needed=$2" + ",settlement_retry_time=$3" + ",settlement_last_http_status=$4" + ",settlement_last_ec=$5" + ",settlement_last_detail=$6" + " WHERE deposit_serial=$1;"); return GNUNET_PQ_eval_prepared_non_select ( pg->conn, "update_deposit_confirmation_status", diff --git a/src/backenddb/pg_update_deposit_confirmation_status.h b/src/backenddb/pg_update_deposit_confirmation_status.h @@ -32,20 +32,22 @@ * * @param cls closure * @param deposit_serial deposit to update status for - * @param wire_pending did the exchange say that the wire is still pending? - * @param future_retry when should we ask the exchange again - * @param retry_backoff current value for the retry backoff - * @param emsg error message to record + * @param retry_needed true if the HTTP request should be retried + * @param retry_time when should we ask the exchange again + * @param last_http_status HTTP status code of the last reply + * @param last_ec Taler error code of the last reply + * @param last_hint hint from error message to record, possibly NULL * @return database result code */ enum GNUNET_DB_QueryStatus TMH_PG_update_deposit_confirmation_status ( void *cls, uint64_t deposit_serial, - bool wire_pending, - struct GNUNET_TIME_Timestamp future_retry, - struct GNUNET_TIME_Relative retry_backoff, - const char *emsg); + bool retry_needed, + struct GNUNET_TIME_Timestamp retry_time, + uint32_t last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_hint); #endif diff --git a/src/backenddb/pg_update_product.c b/src/backenddb/pg_update_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022, 2024 Taler Systems SA + Copyright (C) 2022, 2024, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -60,6 +60,7 @@ TMH_PG_update_product (void *cls, GNUNET_PQ_query_param_array_uint64 (num_cats, cats, pg->conn), + GNUNET_PQ_query_param_string (pd->product_name), GNUNET_PQ_query_param_end }; uint64_t ncat; @@ -101,7 +102,7 @@ TMH_PG_update_product (void *cls, ",out_no_cat AS no_cat" ",out_no_instance AS no_instance" " FROM merchant_do_update_product" - "($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14);"); + "($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15);"); qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, "update_product", params, diff --git a/src/backenddb/pg_update_product.sql b/src/backenddb/pg_update_product.sql @@ -1,6 +1,6 @@ -- -- This file is part of TALER --- Copyright (C) 2024 Taler Systems SA +-- Copyright (C) 2024, 2025 Taler Systems SA -- -- TALER is free software; you can redistribute it and/or modify it under the -- terms of the GNU General Public License as published by the Free Software @@ -15,7 +15,8 @@ -- -CREATE OR REPLACE FUNCTION merchant_do_update_product ( +DROP FUNCTION IF EXISTS merchant_do_update_product; +CREATE FUNCTION merchant_do_update_product ( IN in_instance_id TEXT, IN in_product_id TEXT, IN in_description TEXT, @@ -30,6 +31,7 @@ CREATE OR REPLACE FUNCTION merchant_do_update_product ( IN in_next_restock INT8, IN in_minimum_age INT4, IN ina_categories INT8[], + IN in_product_name TEXT, OUT out_no_instance BOOL, OUT out_no_product BOOL, OUT out_lost_reduced BOOL, @@ -121,6 +123,7 @@ END LOOP; UPDATE merchant_inventory SET description=in_description ,description_i18n=in_description_i18n + ,product_name=in_product_name ,unit=in_unit ,image=in_image ,taxes=in_taxes diff --git a/src/backenddb/pg_update_transfer_status.c b/src/backenddb/pg_update_transfer_status.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2022 Taler Systems SA + Copyright (C) 2022, 2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software @@ -32,18 +32,23 @@ TMH_PG_update_transfer_status ( const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, struct GNUNET_TIME_Absolute next_attempt, + unsigned int http_status, enum TALER_ErrorCode ec, - bool failed, - bool verified) + const char *detail, + bool needs_retry) { struct PostgresClosure *pg = cls; + uint32_t hs32 = (uint32_t) http_status; uint32_t ec32 = (uint32_t) ec; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (wtid), GNUNET_PQ_query_param_string (exchange_url), + GNUNET_PQ_query_param_uint32 (&hs32), GNUNET_PQ_query_param_uint32 (&ec32), - GNUNET_PQ_query_param_bool (failed), - GNUNET_PQ_query_param_bool (verified), + NULL == detail + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (detail), + GNUNET_PQ_query_param_bool (needs_retry), GNUNET_PQ_query_param_absolute_time (&next_attempt), GNUNET_PQ_query_param_end }; @@ -51,11 +56,12 @@ TMH_PG_update_transfer_status ( check_connection (pg); PREPARE (pg, "update_transfer_status", - "UPDATE merchant_transfers SET" - " validation_status=$3" - ",failed=$4" - ",verified=$5" - ",ready_time=$6" + "UPDATE merchant_expected_transfers SET" + " last_http_status=$3" + ",last_ec=$4" + ",last_detail=$5" + ",retry_needed=$6" + ",retry_time=$7" " WHERE wtid=$1" " AND exchange_url=$2"); return GNUNET_PQ_eval_prepared_non_select ( diff --git a/src/backenddb/pg_update_transfer_status.h b/src/backenddb/pg_update_transfer_status.h @@ -33,9 +33,10 @@ * @param exchange_url the exchange that made the transfer * @param wtid wire transfer subject * @param next_attempt when should we try again (if ever) + * @param http_status last HTTP status code from the server, 0 for timeout * @param ec current error state of checking the transfer - * @param failed true if validation has failed for good - * @param verified true if validation has succeeded for good + * @param hint last hint from the server, possibly NULL + * @param needs_retry true if we should retry the request * @return database transaction status */ enum GNUNET_DB_QueryStatus @@ -44,8 +45,9 @@ TMH_PG_update_transfer_status ( const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, struct GNUNET_TIME_Absolute next_attempt, + unsigned int http_status, enum TALER_ErrorCode ec, - bool failed, - bool verified); + const char *hint, + bool needs_retry); #endif diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014--2024 Taler Systems SA + (C) 2014--2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -43,6 +43,7 @@ #include "pg_insert_login_token.h" #include "pg_delete_login_token.h" #include "pg_select_login_token.h" +#include "pg_lookup_login_tokens.h" #include "pg_insert_account.h" #include "pg_update_account.h" #include "pg_lookup_instances.h" @@ -62,6 +63,7 @@ #include "pg_lookup_instance_auth.h" #include "pg_lookup_otp_devices.h" #include "pg_update_transfer_status.h" +#include "pg_finalize_transfer_status.h" #include "pg_insert_instance.h" #include "pg_account_kyc_set_status.h" #include "pg_account_kyc_get_status.h" @@ -120,13 +122,13 @@ #include "pg_delete_transfer.h" #include "pg_check_transfer_exists.h" #include "pg_lookup_account.h" -#include "pg_lookup_wire_fee.h" +#include "pg_lookup_expected_transfers.h" #include "pg_lookup_deposits_by_contract_and_coin.h" -#include "pg_lookup_transfer.h" #include "pg_lookup_transfer_summary.h" #include "pg_lookup_transfer_details.h" #include "pg_lookup_webhooks.h" #include "pg_lookup_webhook.h" +#include "pg_lookup_wire_fee.h" #include "pg_delete_webhook.h" #include "pg_insert_webhook.h" #include "pg_update_webhook.h" @@ -136,7 +138,6 @@ #include "pg_update_pending_webhook.h" #include "pg_lookup_pending_webhooks.h" #include "pg_update_deposit_confirmation_status.h" -#include "pg_set_transfer_status_to_confirmed.h" #include "pg_insert_exchange_keys.h" #include "pg_select_exchange_keys.h" #include "pg_insert_deposit_to_transfer.h" @@ -397,8 +398,12 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_insert_login_token; plugin->delete_login_token = &TMH_PG_delete_login_token; + plugin->delete_login_token_serial + = &TMH_PG_delete_login_token_serial; plugin->select_login_token = &TMH_PG_select_login_token; + plugin->lookup_login_tokens + = &TMH_PG_lookup_login_tokens; plugin->select_account_by_uri = &TMH_PG_select_account_by_uri; plugin->lookup_instance_auth @@ -439,6 +444,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_inactivate_account; plugin->update_transfer_status = &TMH_PG_update_transfer_status; + plugin->finalize_transfer_status + = &TMH_PG_finalize_transfer_status; plugin->lookup_products = &TMH_PG_lookup_products; plugin->lookup_all_products @@ -537,10 +544,6 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_lookup_wire_fee; plugin->lookup_deposits_by_contract_and_coin = &TMH_PG_lookup_deposits_by_contract_and_coin; - plugin->lookup_transfer - = &TMH_PG_lookup_transfer; - plugin->set_transfer_status_to_confirmed - = &TMH_PG_set_transfer_status_to_confirmed; plugin->lookup_transfer_summary = &TMH_PG_lookup_transfer_summary; plugin->lookup_transfer_details @@ -627,6 +630,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) = &TMH_PG_insert_exchange_account; plugin->insert_token_family = &TMH_PG_insert_token_family; + plugin->lookup_expected_transfers + = &TMH_PG_lookup_expected_transfers; plugin->lookup_token_family = &TMH_PG_lookup_token_family; plugin->lookup_token_families diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -716,6 +716,7 @@ make_product (const char *id, struct ProductData *product) { product->id = id; + product->product.product_name = "Test product"; product->product.description = "This is a test product"; product->product.description_i18n = json_array (); GNUNET_assert (NULL != product->product.description_i18n); @@ -1382,8 +1383,10 @@ static int test_products (void) { struct TestProducts_Closure test_cls; + int test_result; + pre_test_products (&test_cls); - int test_result = run_test_products (&test_cls); + test_result = run_test_products (&test_cls); post_test_products (&test_cls); return test_result; } @@ -1601,9 +1604,11 @@ lookup_orders_cb (void *cls, struct GNUNET_TIME_Timestamp timestamp) { struct TestLookupOrders_Closure *cmp = cls; + unsigned int i; + if (NULL == cmp) return; - unsigned int i = cmp->results_length; + i = cmp->results_length; cmp->results_length += 1; if (cmp->orders_to_cmp_length > i) { @@ -2434,8 +2439,10 @@ static int test_orders (void) { struct TestOrders_Closure test_cls; + int test_result; + pre_test_orders (&test_cls); - int test_result = run_test_orders (&test_cls); + test_result = run_test_orders (&test_cls); post_test_orders (&test_cls); return test_result; } @@ -2709,7 +2716,8 @@ test_insert_deposit (const struct InstanceData *instance, &deposit->coin_sig, &deposit->amount_with_fee, &deposit->deposit_fee, - &deposit->refund_fee), + &deposit->refund_fee, + GNUNET_TIME_absolute_get ()), "Insert deposit failed\n"); return 0; } @@ -2879,7 +2887,7 @@ lookup_deposits_contract_coin_cb ( if (NULL == cmp) return; - cmp->results_length += 1; + cmp->results_length++; for (unsigned int i = 0; cmp->deposits_to_cmp_length > i; ++i) { if ((GNUNET_TIME_timestamp_cmp (cmp->deposits_to_cmp[i].timestamp, @@ -2912,7 +2920,7 @@ lookup_deposits_contract_coin_cb ( (0 == GNUNET_memcmp (&cmp->deposits_to_cmp[i].exchange_sig, exchange_sig))) { - cmp->results_matching[i] += 1; + cmp->results_matching[i]++; } } } @@ -3368,8 +3376,10 @@ static int test_deposits (void) { struct TestDeposits_Closure test_cls; + int test_result; + pre_test_deposits (&test_cls); - int test_result = run_test_deposits (&test_cls); + test_result = run_test_deposits (&test_cls); post_test_deposits (&test_cls); return test_result; } @@ -3542,69 +3552,6 @@ make_transfer (const struct ExchangeSignkeyData *signkey, /** - * Tests looking up a transfer from the database. - * - * @param exchange_url url to the exchange of the transfer. - * @param wtid id of the transfer. - * @param total_expected the total amount of the transfer. - * @param fee_expected the fee on the transfer. - * @param time_expected when the transfer was made. - * @param verified_expected whether the transfer was verified. - * @return 1 on success, 0 otherwise. - */ -static int -test_lookup_transfer ( - const struct InstanceData *instance, - const struct TransferData *transfer) -{ - struct TALER_Amount total_with_fee; - struct TALER_Amount total; - struct TALER_Amount fee; - struct TALER_Amount exchange_amount; - struct GNUNET_TIME_Timestamp time; - bool esig; - bool verified; - - if (1 != plugin->lookup_transfer (plugin->cls, - instance->instance.id, - transfer->exchange_url, - &transfer->wtid, - &total, - &fee, - &exchange_amount, - &time, - &esig, - &verified)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup transfer failed\n"); - return 1; - } - GNUNET_assert (0 <= TALER_amount_add (&total_with_fee, - &transfer->data.total_amount, - &transfer->data.wire_fee)); - if ((GNUNET_OK != TALER_amount_cmp_currency (&total_with_fee, - &total)) || - (0 != TALER_amount_cmp (&total_with_fee, - &total)) || - (GNUNET_OK != TALER_amount_cmp_currency (&transfer->data.wire_fee, - &fee)) || - (0 != TALER_amount_cmp (&transfer->data.wire_fee, - &fee)) || - (GNUNET_TIME_timestamp_cmp (transfer->data.execution_time, - !=, - time)) || - (transfer->verified != verified)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Lookup transfer failed: mismatched data\n"); - return 1; - } - return 0; -} - - -/** * Closure for testing 'lookup_transfer_summary' */ struct TestLookupTransferSummary_Closure @@ -3866,7 +3813,8 @@ struct TestLookupTransferDetailsByOrder_Closure * @param execution_time when the transfer found occurred. * @param deposit_value amount of the deposit for the transfer found. * @param deposit_fee amount of the fee for the deposit of the transfer. - * @param transfer_confirmed whether the transfer was confirmed. + * @param transfer_confirmed did the merchant confirm that a wire transfer with + * @a wtid over the total amount happened? */ static void lookup_transfer_details_order_cb ( @@ -3879,13 +3827,14 @@ lookup_transfer_details_order_cb ( bool transfer_confirmed) { struct TestLookupTransferDetailsByOrder_Closure *cmp = cls; + if (NULL == cmp) return; cmp->results_length += 1; for (unsigned int i = 0; i < cmp->transfers_to_cmp_length; ++i) { /* Right now lookup_transfer_details_by_order leaves execution_time - uninitialized and transfer_confirmed always false. */ + uninitialized */ if ((0 == GNUNET_memcmp (&cmp->transfers_to_cmp[i].wtid, wtid)) && (0 == strcmp (cmp->transfers_to_cmp[i].exchange_url, @@ -3903,8 +3852,7 @@ lookup_transfer_details_order_cb ( deposit_fee)) && (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].deposit_fee, - deposit_fee)) /* && - (cmp->transfers_to_cmp[i].confirmed == transfer_confirmed)*/) + deposit_fee)) ) cmp->results_matching[i] += 1; } } @@ -4077,8 +4025,6 @@ struct TestLookupTransfers_Closure * @param transfer_serial_id serial number identifying the transfer in the backend * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_TS * if it did not yet happen - * @param verified true if we checked the exchange's answer and liked it, - * false there is a problem (verification failed or did not yet happen) * @param confirmed true if the merchant confirmed this wire transfer * false if it is so far only claimed to have been made by the exchange */ @@ -4089,8 +4035,7 @@ lookup_transfers_cb (void *cls, struct TALER_FullPayto payto_uri, const char *exchange_url, uint64_t transfer_serial_id, - struct GNUNET_TIME_Timestamp execution_time, - bool verified, + struct GNUNET_TIME_Absolute execution_time, bool confirmed) { struct TestLookupTransfers_Closure *cmp = cls; @@ -4098,21 +4043,17 @@ lookup_transfers_cb (void *cls, return; for (unsigned int i = 0; cmp->transfers_to_cmp_length > i; ++i) { - if ((GNUNET_OK == - TALER_amount_cmp_currency ( - &cmp->transfers_to_cmp[i].data.total_amount, - credit_amount)) && - (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].data.total_amount, - credit_amount)) && - (GNUNET_TIME_timestamp_cmp ( - cmp->transfers_to_cmp[i].data.execution_time, - ==, - execution_time))) + if ( (GNUNET_OK == + TALER_amount_cmp_currency ( + &cmp->transfers_to_cmp[i].data.total_amount, + credit_amount)) && + (0 == TALER_amount_cmp (&cmp->transfers_to_cmp[i].data.total_amount, + credit_amount)) ) { - cmp->results_matching[i] += 1; + cmp->results_matching[i]++; } } - cmp->results_length += 1; + cmp->results_length++; } @@ -4137,7 +4078,6 @@ test_lookup_transfers (const struct InstanceData *instance, struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll filter_verified, unsigned int transfers_length, const struct TransferData *transfers) { @@ -4158,7 +4098,7 @@ test_lookup_transfers (const struct InstanceData *instance, after, limit, offset, - filter_verified, + TALER_EXCHANGE_YNA_ALL, &lookup_transfers_cb, &cmp)) { @@ -4243,17 +4183,15 @@ test_insert_deposit_to_transfer (const struct InstanceData *instance, uint64_t deposit_serial = get_deposit_serial (instance, order, deposit); - bool cleared; TEST_COND_RET_ON_FAIL (expected_result == - plugin->insert_deposit_to_transfer (plugin->cls, - deposit_serial, - &deposit_data, - &cleared), + plugin->insert_deposit_to_transfer ( + plugin->cls, + deposit_serial, + &deposit->h_wire, + deposit->exchange_url, + &deposit_data), "insert deposit to transfer failed\n"); - TEST_COND_RET_ON_FAIL (expect_cleared == - cleared, - "cleared status wrong"); return 0; } @@ -4489,21 +4427,7 @@ run_test_transfers (struct TestTransfers_Closure *cls) cls->order.id, NULL, false, - true)); - TEST_RET_ON_FAIL (test_lookup_transfer (&cls->instance, - &cls->transfers[0])); - TEST_COND_RET_ON_FAIL ( - GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == - plugin->set_transfer_status_to_confirmed ( - plugin->cls, - cls->instance.instance.id, - cls->deposit.exchange_url, - &cls->transfers[0].wtid, - &cls->deposit.amount_with_fee), - "Set transfer status to confirmed failed\n"); - cls->transfers[0].confirmed = true; - TEST_RET_ON_FAIL (test_lookup_transfer (&cls->instance, - &cls->transfers[0])); + false)); TEST_RET_ON_FAIL (test_insert_deposit_to_transfer (&cls->instance, &cls->signkey, &cls->order, @@ -4531,7 +4455,6 @@ run_test_transfers (struct TestTransfers_Closure *cls) GNUNET_TIME_UNIT_ZERO_TS, 8, 0, - TALER_EXCHANGE_YNA_ALL, 1, &cls->transfers[0])); return 0; @@ -4547,9 +4470,10 @@ static int test_transfers (void) { struct TestTransfers_Closure test_cls; + int test_result; pre_test_transfers (&test_cls); - int test_result = run_test_transfers (&test_cls); + test_result = run_test_transfers (&test_cls); post_test_transfers (&test_cls); return test_result; } @@ -5316,8 +5240,10 @@ static int test_refunds (void) { struct TestRefunds_Closure test_cls; + int test_result; + pre_test_refunds (&test_cls); - int test_result = run_test_refunds (&test_cls); + test_result = run_test_refunds (&test_cls); post_test_refunds (&test_cls); return test_result; } @@ -5880,6 +5806,8 @@ static int test_lookup_template (const struct InstanceData *instance, const struct TemplateData *template) { + const struct TALER_MERCHANTDB_TemplateDetails *to_cmp + = &template->template; struct TALER_MERCHANTDB_TemplateDetails lookup_result; if (0 > plugin->lookup_template (plugin->cls, @@ -5892,7 +5820,6 @@ test_lookup_template (const struct InstanceData *instance, TALER_MERCHANTDB_template_details_free (&lookup_result); return 1; } - const struct TALER_MERCHANTDB_TemplateDetails *to_cmp = &template->template; if (0 != check_templates_equal (&lookup_result, to_cmp)) { @@ -6325,7 +6252,9 @@ static int test_lookup_webhook (const struct InstanceData *instance, const struct WebhookData *webhook) { + const struct TALER_MERCHANTDB_WebhookDetails *to_cmp = &webhook->webhook; struct TALER_MERCHANTDB_WebhookDetails lookup_result; + if (0 > plugin->lookup_webhook (plugin->cls, instance->instance.id, webhook->id, @@ -6336,7 +6265,6 @@ test_lookup_webhook (const struct InstanceData *instance, TALER_MERCHANTDB_webhook_details_free (&lookup_result); return 1; } - const struct TALER_MERCHANTDB_WebhookDetails *to_cmp = &webhook->webhook; if (0 != check_webhooks_equal (&lookup_result, to_cmp)) { @@ -6726,8 +6654,10 @@ static int test_webhooks (void) { struct TestWebhooks_Closure test_cls; + int test_result; + pre_test_webhooks (&test_cls); - int test_result = run_test_webhooks (&test_cls); + test_result = run_test_webhooks (&test_cls); post_test_webhooks (&test_cls); return test_result; } @@ -7220,6 +7150,9 @@ post_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) static int run_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) { + uint64_t webhook_pending_serial0; + uint64_t webhook_pending_serial1; + /* Test that insert without an instance fails */ TEST_RET_ON_FAIL (test_insert_pending_webhook (&cls->instance, &cls->pwebhooks[0], @@ -7255,10 +7188,10 @@ run_test_pending_webhooks (struct TestPendingWebhooks_Closure *cls) 2, cls->pwebhooks)); - uint64_t webhook_pending_serial0 = get_pending_serial (&cls->instance, - &cls->pwebhooks[0]); - uint64_t webhook_pending_serial1 = get_pending_serial (&cls->instance, - &cls->pwebhooks[1]); + webhook_pending_serial0 = get_pending_serial (&cls->instance, + &cls->pwebhooks[0]); + webhook_pending_serial1 = get_pending_serial (&cls->instance, + &cls->pwebhooks[1]); /* Test webhook deletion */ TEST_RET_ON_FAIL (test_delete_pending_webhook (webhook_pending_serial1, @@ -7284,8 +7217,10 @@ static int test_pending_webhooks (void) { struct TestPendingWebhooks_Closure test_cls; + int test_result; + pre_test_pending_webhooks (&test_cls); - int test_result = run_test_pending_webhooks (&test_cls); + test_result = run_test_pending_webhooks (&test_cls); post_test_pending_webhooks (&test_cls); return test_result; } diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2024 Taler Systems SA + Copyright (C) 2014-2025 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 @@ -1644,6 +1644,11 @@ struct TALER_MERCHANT_ProductGetResponse * #GNUNET_TIME_UNIT_FOREVER_ABS for 'never'. */ struct GNUNET_TIME_Timestamp next_restock; + + /** + * name of the product + */ + const char *product_name; } ok; } details; @@ -4390,22 +4395,15 @@ struct TALER_MERCHANT_TransferData uint64_t credit_serial; /** - * Time of the wire transfer, according to the exchange. - * 0 for not provided by the exchange. + * Time of the wire transfer, based on when we received + * a confirmation for the wire transfer. */ struct GNUNET_TIME_Timestamp execution_time; /** - * Did we check the exchange's answer and are happy about it? False if we - * did not check or are unhappy with the answer. - */ - bool verified; - - /** - * Did we confirm the wire transfer happened (via - * #TALER_MERCHANT_transfers_post())? + * True if this wire transfer was expected. */ - bool confirmed; + bool expected; }; @@ -4461,16 +4459,7 @@ typedef void /** * Request backend to return list of all wire transfers that - * we received (or that the exchange claims we should have received). - * - * Note that when filtering by timestamp (using “before” and/or “after”), we - * use the time reported by the exchange and thus will ONLY return results for - * which we already have a response from the exchange. This should be - * virtually all transfers, however it is conceivable that for some transfer - * the exchange responded with a temporary error (i.e. HTTP status 500+) and - * then we do not yet have an execution time to filter by. Thus, IF timestamp - * filters are given, transfers for which we have no response from the - * exchange yet are automatically excluded. + * we received. * * @param ctx execution context * @param backend_url base URL of the backend @@ -4481,7 +4470,7 @@ typedef void * #GNUNET_TIME_UNIT_ZERO_ABS to not filter by @a after * @param limit return at most this number of results; negative to descend in execution time * @param offset start at this "credit serial" number (exclusive) - * @param verified filter results by verification status + * @param expected filter results by expectation status * @param cb the callback to call when a reply for this request is available * @param cb_cls closure for @a cb * @return a handle for this request @@ -4495,7 +4484,7 @@ TALER_MERCHANT_transfers_get ( const struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll verified, + enum TALER_EXCHANGE_YesNoAll expected, TALER_MERCHANT_GetTransfersCallback cb, void *cb_cls); diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -296,6 +296,11 @@ typedef void struct TALER_MERCHANTDB_ProductDetails { /** + * Name of the product. + */ + char *product_name; + + /** * Description of the product. */ char *description; @@ -386,6 +391,26 @@ typedef void /** + * Typically called by `lookup_login_tokens`. + * + * @param cls a `json_t *` JSON array to build + * @param creation_time creation time of the token + * @param expiration_time expiration time of the token + * @param scope validity scope of the token + * @param description description of the token + * @param serial serial number of the token + */ +typedef void +(*TALER_MERCHANTDB_LoginTokensCallback)( + void *cls, + struct GNUNET_TIME_Timestamp creation_time, + struct GNUNET_TIME_Timestamp expiration_time, + uint32_t scope, + const char *description, + uint64_t serial); + + +/** * Typically called by `lookup_templates`. * * @param cls closure @@ -893,10 +918,9 @@ typedef void * @param cls closure * @param rowid row of the transfer in the merchant database * @param instance_id instance that received the transfer - * @param exchange_url base URL of the exchange that initiated the transfer + * @param exchange_url URL of the exchange that is making the deposit * @param payto_uri account of the merchant that received the transfer * @param wtid wire transfer subject identifying the aggregation - * @param total total amount that was wired * @param next_attempt when should we next try to interact with the exchange */ typedef void @@ -907,7 +931,6 @@ typedef void const char *exchange_url, struct TALER_FullPayto payto_uri, const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *total, struct GNUNET_TIME_Absolute next_attempt); @@ -917,11 +940,12 @@ typedef void * @param cls NULL * @param deposit_serial identifies the deposit operation * @param wire_deadline when is the wire due - * @param retry_backoff current value of the retry backoff + * @param retry_time when to next try the exchange again * @param h_contract_terms hash of the contract terms * @param merchant_priv private key of the merchant * @param instance_id name of the instance - * @param h_wire hash of the merchant's wire account into * @param amount_with_fee amount the exchange will deposit for this coin + * @param h_wire hash of the merchant's wire account into + * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin which the deposit was made * @param coin_pub public key of the deposited coin */ @@ -930,7 +954,7 @@ typedef void void *cls, uint64_t deposit_serial, struct GNUNET_TIME_Absolute wire_deadline, - struct GNUNET_TIME_Relative retry_backoff, + struct GNUNET_TIME_Absolute retry_time, const struct TALER_PrivateContractHashP *h_contract_terms, const struct TALER_MerchantPrivateKeyP *merchant_priv, const char *instance_id, @@ -980,32 +1004,62 @@ typedef void * Function called with information about a wire transfer. * * @param cls closure with a `json_t *` array to build up the response - * @param credit_amount how much was wired to the merchant (minus fees) + * @param expected_credit_amount how we expect to see wired to the merchant (minus fees), NULL if unknown * @param wtid wire transfer identifier * @param payto_uri target account that received the wire transfer * @param exchange_url base URL of the exchange that made the wire transfer - * @param transfer_serial_id serial number identifying the transfer in the backend + * @param expected_transfer_serial_id serial number identifying the expected transfer in the backend * @param execution_time when did the exchange make the transfer, #GNUNET_TIME_UNIT_FOREVER_ABS * if it did not yet happen - * @param verified true if we checked the exchange's answer and liked it, - * false there is a problem (verification failed or did not yet happen) * @param confirmed true if the merchant confirmed this wire transfer * false if it is so far only claimed to have been made by the exchange */ typedef void (*TALER_MERCHANTDB_TransferCallback)( void *cls, - const struct TALER_Amount *credit_amount, + const struct TALER_Amount *expected_credit_amount, const struct TALER_WireTransferIdentifierRawP *wtid, struct TALER_FullPayto payto_uri, const char *exchange_url, - uint64_t transfer_serial_id, - struct GNUNET_TIME_Timestamp execution_time, - bool verified, + uint64_t expected_transfer_serial_id, + struct GNUNET_TIME_Absolute execution_time, bool confirmed); /** + * Function called with information about expected incoming wire transfers. + * + * @param cls closure with a `json_t *` array to build up the response + * @param expected_credit_amount how we expect to see wired to the merchant (minus fees), NULL if unknown + * @param wtid wire transfer identifier + * @param payto_uri target account that received the wire transfer + * @param exchange_url base URL of the exchange that made the wire transfer + * @param expected_transfer_serial_id serial number identifying the expected transfer in the backend + * @param execution_time when did the exchange claim to have made the transfer + * @param confirmed true if the merchant confirmed this wire transfer + * false if it is so far only claimed to have been made by the exchange + * @param validated true if the reconciliation succeeded + * @param last_http_status HTTP status of our last request to the exchange for this transfer + * @param last_ec last error code we got back (otherwise #TALER_EC_NONE) + * @param last_error_detail last detail we got back (or NULL for none) + */ +typedef void +(*TALER_MERCHANTDB_IncomingCallback)( + void *cls, + const struct TALER_Amount *expected_credit_amount, + const struct TALER_WireTransferIdentifierRawP *wtid, + struct TALER_FullPayto payto_uri, + const char *exchange_url, + uint64_t expected_transfer_serial_id, + struct GNUNET_TIME_Timestamp execution_time, + bool confirmed, + bool validated, + unsigned int last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_error_detail); + + +/** * If the given account is feasible, add it to the array * of accounts we return. * @@ -1659,6 +1713,7 @@ struct TALER_MERCHANTDB_Plugin * @param creation_time the current time * @param expiration_time when does the token expire * @param validity_scope scope of the token + * @param description description of the token * @return database result code */ enum GNUNET_DB_QueryStatus @@ -1668,7 +1723,8 @@ struct TALER_MERCHANTDB_Plugin const struct TALER_MERCHANTDB_LoginTokenP *token, struct GNUNET_TIME_Timestamp creation_time, struct GNUNET_TIME_Timestamp expiration_time, - uint32_t validity_scope); + uint32_t validity_scope, + const char *description); /** @@ -1689,6 +1745,25 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Timestamp *expiration_time, uint32_t *validity_scope); + /** + * Lookup login tokens for instance. + * + * @param cls closure + * @param instance_id instance to lookup tokens for + * @param offset transfer_serial number of the transfer we want to offset from + * @param limit number of entries to return, negative for descending, + * positive for ascending + * @param cb function to call on all products found + * @param cb_cls closure for @a cb + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*lookup_login_tokens)(void *cls, + const char *instance_id, + uint64_t offset, + int64_t limit, + TALER_MERCHANTDB_LoginTokensCallback cb, + void *cb_cls); /** * Delete login token from database. @@ -1704,6 +1779,20 @@ struct TALER_MERCHANTDB_Plugin const char *id, const struct TALER_MERCHANTDB_LoginTokenP *token); + /** + * Delete login token from database by serial. + * + * @param cls closure + * @param id identifier of the instance + * @param serial serial of the token + * @return database result code + */ + enum GNUNET_DB_QueryStatus + (*delete_login_token_serial)( + void *cls, + const char *id, + uint64_t serial); + /** * Update information about an instance's account into our database. @@ -2541,6 +2630,8 @@ struct TALER_MERCHANTDB_Plugin * @param amount_with_fee amount the exchange will deposit for this coin * @param deposit_fee fee the exchange will charge for this coin * @param refund_fee fee the exchange will charge for refunds of coin + * @param check_time at what time should we check the deposit status + * with the exchange (for settlement) * @return transaction status */ enum GNUNET_DB_QueryStatus @@ -2552,7 +2643,8 @@ struct TALER_MERCHANTDB_Plugin const struct TALER_CoinSpendSignatureP *coin_sig, const struct TALER_Amount *amount_with_fee, const struct TALER_Amount *deposit_fee, - const struct TALER_Amount *refund_fee); + const struct TALER_Amount *refund_fee, + struct GNUNET_TIME_Absolute check_time); /** @@ -2712,9 +2804,10 @@ struct TALER_MERCHANTDB_Plugin * @param exchange_url the exchange that made the transfer * @param wtid wire transfer subject * @param next_attempt when should we try again (if ever) + * @param http_status last HTTP status code from the server, 0 for timeout * @param ec current error state of checking the transfer - * @param failed true if validation has failed for good - * @param verified true if validation has succeeded for good + * @param detail last error detail from the server, possibly NULL + * @param needs_retry true if we should retry the request * @return database transaction status */ enum GNUNET_DB_QueryStatus @@ -2723,9 +2816,36 @@ struct TALER_MERCHANTDB_Plugin const char *exchange_url, const struct TALER_WireTransferIdentifierRawP *wtid, struct GNUNET_TIME_Absolute next_attempt, + unsigned int http_status, enum TALER_ErrorCode ec, - bool failed, - bool verified); + const char *detail, + bool needs_retry); + + + /** + * Finalize transfer status with success. + * + * @param cls closure + * @param exchange_url the exchange that made the transfer + * @param wtid wire transfer subject + * @param h_details hash over all of the aggregated deposits + * @param total_amount total amount exchange claimed to have transferred + * @param wire_fee wire fee charged by the exchange + * @param exchange_pub key used to make @e exchange_sig + * @param exchange_sig signature of the exchange over reconciliation data + * @return database transaction status + */ + enum GNUNET_DB_QueryStatus + (*finalize_transfer_status)( + void *cls, + const char *exchange_url, + const struct TALER_WireTransferIdentifierRawP *wtid, + const struct GNUNET_HashCode *h_details, + const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, + const struct TALER_ExchangePublicKeyP *exchange_pub, + const struct TALER_ExchangeSignatureP *exchange_sig); + /** * Retrieve wire transfer details of wire details @@ -2751,15 +2871,17 @@ struct TALER_MERCHANTDB_Plugin * * @param cls closure * @param deposit_serial serial number of the deposit + * @param h_wire hash of the merchant's account that should receive the deposit + * @param exchange_url URL of the exchange that is making the deposit * @param dd deposit transfer data from the exchange to store - * @param[out] wpc set to true if the wire_pending flag was cleared * @return transaction status */ enum GNUNET_DB_QueryStatus (*insert_deposit_to_transfer)(void *cls, uint64_t deposit_serial, - const struct TALER_EXCHANGE_DepositData *dd, - bool *wpc); + const struct TALER_MerchantWireHashP *h_wire, + const char *exchange_url, + const struct TALER_EXCHANGE_DepositData *dd); /** @@ -2961,8 +3083,7 @@ struct TALER_MERCHANTDB_Plugin * @param wtid identifier of the wire transfer * @param credit_amount how much did we receive * @param payto_uri what is the merchant's bank account that received the transfer - * @param confirmed whether the transfer was confirmed by the merchant or - * was merely claimed by the exchange at this point + * @param bank_serial_id bank serial transfer ID, 0 for none (use NULL in DB!) * @return transaction status */ enum GNUNET_DB_QueryStatus @@ -2973,7 +3094,7 @@ struct TALER_MERCHANTDB_Plugin const struct TALER_WireTransferIdentifierRawP *wtid, const struct TALER_Amount *credit_amount, struct TALER_FullPayto payto_uri, - bool confirmed); + uint64_t bank_serial_id); /** @@ -3096,56 +3217,6 @@ struct TALER_MERCHANTDB_Plugin /** - * Lookup transfer status. - * - * @param cls closure - * @param instance_id the instance to look up details at - * @param exchange_url the exchange that made the transfer - * @param wtid wire transfer subject - * @param[out] total_amount amount that was debited from our - * aggregate balance at the exchange (in total, sum of - * the wire transfer amount and the @a wire_fee) - * @param[out] wire_fee the wire fee the exchange charged (only set if @a have_exchange_sig is true) - * @param[out] exchange_amount the amount the exchange claims was transferred (only set if @a have_exchange_sig is true) - * @param[out] execution_time when the transfer was executed by the exchange (only set if @a have_exchange_sig is true) - * @param[out] have_exchange_sig do we have a response from the exchange about this transfer - * @param[out] verified did we confirm the transfer was OK - * @return transaction status - */ - enum GNUNET_DB_QueryStatus - (*lookup_transfer)( - void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - struct TALER_Amount *total_amount, - struct TALER_Amount *wire_fee, - struct TALER_Amount *exchange_amount, - struct GNUNET_TIME_Timestamp *execution_time, - bool *have_exchange_sig, - bool *verified); - - - /** - * Set transfer status to confirmed. - * - * @param cls closure - * @param instance_id instance to lookup payments for - * @param exchange_url the exchange that made the transfer - * @param wtid wire transfer subject - * @param amount confirmed amount of the wire transfer - * @return transaction status - */ - enum GNUNET_DB_QueryStatus - (*set_transfer_status_to_confirmed)( - void *cls, - const char *instance_id, - const char *exchange_url, - const struct TALER_WireTransferIdentifierRawP *wtid, - const struct TALER_Amount *amount); - - - /** * Lookup transfer summary (used if we already verified the details). * * @param cls closure @@ -3194,7 +3265,7 @@ struct TALER_MERCHANTDB_Plugin * @param limit number of entries to return, negative for descending in execution time, * positive for ascending in execution time * @param offset transfer_serial number of the transfer we want to offset from - * @param verified filter transfers by verification status + * @param expected filter for transfers that were expected * @param cb function to call with detailed transfer data * @param cb_cls closure for @a cb * @return transaction status @@ -3208,12 +3279,44 @@ struct TALER_MERCHANTDB_Plugin struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll yna, + enum TALER_EXCHANGE_YesNoAll expected, TALER_MERCHANTDB_TransferCallback cb, void *cb_cls); /** + * Lookup expected incoming transfers. + * + * @param cls closure + * @param instance_id instance to lookup payments for + * @param payto_uri account that we are interested in transfers to + * @param before timestamp for the earliest transfer we care about + * @param after timestamp for the last transfer we care about + * @param limit number of entries to return, negative for descending in execution time, + * positive for ascending in execution time + * @param offset expected_transfer_serial number of the transfer we want to offset from + * @param confirmed filter by confirmation status + * @param verified filter by verification status + * @param cb function to call with detailed transfer data + * @param cb_cls closure for @a cb + * @return transaction status + */ + enum GNUNET_DB_QueryStatus + (*lookup_expected_transfers)( + void *cls, + const char *instance_id, + struct TALER_FullPayto payto_uri, + struct GNUNET_TIME_Timestamp before, + struct GNUNET_TIME_Timestamp after, + int64_t limit, + uint64_t offset, + enum TALER_EXCHANGE_YesNoAll confirmed, + enum TALER_EXCHANGE_YesNoAll verified, + TALER_MERCHANTDB_IncomingCallback cb, + void *cb_cls); + + + /** * Store information about wire fees charged by an exchange, * including signature (so we have proof). * @@ -3973,20 +4076,23 @@ struct TALER_MERCHANTDB_Plugin * * @param cls closure * @param deposit_serial deposit to update status for - * @param wire_pending should we keep checking for the wire status with the exchange? - * @param future_retry when should we ask the exchange again - * @param retry_backoff current value for the retry backoff - * @param emsg error message to record + * @param retry_needed true if the HTTP request should be retried + * @param retry_time when should we ask the exchange again + * @param last_http_status HTTP status code of the last reply + * @param last_ec Taler error code of the last reply + * @param last_detail detail from error message to record, possibly NULL * @return database result code */ enum GNUNET_DB_QueryStatus (*update_deposit_confirmation_status)( void *cls, uint64_t deposit_serial, - bool wire_pending, - struct GNUNET_TIME_Timestamp future_retry, - struct GNUNET_TIME_Relative retry_backoff, - const char *emsg); + bool retry_needed, + struct GNUNET_TIME_Timestamp retry_time, + uint32_t last_http_status, + enum TALER_ErrorCode last_ec, + const char *last_detail); + #ifdef HAVE_DONAU_DONAU_SERVICE_H enum GNUNET_DB_QueryStatus diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 19 +#define MERCHANT_PROTOCOL_CURRENT 20 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 7 +#define MERCHANT_PROTOCOL_AGE 8 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get_product.c b/src/lib/merchant_api_get_product.c @@ -1,6 +1,6 @@ /* This file is part of TALER - Copyright (C) 2014-2024 Taler Systems SA + Copyright (C) 2014--2025 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -94,6 +94,9 @@ handle_get_product_finished (void *cls, { struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_string ( + "product_name", + &pgr.details.ok.product_name), + GNUNET_JSON_spec_string ( "description", &pgr.details.ok.description), GNUNET_JSON_spec_object_const ( diff --git a/src/lib/merchant_api_get_transfers.c b/src/lib/merchant_api_get_transfers.c @@ -136,18 +136,10 @@ handle_transfers_get_finished (void *cls, &td->exchange_url), GNUNET_JSON_spec_uint64 ("transfer_serial_id", &td->credit_serial), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("execution_time", - &td->execution_time), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("verified", - &td->verified), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("confirmed", - &td->confirmed), - NULL), + GNUNET_JSON_spec_timestamp ("execution_time", + &td->execution_time), + GNUNET_JSON_spec_bool ("expected", + &td->expected), GNUNET_JSON_spec_end () }; @@ -224,13 +216,13 @@ TALER_MERCHANT_transfers_get ( const struct GNUNET_TIME_Timestamp after, int64_t limit, uint64_t offset, - enum TALER_EXCHANGE_YesNoAll verified, + enum TALER_EXCHANGE_YesNoAll expected, TALER_MERCHANT_GetTransfersCallback cb, void *cb_cls) { struct TALER_MERCHANT_GetTransfersHandle *gth; CURL *eh; - const char *verified_s = NULL; + const char *expected_s = NULL; char limit_s[30]; char offset_s[30]; char before_s[30]; @@ -240,7 +232,7 @@ TALER_MERCHANT_transfers_get ( gth->ctx = ctx; gth->cb = cb; gth->cb_cls = cb_cls; - verified_s = TALER_yna_to_string (verified); + expected_s = TALER_yna_to_string (expected); GNUNET_snprintf (limit_s, sizeof (limit_s), "%lld", @@ -249,8 +241,6 @@ TALER_MERCHANT_transfers_get ( sizeof (offset_s), "%lld", (unsigned long long) offset); - - GNUNET_snprintf (before_s, sizeof (before_s), "%llu", @@ -266,9 +256,9 @@ TALER_MERCHANT_transfers_get ( "private/transfers", "payto_uri", enc_payto, - "verified", - (TALER_EXCHANGE_YNA_ALL != verified) - ? verified_s + "expected", + (TALER_EXCHANGE_YNA_ALL != expected) + ? expected_s : NULL, "limit", 0 != limit diff --git a/src/lib/merchant_api_patch_product.c b/src/lib/merchant_api_patch_product.c @@ -178,6 +178,10 @@ TALER_MERCHANT_product_patch ( json_t *req_obj; req_obj = GNUNET_JSON_PACK ( + /* FIXME: once we move to the new-style API, + allow applications to set the product name properly! */ + GNUNET_JSON_pack_string ("product_name", + description), GNUNET_JSON_pack_string ("description", description), GNUNET_JSON_pack_object_incref ("description_i18n", diff --git a/src/lib/merchant_api_post_products.c b/src/lib/merchant_api_post_products.c @@ -198,6 +198,10 @@ TALER_MERCHANT_products_post3 ( req_obj = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("product_id", product_id), + /* FIXME: once we move to the new-style API, + allow applications to set the product name properly! */ + GNUNET_JSON_pack_string ("product_name", + description), GNUNET_JSON_pack_string ("description", description), GNUNET_JSON_pack_allow_null ( diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -498,6 +498,8 @@ run (void *cls, MHD_HTTP_NO_CONTENT, "deposit-simple", NULL), + TALER_TESTING_cmd_depositcheck ("run taler-merchant-depositcheck-1", + config_file), TALER_TESTING_cmd_run_tme ("run taler-merchant-reconciliation-1", config_file), TALER_TESTING_cmd_merchant_post_transfer2 ("post-transfer-bad", diff --git a/src/testing/test_merchant_api.conf b/src/testing/test_merchant_api.conf @@ -17,6 +17,9 @@ DURATION = 14 days [bank] HTTP_PORT = 8082 +[merchant-exchange-chf] +DISABLED = YES + [libeufin-bank] CURRENCY = EUR WIRE_TYPE = iban @@ -36,6 +39,7 @@ CURRENCY = EUR PORT = 8080 SERVE = tcp DB = postgres +STRICT_PROTOCOL_V19 = NO [merchantdb-postgres] CONFIG = postgres:///talercheck diff --git a/src/testing/test_merchant_instance_auth.sh b/src/testing/test_merchant_instance_auth.sh @@ -55,7 +55,7 @@ BASIC_AUTH=$(echo -n admin:new_pw | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ http://localhost:9966/private/token \ - -d '{"scope":"write"}' \ + -d '{"scope":"spa"}' \ -w "%{http_code}" -s -o $LAST_RESPONSE) @@ -140,7 +140,7 @@ BASIC_AUTH=$(echo -n "admin:$NEW_SECRET" | base64) STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H "Authorization: Basic $BASIC_AUTH" \ http://localhost:9966/private/token \ - -d '{"scope":"write"}' \ + -d '{"scope":"spa"}' \ -w "%{http_code}" -s -o $LAST_RESPONSE) @@ -246,12 +246,12 @@ TOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") echo " OK" >&2 -echo -n "Requesting login token... (write)" >&2 +echo -n "Requesting login token... (spa)" >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Basic '"$BASIC_AUTH2" \ http://localhost:9966/instances/second/private/token \ - -d '{"scope":"write","refreshable":true}' \ + -d '{"scope":"spa"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] @@ -286,6 +286,8 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -d '{"method":"token","password":"again"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") +BASIC_AUTH2=$(echo -n second:again | base64) + if [ "$STATUS" != "204" ] then cat $LAST_RESPONSE @@ -309,12 +311,12 @@ fi echo " OK" >&2 -echo -n "Refreshing login token..." >&2 +echo -n "Refreshing login token... (expected failure)" >&2 STATUS=$(curl -H "Content-Type: application/json" -X POST \ -H 'Authorization: Bearer '"$TOKEN" \ http://localhost:9966/instances/second/private/token \ - -d '{"scope":"write","refreshable":true}' \ + -d '{"scope":"spa","refreshable":true}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "403" ] @@ -325,6 +327,129 @@ fi echo " OK" >&2 +echo -n "Refreshing login token... (expected failure)" >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"spa","refreshable":true}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "401" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 401, refused to upgrade login token. got: $STATUS" +fi + +echo " OK" >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Basic '"$BASIC_AUTH2" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"spa:refreshable"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, login token created. got: $STATUS" +fi + +RWTOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"spa","refreshable":true}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 200. got: $STATUS" +fi + +echo " OK" >&2 + + +echo -n "Requesting another login token... (read)" >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Basic '"$BASIC_AUTH2" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"readonly", "refreshable": false}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, login token created. got: $STATUS" +fi + +RTOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") + +echo " OK" >&2 + +echo -n "Requesting another login token... (read:refreshable)" >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X POST \ + -H 'Authorization: Basic '"$BASIC_AUTH2" \ + http://localhost:9966/instances/second/private/token \ + -d '{"scope":"readonly:refreshable", "description": "readonly but refreshable"}' \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 200, login token created. got: $STATUS" +fi + +RTOKEN=$(jq -e -r .access_token < "$LAST_RESPONSE") + +echo " OK" >&2 + +echo "Getting 2 login tokens with offset 2." >&2 + +STATUS=$(curl -H "Content-Type: application/json" \ + -H "Authorization: Bearer $RWTOKEN" \ + 'http://localhost:9966/instances/second/private/tokens?limit=2&offset=4' \ + -w "%{http_code}" -s -o $LAST_RESPONSE) + +if [ "$STATUS" != "200" ] +then + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +TOKEN_SERIAL=$(jq -e -r .tokens[1].serial < "$LAST_RESPONSE") + +echo -n "Deleting second login token by serial..." >&2 + +STATUS=$(curl -H "Content-Type: application/json" -X DELETE \ + -H 'Authorization: Bearer '"$RWTOKEN" \ + http://localhost:9966/instances/second/private/tokens/$TOKEN_SERIAL \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "204" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 204, login token deleted. got: $STATUS" +fi +echo " OK" >&2 + +echo -n "Using deleted login token..." >&2 + +STATUS=$(curl "http://localhost:9966/instances/second/private/orders" \ + -H 'Authorization: Bearer '"$RTOKEN" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") + +if [ "$STATUS" != "401" ] +then + jq < "$LAST_RESPONSE" >&2 + exit_fail "Expected 401, token was deleted. got: $STATUS" +fi + +echo " OK" >&2 + echo -n "Deleting login token..." >&2 diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -482,8 +482,15 @@ echo "OK" NOW=$(date +%s) echo -n "Pay first order ${PAY_URL} ..." -taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" handle-uri "${PAY_URL}" -y 2> wallet-pay1.err > wallet-pay1.log -taler-wallet-cli --no-throttle --wallet-db="$WALLET_DB" run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log +taler-wallet-cli \ + --no-throttle \ + --wallet-db="$WALLET_DB" \ + handle-uri "${PAY_URL}" \ + -y 2> wallet-pay1.err > wallet-pay1.log +taler-wallet-cli \ + --no-throttle \ + --wallet-db="$WALLET_DB" \ + run-until-done 2> wallet-finish-pay1.err > wallet-finish-pay1.log NOW2=$(date +%s) echo " OK (took $(( NOW2 - NOW )) secs )" @@ -513,12 +520,20 @@ WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPON NOW=$(date +%s) -TO_SLEEP=$(( WIRE_DEADLINE - NOW )) +TO_SLEEP=$((1200 + WIRE_DEADLINE - NOW )) echo "Waiting $TO_SLEEP secs for wire transfer" echo -n "Perform wire transfers ..." -taler-exchange-aggregator -y -c "$CONF" -T "${TO_SLEEP}"000000 -t -L INFO &> aggregator.log -taler-exchange-transfer -c "$CONF" -t -L INFO &> transfer.log +taler-exchange-aggregator \ + -y \ + -c "$CONF" \ + -T "${TO_SLEEP}"000000 \ + -t \ + -L INFO &> aggregator.log +taler-exchange-transfer \ + -c "$CONF" \ + -t \ + -L INFO &> transfer.log echo " DONE" echo -n "Give time to Nexus to route the payment to Sandbox..." # FIXME-MS: trigger immediate update at nexus @@ -560,21 +575,6 @@ fi echo "OK" -echo -n "Notifying merchant of correct wire transfer (conflicting with old data)..." - -STATUS=$(curl 'http://localhost:9966/private/transfers' \ - -d '{"credit_amount":"'"$CREDIT_AMOUNT"'","wtid":"'"$WTID"'","payto_uri":"'"$TARGET_PAYTO"'","exchange_url":"'"$WURL"'"}' \ - -m 3 \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") - -if [ "$STATUS" != "409" ] -then - jq . < "$LAST_RESPONSE" - exit_fail "Expected response conflict, after providing conflicting transfer data. got: $STATUS" -fi - -echo " OK" - echo -n "Deleting bogus wire transfer ..." TID=$(curl -s http://localhost:9966/private/transfers | jq -r .transfers[0].transfer_serial_id) @@ -599,7 +599,7 @@ fi echo " OK" -echo -n "Notifying merchant of correct wire transfer (now working)..." +echo -n "Notifying merchant of correct wire transfer..." STATUS=$(curl 'http://localhost:9966/private/transfers' \ -d '{"credit_amount":"'"$CREDIT_AMOUNT"'","wtid":"'"$WTID"'","payto_uri":"'"$TARGET_PAYTO"'","exchange_url":"'"$WURL"'"}' \ @@ -614,26 +614,22 @@ fi echo " OK" -echo -n "Testing idempotence ..." +echo -n "Running taler-merchant-depositcheck ..." set -e - - -# Test idempotence: do it again! - -STATUS=$(curl 'http://localhost:9966/private/transfers' \ - -d '{"credit_amount":"'"$CREDIT_AMOUNT"'","wtid":"'"$WTID"'","payto_uri":"'"$TARGET_PAYTO"'","exchange_url":"'"$WURL"'"}' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") - -if [ "$STATUS" != "204" ] -then - jq . < "$LAST_RESPONSE" - exit_fail "Expected response No Content, after providing transfer data. got: $STATUS" -fi - +taler-merchant-depositcheck \ + -L INFO \ + -c "$CONF" \ + -T "${TO_SLEEP}"000000 \ + -t &> taler-merchant-depositcheck.log echo " OK" -echo -n "Testing taler-merchant-reconciliation ..." + +echo -n "Running taler-merchant-reconciliation ..." set -e -taler-merchant-reconciliation -L INFO -c "$CONF" -t &> taler-merchant-reconciliation.log +taler-merchant-reconciliation \ + -L INFO \ + -c "$CONF" \ + -T "${TO_SLEEP}"000000 \ + -t &> taler-merchant-reconciliation.log echo " OK" diff --git a/src/testing/test_merchant_product_creation.sh b/src/testing/test_merchant_product_creation.sh @@ -89,8 +89,8 @@ fi echo "OK" RANDOM_IMG='data:image/png;base64,abcdefg' -INFINITE_PRODUCT_TEMPLATE='{"product_id":"2","description":"product with id 2 and price :15","price":"TESTKUDOS:15","total_stock":-1,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}' -MANAGED_PRODUCT_TEMPLATE='{"product_id":"3","description":"product with id 3 and price :10","price":"TESTKUDOS:150","total_stock":2,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}' +INFINITE_PRODUCT_TEMPLATE='{"product_id":"2","product_name":"stuff","description":"product with id 2 and price :15","price":"TESTKUDOS:15","total_stock":-1,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}' +MANAGED_PRODUCT_TEMPLATE='{"product_id":"3","product_name":"more stuff","description":"product with id 3 and price :10","price":"TESTKUDOS:150","total_stock":2,"unit":"","image":"'"$RANDOM_IMG"'","taxes":[]}' echo -n "Creating products..." STATUS=$(curl 'http://localhost:9966/private/products' \ diff --git a/src/testing/test_merchant_transfer_tracking.sh b/src/testing/test_merchant_transfer_tracking.sh @@ -86,7 +86,10 @@ echo -n "." # NOTE: once libeufin can do long-polling, we should # be able to reduce the delay here and run wirewatch # always in the background via setup -taler-exchange-wirewatch -L "INFO" -c "$CONF" -t &> taler-exchange-wirewatch.out +taler-exchange-wirewatch \ + -L "INFO" \ + -c "$CONF" \ + -t &> taler-exchange-wirewatch0.out echo -n "." taler-wallet-cli \ @@ -236,7 +239,7 @@ WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPON NOW=$(date +%s) -TO_SLEEP=$(( WIRE_DEADLINE - NOW )) +TO_SLEEP=$((3600 + WIRE_DEADLINE - NOW )) echo "waiting $TO_SLEEP secs for wire transfer" echo -n "Perform wire transfers ..." @@ -277,7 +280,9 @@ echo -n "Notifying merchant of correct wire transfer, but on wrong instance..." STATUS=$(curl 'http://localhost:9966/private/transfers' \ -d "{\"credit_amount\":\"$CREDIT_AMOUNT\",\"wtid\":\"$WTID\",\"payto_uri\":\"$TOR_PAYTO\",\"exchange_url\":\"$WURL\"}" \ -m 3 \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] then @@ -290,7 +295,9 @@ echo " OK" echo -n "Fetching wire transfers of ADMIN instance ..." STATUS=$(curl 'http://localhost:9966/private/transfers' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] then @@ -309,13 +316,18 @@ fi echo "OK" echo -n "Fetching running taler-merchant-reconciliation on bogus transfer ..." -taler-merchant-reconciliation -c "$CONF" -L INFO -t &> taler-merchant-reconciliation-bad.log +taler-merchant-reconciliation \ + -c "$CONF" \ + -L INFO \ + -t &> taler-merchant-reconciliation0.log echo "OK" echo -n "Fetching wire transfers of 'test' instance ..." STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] then @@ -356,26 +368,36 @@ echo " OK" echo -n "Notifying merchant of correct wire transfer in the correct instance..." #this time in the correct instance so the order will be marked as wired... -STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -d '{"credit_amount":"'"$CREDIT_AMOUNT"'","wtid":"'"$WTID"'","payto_uri":"'"$TARGET_PAYTO"'","exchange_url":"'"$WURL"'"}' \ - -m 3 \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") +echo -n "Running taler-merchant-wirewatch to check transfer ..." +taler-merchant-wirewatch \ + -c $CONF \ + -t \ + -L INFO &> taler-merchant-wirewatch1.log +echo " DONE" -if [ "$STATUS" != "204" ] -then - jq . < "$LAST_RESPONSE" - exit_fail "Expected response 204 no content, after providing transfer data. got: $STATUS" -fi -echo " OK" +echo -n "Post-check for exchange deposit ..." +taler-merchant-depositcheck \ + -c $CONF \ + -t \ + -e "http://localhost:8081/" \ + -T ${TO_SLEEP}000000 \ + -L INFO &> depositcheck1a.log +echo " DONE" echo -n "Fetching running taler-merchant-reconciliation on good transfer ..." -taler-merchant-reconciliation -c $CONF -L INFO -t &> taler-merchant-reconciliation-bad.log +taler-merchant-reconciliation \ + -c $CONF \ + -L INFO \ + -T ${TO_SLEEP}000000 \ + -t &> taler-merchant-reconciliation1.log echo "OK" echo -n "Fetching wire transfers of TEST instance ..." STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] then @@ -481,20 +503,37 @@ WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPON NOW=$(date +%s) -TO_SLEEP=$(( WIRE_DEADLINE - NOW )) +TO_SLEEP=$((3600 + WIRE_DEADLINE - NOW )) echo "waiting $TO_SLEEP secs for wire transfer" echo -n "Pre-check for exchange deposit ..." -taler-merchant-depositcheck -c $CONF -t -L INFO &> depositcheck2a.log +taler-merchant-depositcheck \ + -c $CONF \ + -t \ + -e "http://localhost:8081/" \ + -L INFO &> depositcheck1b.log echo " DONE" echo -n "Perform wire transfers ..." -taler-exchange-aggregator -y -c $CONF -T ${TO_SLEEP}000000 -t -L INFO &> aggregator2.log -taler-exchange-transfer -c $CONF -t -L INFO &> transfer2.log +taler-exchange-aggregator \ + -y \ + -c $CONF \ + -T ${TO_SLEEP}000000 \ + -t \ + -L INFO &> aggregator2.log +taler-exchange-transfer \ + -c $CONF \ + -t \ + -L INFO &> transfer2.log echo " DONE" echo -n "Post-check for exchange deposit ..." -taler-merchant-depositcheck -c $CONF -t -T ${TO_SLEEP}000000 -L INFO &> depositcheck2b.log +taler-merchant-depositcheck \ + -c $CONF \ + -t \ + -e "http://localhost:8081/" \ + -T ${TO_SLEEP}000000 \ + -L INFO &> depositcheck1c.log echo " DONE" @@ -523,29 +562,27 @@ then fi echo " OK" -echo -n "Notifying merchant of correct wire transfer in the correct instance..." -#this time in the correct instance so the order will be marked as wired... - -STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -d '{"credit_amount":"'"$CREDIT_AMOUNT"'","wtid":"'"$WTID"'","payto_uri":"'"$TARGET_PAYTO"'","exchange_url":"'"$WURL"'"}' \ - -m 3 \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") - -if [ "$STATUS" != "204" ] -then - jq . < "$LAST_RESPONSE" - exit_fail "Expected response 204 no content, after providing transfer data. got: $STATUS" -fi -echo " OK" +echo -n "Running taler-merchant-wirewatch to check transfer ..." +taler-merchant-wirewatch \ + -c $CONF \ + -t \ + -L INFO &> taler-merchant-wirewatch2.log +echo " DONE" echo -n "Fetching running taler-merchant-reconciliation on good transfer ..." -taler-merchant-reconciliation -c $CONF -L INFO -t &> taler-merchant-reconciliation2.log +taler-merchant-reconciliation \ + -c $CONF \ + -L INFO \ + -T ${TO_SLEEP}000000 \ + -t &> taler-merchant-reconciliation2.log echo "OK" echo -n "Fetching wire transfers of TEST instance ..." STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] then @@ -648,26 +685,44 @@ WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPON NOW=$(date +%s) -TO_SLEEP=$(( WIRE_DEADLINE - NOW )) +TO_SLEEP=$((1200 + WIRE_DEADLINE - NOW )) echo "waiting $TO_SLEEP secs for wire transfer" -echo -n "Perform wire transfers ..." -taler-exchange-aggregator -y -c $CONF -T ${TO_SLEEP}000000 -t -L INFO &> aggregator3.log -taler-exchange-transfer -c $CONF -t -L INFO &> transfer3.log +echo -n "Perform wire transfers for 3rd order..." +taler-exchange-aggregator \ + -y \ + -c $CONF \ + -T ${TO_SLEEP}000000 \ + -t \ + -L INFO &> aggregator3.log +taler-exchange-transfer \ + -c $CONF \ + -t \ + -L INFO &> transfer3.log echo " DONE" echo -n "Running taler-merchant-wirewatch to check transfer ..." -taler-merchant-wirewatch -c $CONF -t -L INFO &> taler-merchant-wirewatch.log +taler-merchant-wirewatch \ + -c $CONF \ + -t \ + -L INFO &> taler-merchant-wirewatch3.log echo " DONE" echo -n "Post-wirewatch check for exchange deposit ..." -taler-merchant-depositcheck -c $CONF -t -T ${TO_SLEEP}000000 -L INFO &> depositcheck2b.log +taler-merchant-depositcheck \ + -c $CONF \ + -t \ + -e "http://localhost:8081/" \ + -T ${TO_SLEEP}000000 \ + -L INFO &> depositcheck1d.log echo " DONE" echo -n "Fetching wire transfers of TEST instance ..." STATUS=$(curl 'http://localhost:9966/instances/test/private/transfers' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" \ + -s \ + -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] then @@ -686,7 +741,11 @@ fi echo "OK" echo -n "Fetching running taler-merchant-reconciliation on good transfer ..." -taler-merchant-reconciliation -c $CONF -L INFO -t &> taler-merchant-reconciliation2.log +taler-merchant-reconciliation \ + -c $CONF \ + -L INFO \ + -T ${TO_SLEEP}000000 \ + -t &> taler-merchant-reconciliation3.log echo "OK" echo -n "Checking order status ..." diff --git a/src/testing/test_merchant_wirewatch.sh b/src/testing/test_merchant_wirewatch.sh @@ -298,7 +298,7 @@ fi WIRE_DEADLINE=$(jq -r .contract_terms.wire_transfer_deadline.t_s < "$LAST_RESPONSE") NOW=$(date +%s) -TO_SLEEP="$(( 1 + WIRE_DEADLINE - NOW ))" +TO_SLEEP="$(( 3600 + WIRE_DEADLINE - NOW ))" echo -n "Perform wire transfers (with ${TO_SLEEP}s timeshift) ..." taler-exchange-aggregator \ -y \ @@ -331,6 +331,23 @@ taler-merchant-wirewatch \ -L INFO &> merchant-wirewatch.log echo " OK" +echo -n "Obtaining deposit data from exchange..." +taler-merchant-depositcheck \ + -c "$CONF" \ + -e "http://localhost:8081/" \ + -T "${TO_SLEEP}000000" \ + -t \ + -L INFO &> merchant-depositcheck.log +echo " OK" + +echo -n "Obtaining reconciliation data from exchange..." +taler-merchant-reconciliation \ + -c "$CONF" \ + -T "${TO_SLEEP}000000" \ + -t \ + -L INFO &> merchant-reconciliation.log +echo " OK" + echo -n "Fetching wire transfers of ADMIN instance ..." STATUS=$(curl 'http://localhost:9966/private/transfers' \ -w "%{http_code}" \ @@ -347,7 +364,7 @@ then fi echo " OK" -echo -n "Integrating wire transfer data with exchange..." +echo -n "Reconciling wire transfer data with exchange..." taler-merchant-reconciliation \ -c "$CONF" \ -t \ diff --git a/src/testing/testing_api_cmd_depositcheck.c b/src/testing/testing_api_cmd_depositcheck.c @@ -70,7 +70,8 @@ depositcheck_run (void *cls, "taler-merchant-depositcheck", "-c", ws->config_filename, "-t", /* exit when done */ - "-L", "DEBUG", + "-T", "1200s", + "-L", "INFO", NULL); if (NULL == ws->depositcheck_proc) { diff --git a/src/testing/testing_api_cmd_tme.c b/src/testing/testing_api_cmd_tme.c @@ -69,7 +69,8 @@ tme_run (void *cls, "taler-merchant-reconciliation", "-c", ws->config_filename, "-t", /* exit when done */ - "-L", "DEBUG", + "-T", "1200s", + "-L", "INFO", NULL); if (NULL == ws->merchant_reconciliation_proc) {