merchant

Merchant backend to process payments, run by merchants
Log | Files | Refs | Submodules | README | LICENSE

commit 4190d65777940d270ebc9b1f5cd0ff36a61b3d5b
parent 26b591d96d4ab359f02d706174b3273be1141eaf
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 10 Jul 2025 20:59:56 +0200

fix for #9454

Diffstat:
Msrc/backend/Makefile.am | 3+++
Msrc/backend/taler-merchant-depositcheck.c | 124+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/backend/taler-merchant-httpd.c | 9+++++++++
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 5++++-
Asrc/backend/taler-merchant-httpd_private-get-incoming.c | 193+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backend/taler-merchant-httpd_private-get-incoming.h | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/backend/taler-merchant-httpd_private-get-transfers.c | 113+++++++++++++++++++++++++------------------------------------------------------
Msrc/backend/taler-merchant-httpd_private-post-transfers.c | 17++++++-----------
Msrc/backend/taler-merchant-reconciliation.c | 131+++++++++++++++++++++++++++++++++++++------------------------------------------
Msrc/backend/taler-merchant-wirewatch.c | 49++-----------------------------------------------
Msrc/backenddb/Makefile.am | 5+++--
Asrc/backenddb/merchant-0021.sql | 295+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_finalize_transfer_status.c | 80+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_finalize_transfer_status.h | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_insert_deposit.c | 7+++++--
Msrc/backenddb/pg_insert_deposit.h | 5++++-
Msrc/backenddb/pg_insert_deposit_to_transfer.c | 48++++++++++++++++--------------------------------
Msrc/backenddb/pg_insert_deposit_to_transfer.h | 10++++++----
Msrc/backenddb/pg_insert_deposit_to_transfer.sql | 205++++++++++++++++++++++++++++++-------------------------------------------------
Msrc/backenddb/pg_insert_transfer.c | 20+++++++++++++++-----
Msrc/backenddb/pg_insert_transfer.h | 6+++---
Msrc/backenddb/pg_insert_transfer_details.c | 4+++-
Msrc/backenddb/pg_insert_transfer_details.sql | 125++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/backenddb/pg_lookup_deposits_by_contract_and_coin.c | 2+-
Asrc/backenddb/pg_lookup_expected_transfers.c | 272+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/backenddb/pg_lookup_expected_transfers.h | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_lookup_pending_deposits.c | 32++++++++++++++++----------------
Dsrc/backenddb/pg_lookup_transfer.c | 125-------------------------------------------------------------------------------
Dsrc/backenddb/pg_lookup_transfer.h | 57---------------------------------------------------------
Msrc/backenddb/pg_lookup_transfer_details.c | 14++++++++------
Msrc/backenddb/pg_lookup_transfer_details_by_order.c | 31++++++++++++++++---------------
Msrc/backenddb/pg_lookup_transfer_summary.c | 15++++++++-------
Msrc/backenddb/pg_lookup_transfers.c | 79+++++++++++++++++++++++++++++++++----------------------------------------------
Msrc/backenddb/pg_lookup_transfers.h | 4++--
Msrc/backenddb/pg_select_open_transfers.c | 43++++++++++++++++++-------------------------
Dsrc/backenddb/pg_set_transfer_status_to_confirmed.c | 66------------------------------------------------------------------
Dsrc/backenddb/pg_set_transfer_status_to_confirmed.h | 48------------------------------------------------
Msrc/backenddb/pg_update_deposit_confirmation_status.c | 37+++++++++++++++++++------------------
Msrc/backenddb/pg_update_deposit_confirmation_status.h | 18++++++++++--------
Msrc/backenddb/pg_update_transfer_status.c | 26++++++++++++++++----------
Msrc/backenddb/pg_update_transfer_status.h | 10++++++----
Msrc/backenddb/plugin_merchantdb_postgres.c | 16++++++++--------
Msrc/backenddb/test_merchantdb.c | 186++++++++++++++++++++++++++-----------------------------------------------------
Msrc/include/taler_merchant_service.h | 32++++++++------------------------
Msrc/include/taler_merchantdb_plugin.h | 216++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Msrc/lib/merchant_api_get_transfers.c | 30++++++++++--------------------
Msrc/testing/test_merchant_api.c | 2++
Msrc/testing/test_merchant_order_creation.sh | 72++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/testing/test_merchant_transfer_tracking.sh | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/testing/test_merchant_wirewatch.sh | 21+++++++++++++++++++--
Msrc/testing/testing_api_cmd_depositcheck.c | 3++-
Msrc/testing/testing_api_cmd_tme.c | 3++-
52 files changed, 1926 insertions(+), 1297 deletions(-)

diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am @@ -127,6 +127,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 \ @@ -267,6 +269,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,6 +50,7 @@ #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" @@ -1554,6 +1555,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", diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -933,7 +933,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-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-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-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, - &timestamp, - &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 @@ -37,6 +37,7 @@ sql_DATA = \ merchant-0018.sql \ merchant-0019.sql \ merchant-0020.sql \ + merchant-0021.sql \ drop.sql BUILT_SOURCES = \ @@ -141,6 +142,7 @@ 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 \ @@ -165,7 +167,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 \ @@ -190,7 +191,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 \ @@ -205,6 +205,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-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_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_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_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, + &ltc); + 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_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_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_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 @@ -63,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 +121,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 +137,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" @@ -434,6 +434,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 @@ -530,10 +532,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 @@ -620,6 +618,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 @@ -1383,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; } @@ -1602,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) { @@ -2435,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; } @@ -2710,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; } @@ -2880,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, @@ -2913,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]++; } } } @@ -3369,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; } @@ -3543,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 @@ -3867,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 ( @@ -3880,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, @@ -3904,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; } } @@ -4078,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 */ @@ -4090,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; @@ -4099,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++; } @@ -4138,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) { @@ -4159,7 +4098,7 @@ test_lookup_transfers (const struct InstanceData *instance, after, limit, offset, - filter_verified, + TALER_EXCHANGE_YNA_ALL, &lookup_transfers_cb, &cmp)) { @@ -4244,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; } @@ -4490,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, @@ -4532,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; @@ -4548,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; } @@ -5317,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; } @@ -5881,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, @@ -5893,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)) { @@ -6326,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, @@ -6337,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)) { @@ -6727,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; } @@ -7221,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], @@ -7256,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, @@ -7285,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 @@ -4396,22 +4396,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. + * True if this wire transfer was expected. */ - bool verified; - - /** - * Did we confirm the wire transfer happened (via - * #TALER_MERCHANT_transfers_post())? - */ - bool confirmed; + bool expected; }; @@ -4467,16 +4460,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 @@ -4487,7 +4471,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 @@ -4501,7 +4485,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 @@ -913,10 +913,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 @@ -927,7 +926,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); @@ -937,11 +935,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 */ @@ -950,7 +949,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, @@ -1000,32 +999,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. * @@ -2586,6 +2615,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 @@ -2597,7 +2628,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); /** @@ -2757,9 +2789,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 @@ -2768,9 +2801,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 @@ -2796,15 +2856,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); /** @@ -3006,8 +3068,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 @@ -3018,7 +3079,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); /** @@ -3141,56 +3202,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 @@ -3239,7 +3250,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 @@ -3253,12 +3264,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). * @@ -4018,20 +4061,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); + /** * Lookup amount statistics for instance and slug by bucket. 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/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -494,6 +494,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_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_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) {