merchant

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

commit 57866faede9d014a8410ae0844ff91a9e0116c26
parent d0e0fcaca528979231e254976a88bbc86f45cf3a
Author: Christian Grothoff <christian@grothoff.org>
Date:   Thu, 13 Aug 2026 00:41:00 +0200

implement protocol v35: #10713

Diffstat:
Msrc/backend/taler-merchant-httpd_get-config.c | 2+-
Msrc/backend/taler-merchant-httpd_get-private-transfers.c | 6++++++
Msrc/backend/taler-merchant-reconciliation.c | 3++-
Msrc/backenddb/iterate_transfers.c | 34++++++++++++++++++++++++++++++----
Msrc/backenddb/sql-schema/meson.build | 59+++++++++++++++++++++++++++++++++--------------------------
Msrc/backenddb/test_merchantdb.c | 3+++
Msrc/backenddb/update_to_expected_transfer_finalized.c | 11++++++++++-
Msrc/include/merchant-database/iterate_transfers.h | 3+++
Msrc/include/merchant-database/update_to_expected_transfer_finalized.h | 23++++++++++++-----------
Msrc/include/taler/merchant/get-private-transfers.h | 8++++++++
Msrc/lib/merchant_api_get-config.c | 4++--
Msrc/lib/merchant_api_get-private-transfers.c | 6++++++
12 files changed, 116 insertions(+), 46 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-config.c b/src/backend/taler-merchant-httpd_get-config.c @@ -44,7 +44,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_get_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "34:0:22" +#define MERCHANT_PROTOCOL_VERSION "35:0:23" /** diff --git a/src/backend/taler-merchant-httpd_get-private-transfers.c b/src/backend/taler-merchant-httpd_get-private-transfers.c @@ -35,6 +35,8 @@ * @param 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_payto_uri account of the exchange that was debited, + * `full_payto` NULL if the exchange did not tell us * @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, 0 if not @a expected @@ -47,6 +49,7 @@ transfer_cb (void *cls, const struct TALER_Amount *credit_amount, const struct TALER_WireTransferIdentifierRawP *wtid, struct TALER_FullPayto payto_uri, + struct TALER_FullPayto exchange_payto_uri, const char *exchange_url, uint64_t transfer_serial_id, uint64_t expected_transfer_serial_id, @@ -63,6 +66,9 @@ transfer_cb (void *cls, wtid), TALER_JSON_pack_full_payto ("payto_uri", payto_uri), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_full_payto ("exchange_payto_uri", + exchange_payto_uri)), GNUNET_JSON_pack_string ("exchange_url", exchange_url), GNUNET_JSON_pack_uint64 ("transfer_serial_id", diff --git a/src/backend/taler-merchant-reconciliation.c b/src/backend/taler-merchant-reconciliation.c @@ -982,7 +982,8 @@ wire_transfer_cb (struct Inquiry *w, &td->total_amount, &td->wire_fee, &td->exchange_pub, - &td->exchange_sig); + &td->exchange_sig, + td->exchange_payto_uri); if (qs < 0) { GNUNET_break (0); diff --git a/src/backenddb/iterate_transfers.c b/src/backenddb/iterate_transfers.c @@ -79,6 +79,9 @@ lookup_transfers_cb (void *cls, struct TALER_Amount credit_amount; struct TALER_WireTransferIdentifierRawP wtid; struct TALER_FullPayto payto_uri; + struct TALER_FullPayto exchange_payto_uri = { + .full_payto = NULL + }; char *exchange_url; uint64_t transfer_serial_id; uint64_t expected_transfer_serial_id = 0; @@ -93,6 +96,10 @@ lookup_transfers_cb (void *cls, &payto_uri.full_payto), GNUNET_PQ_result_spec_string ("exchange_url", &exchange_url), + GNUNET_PQ_result_spec_allow_null ( + GNUNET_PQ_result_spec_string ("exchange_payto_uri", + &exchange_payto_uri.full_payto), + NULL), GNUNET_PQ_result_spec_uint64 ("credit_serial", &transfer_serial_id), GNUNET_PQ_result_spec_allow_null ( @@ -119,6 +126,7 @@ lookup_transfers_cb (void *cls, &credit_amount, &wtid, payto_uri, + exchange_payto_uri, exchange_url, transfer_serial_id, expected_transfer_serial_id, @@ -182,15 +190,24 @@ TALER_MERCHANTDB_iterate_transfers ( ",mt.credit_serial" ",mt.execution_time" ",mt.expected" - ",met.expected_credit_serial" + /* 'expected' means the exchange's report and our + bank statement agree on the amount; keep gating + the serial on it, but not the account, which we + know as soon as the exchange reported it. */ + ",CASE WHEN mt.expected" + " THEN met.expected_credit_serial" + " ELSE NULL END AS expected_credit_serial" + ",met.exchange_payto_uri" " FROM merchant_transfers mt" " JOIN merchant_accounts mac" " USING (account_serial)" + /* (wtid, exchange_url, account_serial) is UNIQUE on + merchant_expected_transfers, so this matches at + most one row and cannot fan out the result. */ " LEFT JOIN merchant_expected_transfers met" " ON mt.wtid = met.wtid" " AND mt.account_serial = met.account_serial" " AND mt.exchange_url = met.exchange_url" - " AND mt.expected" " WHERE ( $6 OR " " (mt.execution_time < $1 AND" " mt.execution_time >= $2) )" @@ -214,15 +231,24 @@ TALER_MERCHANTDB_iterate_transfers ( ",mt.credit_serial" ",mt.execution_time" ",mt.expected" - ",met.expected_credit_serial" + /* 'expected' means the exchange's report and our + bank statement agree on the amount; keep gating + the serial on it, but not the account, which we + know as soon as the exchange reported it. */ + ",CASE WHEN mt.expected" + " THEN met.expected_credit_serial" + " ELSE NULL END AS expected_credit_serial" + ",met.exchange_payto_uri" " FROM merchant_transfers mt" " JOIN merchant_accounts mac" " USING (account_serial)" + /* (wtid, exchange_url, account_serial) is UNIQUE on + merchant_expected_transfers, so this matches at + most one row and cannot fan out the result. */ " LEFT JOIN merchant_expected_transfers met" " ON mt.wtid = met.wtid" " AND mt.account_serial = met.account_serial" " AND mt.exchange_url = met.exchange_url" - " AND mt.expected" " WHERE ( $6 OR " " (mt.execution_time < $1 AND" " mt.execution_time >= $2) )" diff --git a/src/backenddb/sql-schema/meson.build b/src/backenddb/sql-schema/meson.build @@ -23,13 +23,15 @@ sql_global_procedures = [ '../gc.sql', ] -gprocedures_sql = custom_target('sql_global_procedures', - input: sql_global_procedures, - output: 'global_procedures.sql', - capture: true, - command: ['./gen-procedures.sh', 'merchant', '@INPUT@'], - install: true, - install_dir: sqldir) +gprocedures_sql = custom_target( + 'sql_global_procedures', + input: sql_global_procedures, + output: 'global_procedures.sql', + capture: true, + command: ['./gen-procedures.sh', 'merchant', '@INPUT@'], + install: true, + install_dir: sqldir, +) sql_instance_procedures = [ '../pg_statistics_helpers.sql', @@ -70,13 +72,15 @@ sql_instance_procedures = [ '../pg_update_inventory_locked.sql', ] -iprocedures_sql = custom_target('instance_procedures', - input: sql_instance_procedures, - output: 'instance_procedures.sql', - capture: true, - command: ['./gen-procedures.sh', 'merchant_instances', '@INPUT@'], - install: true, - install_dir: sqldir) +iprocedures_sql = custom_target( + 'instance_procedures', + input: sql_instance_procedures, + output: 'instance_procedures.sql', + capture: true, + command: ['./gen-procedures.sh', 'merchant_instances', '@INPUT@'], + install: true, + install_dir: sqldir, +) # gen-procedures.sh wraps each concatenation above in a single # BEGIN ... COMMIT; make sure no input file closes that transaction early. @@ -134,6 +138,7 @@ generated_sql = [ ['merchant-0041.sql'], ['merchant-0042.sql'], ['merchant-0043.sql'], + ['merchant-0044.sql'], ] foreach g : generated_sql @@ -150,15 +155,17 @@ endforeach custom_target( - 'gen-merchantdb-merchant_0036.sql', - input: ['merchant-0036.sql.in', - 'merchant-0036-init.sql.fragment', - 'merchant-0036-copy.sql.fragment', - 'merchant-0036-drop.sql.fragment', - 'merchant-0036-setval.sql.fragment'], - output: ['merchant-0036.sql'], - capture: true, - command: ['./preprocess-sql.sh', '@INPUT@'], - install: true, - install_dir: sqldir, - ) + 'gen-merchantdb-merchant_0036.sql', + input: [ + 'merchant-0036.sql.in', + 'merchant-0036-init.sql.fragment', + 'merchant-0036-copy.sql.fragment', + 'merchant-0036-drop.sql.fragment', + 'merchant-0036-setval.sql.fragment', + ], + output: ['merchant-0036.sql'], + capture: true, + command: ['./preprocess-sql.sh', '@INPUT@'], + install: true, + install_dir: sqldir, +) diff --git a/src/backenddb/test_merchantdb.c b/src/backenddb/test_merchantdb.c @@ -6017,6 +6017,8 @@ struct TestLookupTransfers_Closure * @param credit_amount how much was wired to the merchant (minus fees) * @param wtid wire transfer identifier * @param payto_uri target account that received the wire transfer + * @param exchange_payto_uri account of the exchange that was debited, + * `full_payto` NULL if unknown; unused here * @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, 0 if not @a expected @@ -6029,6 +6031,7 @@ lookup_transfers_cb (void *cls, const struct TALER_Amount *credit_amount, const struct TALER_WireTransferIdentifierRawP *wtid, struct TALER_FullPayto payto_uri, + struct TALER_FullPayto exchange_payto_uri, const char *exchange_url, uint64_t transfer_serial_id, uint64_t expected_transfer_serial_id, diff --git a/src/backenddb/update_to_expected_transfer_finalized.c b/src/backenddb/update_to_expected_transfer_finalized.c @@ -33,7 +33,8 @@ TALER_MERCHANTDB_update_to_expected_transfer_finalized ( const struct TALER_Amount *total_amount, const struct TALER_Amount *wire_fee, const struct TALER_ExchangePublicKeyP *exchange_pub, - const struct TALER_ExchangeSignatureP *exchange_sig) + const struct TALER_ExchangeSignatureP *exchange_sig, + const struct TALER_FullPayto exchange_payto_uri) { struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_auto_from_type (wtid), @@ -45,6 +46,9 @@ TALER_MERCHANTDB_update_to_expected_transfer_finalized ( 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), + NULL == exchange_payto_uri.full_payto + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (exchange_payto_uri.full_payto), GNUNET_PQ_query_param_end }; @@ -66,6 +70,11 @@ TALER_MERCHANTDB_update_to_expected_transfer_finalized ( ",h_details=$5" ",signkey_serial=subquery.signkey_serial" ",exchange_sig=$7" + /* COALESCE: a retry against an exchange that stopped + reporting the account (downgrade, or a pre-v39 + exchange) must not erase what we already learned. */ + ",exchange_payto_uri=" + "COALESCE(CAST($8 AS TEXT),exchange_payto_uri)" " FROM subquery" " WHERE wtid=$1" " AND exchange_url=$2"); diff --git a/src/include/merchant-database/iterate_transfers.h b/src/include/merchant-database/iterate_transfers.h @@ -33,6 +33,8 @@ * @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_payto_uri account of the exchange that was debited; + * `full_payto` is NULL if the exchange did not tell us * @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, 0 if not @a expected @@ -46,6 +48,7 @@ typedef void const struct TALER_Amount *expected_credit_amount, const struct TALER_WireTransferIdentifierRawP *wtid, struct TALER_FullPayto payto_uri, + struct TALER_FullPayto exchange_payto_uri, const char *exchange_url, uint64_t transfer_serial_id, uint64_t expected_transfer_serial_id, diff --git a/src/include/merchant-database/update_to_expected_transfer_finalized.h b/src/include/merchant-database/update_to_expected_transfer_finalized.h @@ -38,20 +38,21 @@ struct TALER_MERCHANTDB_PostgresContext; * @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 + * @param exchange_payto_uri bank account of the exchange that was debited; + * `full_payto` is NULL if the exchange did not tell us * @return database transaction status */ enum GNUNET_DB_QueryStatus -TALER_MERCHANTDB_update_to_expected_transfer_finalized (struct TALER_MERCHANTDB_PostgresContext *pg, - 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); +TALER_MERCHANTDB_update_to_expected_transfer_finalized ( + struct TALER_MERCHANTDB_PostgresContext *pg, + 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, + const struct TALER_FullPayto exchange_payto_uri); #endif diff --git a/src/include/taler/merchant/get-private-transfers.h b/src/include/taler/merchant/get-private-transfers.h @@ -309,6 +309,14 @@ struct TALER_MERCHANT_GetPrivateTransfersTransferData struct TALER_FullPayto payto_uri; /** + * URI of the exchange's own bank account that was debited. + * `full_payto` is NULL if the backend does not know it: the + * exchange only started reporting it with its protocol v39, + * and even then not for older transfers. + */ + struct TALER_FullPayto exchange_payto_uri; + + /** * URL of the exchange that made the transfer. */ const char *exchange_url; diff --git a/src/lib/merchant_api_get-config.c b/src/lib/merchant_api_get-config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 34 +#define MERCHANT_PROTOCOL_CURRENT 35 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 10 +#define MERCHANT_PROTOCOL_AGE 11 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get-private-transfers.c b/src/lib/merchant_api_get-private-transfers.c @@ -148,6 +148,12 @@ parse_transfers ( &td->wtid), TALER_JSON_spec_full_payto_uri ("payto_uri", &td->payto_uri), + /* Optional: unknown for transfers reported by an exchange + older than protocol v39. */ + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_full_payto_uri ("exchange_payto_uri", + &td->exchange_payto_uri), + NULL), TALER_JSON_spec_web_url ("exchange_url", &td->exchange_url), GNUNET_JSON_spec_uint64 ("transfer_serial_id",