merchant

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

commit f73e437486716e464a4afdf15bd2d4ccc7eac6dc
parent 4c62524511278ea4c1c75b17fef6e004a9d43511
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon,  6 Apr 2026 19:41:19 +0200

fix #11337

Diffstat:
Msrc/backend/taler-merchant-httpd_post-private-accounts.c | 125++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/backend/taler-merchant-kyccheck.c | 1-
Msrc/backenddb/pg_activate_account.c | 62++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/backenddb/pg_activate_account.h | 20++++++++++++++------
Asrc/backenddb/pg_activate_account.sql | 146+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/pg_inactivate_account.c | 29+++++++++++++++++++----------
Asrc/backenddb/pg_inactivate_account.sql | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/backenddb/procedures.sql.in | 2++
Msrc/include/taler/taler_merchantdb_plugin.h | 17++++++++++++-----
9 files changed, 352 insertions(+), 99 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_post-private-accounts.c b/src/backend/taler-merchant-httpd_post-private-accounts.c @@ -190,10 +190,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, MHD_RESULT mret; GNUNET_break_op (0); - mret = TALER_MHD_reply_with_error (connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PAYTO_URI_MALFORMED, - err); + mret = TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PAYTO_URI_MALFORMED, + err); GNUNET_free (err); return mret; } @@ -287,22 +288,10 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, TALER_MERCHANT_BANK_auth_free (&auth); } - TMH_db->preflight (TMH_db->cls); - for (unsigned int retries = 0; - retries < MAX_RETRIES; - retries++) { enum GNUNET_DB_QueryStatus qs; - struct TMH_WireMethod *wm; - TMH_db->rollback (TMH_db->cls); - if (GNUNET_OK != - TMH_db->start (TMH_db->cls, - "post-account")) - { - GNUNET_break (0); - break; - } + TMH_db->preflight (TMH_db->cls); qs = TMH_db->select_accounts (TMH_db->cls, mi->settings.id, &account_cb, @@ -311,13 +300,16 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, { case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); return TALER_MHD_reply_with_error (connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "select_accounts"); case GNUNET_DB_STATUS_SOFT_ERROR: - continue; + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "select_accounts"); case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: break; case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: @@ -327,7 +319,6 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, if (pac.have_same_account) { /* Idempotent request */ - TMH_db->rollback (TMH_db->cls); return TALER_MHD_REPLY_JSON_PACK ( connection, MHD_HTTP_OK, @@ -337,13 +328,11 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, GNUNET_JSON_pack_data_auto ( "h_wire", &pac.h_wire)); - } if (pac.have_conflicting_account) { /* Conflict, refuse request */ - TMH_db->rollback (TMH_db->cls); GNUNET_break_op (0); return TALER_MHD_reply_with_error (connection, MHD_HTTP_CONFLICT, @@ -356,7 +345,6 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, /* MFA needed */ enum GNUNET_GenericReturnValue ret; - TMH_db->rollback (TMH_db->cls); ret = TMH_mfa_check_simple (hc, TALER_MERCHANT_MFA_CO_ACCOUNT_CONFIGURATION, mi); @@ -370,15 +358,20 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, : MHD_NO; } } + } + + /* All pre-checks clear, now try to activate/setup the new account */ + { + struct TMH_WireMethod *wm; /* convert provided payto URI into internal data structure with salts */ wm = TMH_setup_wire_account (pac.uri, pac.credit_facade_url, pac.credit_facade_credentials); + GNUNET_assert (NULL != wm); if (NULL != pac.extra_wire_subject_metadata) wm->extra_wire_subject_metadata = GNUNET_strdup (pac.extra_wire_subject_metadata); - GNUNET_assert (NULL != wm); { struct TALER_MERCHANTDB_AccountDetails ad = { .payto_uri = wm->payto_uri, @@ -388,64 +381,79 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, .credit_facade_url = wm->credit_facade_url, .credit_facade_credentials = wm->credit_facade_credentials, .extra_wire_subject_metadata = wm->extra_wire_subject_metadata, - .active = wm->active + .active = true }; - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) - }; - - qs = TMH_db->insert_account (TMH_db->cls, - &ad); + enum GNUNET_DB_QueryStatus qs; + struct TALER_MerchantWireHashP h_wire; + struct TALER_WireSaltP salt; + bool not_found; + bool conflict; + + TMH_db->preflight (TMH_db->cls); + qs = TMH_db->activate_account (TMH_db->cls, + &ad, + &h_wire, + &salt, + &not_found, + &conflict); switch (qs) { case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: break; case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); TMH_wire_method_free (wm); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_INVARIANT_FAILURE, - "insert_account"); + "activate_account"); case GNUNET_DB_STATUS_SOFT_ERROR: - continue; + GNUNET_break (0); + TMH_wire_method_free (wm); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_STORE_FAILED, + "activate_account"); case GNUNET_DB_STATUS_HARD_ERROR: GNUNET_break (0); - TMH_db->rollback (TMH_db->cls); TMH_wire_method_free (wm); return TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_STORE_FAILED, - "insert_account"); + "activate_account"); } - - TMH_db->event_notify (TMH_db->cls, - &es, - NULL, - 0); - qs = TMH_db->commit (TMH_db->cls); - switch (qs) + if (not_found) { - case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT: - break; - case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS: - break; - case GNUNET_DB_STATUS_SOFT_ERROR: + /* must have been concurrently deleted, rare! */ TMH_wire_method_free (wm); - continue; - case GNUNET_DB_STATUS_HARD_ERROR: + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_INSTANCE_UNKNOWN, + mi->settings.id); + } + if (conflict) + { + /* Conflicting POST must have been done between our pre-check + and the actual transaction, rare! */ TMH_wire_method_free (wm); - GNUNET_break (0); + GNUNET_break_op (0); return TALER_MHD_reply_with_error ( connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_COMMIT_FAILED, - "post-account"); + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_ACCOUNT_EXISTS, + pac.uri.full_payto); } + + /* Update salt/h_wire in case we re-activated an + existing account and found a different salt + value already in the DB */ + wm->wire_salt = salt; + wm->h_wire = h_wire; + /* Finally, also update our running process */ GNUNET_CONTAINER_DLL_insert (mi->wm_head, mi->wm_tail, @@ -461,12 +469,7 @@ TMH_private_post_account (const struct TMH_RequestHandler *rh, &wm->wire_salt), GNUNET_JSON_pack_data_auto ("h_wire", &wm->h_wire)); - } /* end retries */ - TMH_db->rollback (TMH_db->cls); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_SOFT_FAILURE, - "post-accounts"); + } } diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -1745,7 +1745,6 @@ run (void *cls, GNUNET_CONFIGURATION_iterate_sections (cfg, &accept_exchanges, NULL); - fprintf (stderr, "NOW\n"); { struct GNUNET_DB_EventHeaderP es = { .size = htons (sizeof (es)), diff --git a/src/backenddb/pg_activate_account.c b/src/backenddb/pg_activate_account.c @@ -16,7 +16,7 @@ /** * @file backenddb/pg_activate_account.c * @brief Implementation of the activate_account function for Postgres - * @author Iván Ávalos + * @author Christian Grothoff */ #include "taler/platform.h" #include <taler/taler_error_codes.h> @@ -25,29 +25,59 @@ #include "pg_activate_account.h" #include "pg_helper.h" + enum GNUNET_DB_QueryStatus -TMH_PG_activate_account (void *cls, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire) +TMH_PG_activate_account ( + void *cls, + const struct TALER_MERCHANTDB_AccountDetails *account_details, + struct TALER_MerchantWireHashP *h_wire, + struct TALER_WireSaltP *salt, + bool *not_found, + bool *conflict) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { - GNUNET_PQ_query_param_string (merchant_id), - GNUNET_PQ_query_param_auto_from_type (h_wire), + GNUNET_PQ_query_param_string (account_details->instance_id), + GNUNET_PQ_query_param_auto_from_type (&account_details->h_wire), + GNUNET_PQ_query_param_auto_from_type (&account_details->salt), + GNUNET_PQ_query_param_string (account_details->payto_uri.full_payto), + NULL ==account_details->credit_facade_url + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string (account_details->credit_facade_url), + NULL == account_details->credit_facade_credentials + ? GNUNET_PQ_query_param_null () + : TALER_PQ_query_param_json (account_details->credit_facade_credentials), + NULL == account_details->extra_wire_subject_metadata + ? GNUNET_PQ_query_param_null () + : GNUNET_PQ_query_param_string ( + account_details->extra_wire_subject_metadata), GNUNET_PQ_query_param_end }; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("not_found", + not_found), + GNUNET_PQ_result_spec_bool ("conflict", + conflict), + GNUNET_PQ_result_spec_auto_from_type ("h_wire", + h_wire), + GNUNET_PQ_result_spec_auto_from_type ("salt", + salt), + GNUNET_PQ_result_spec_end + }; + GNUNET_assert (account_details->active); check_connection (pg); PREPARE (pg, "activate_account", - "UPDATE merchant_accounts SET" - " active=TRUE" - " WHERE h_wire=$2 AND" - " merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "activate_account", - params); + "SELECT " + " out_h_wire AS h_wire" + " ,out_salt AS salt" + " ,out_conflict AS conflict" + " ,out_not_found AS not_found" + " FROM merchant_do_activate_account" + " ($1,$2,$3,$4,$5,$6,$7);"); + return GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "activate_account", + params, + rs); } diff --git a/src/backenddb/pg_activate_account.h b/src/backenddb/pg_activate_account.h @@ -26,16 +26,24 @@ #include "taler/taler_merchantdb_plugin.h" /** - * Set an instance's account in our database to "active". + * Insert account or set an instance's + * existing account in our database to "active". * * @param cls closure - * @param merchant_id merchant backend instance ID - * @param h_wire hash of the wire account to set to active + * @param account_details details of the account to actiate + * @param[out] h_wire set to the hash of the salted payto URI + * @param[out] salt set to salt used for computing @a h_wire + * @param[out] not_found set to true if the instance was not found + * @param[out] conflict set to true if a conflicting account is active * @return database result code */ enum GNUNET_DB_QueryStatus -TMH_PG_activate_account (void *cls, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire); +TMH_PG_activate_account ( + void *cls, + const struct TALER_MERCHANTDB_AccountDetails *account_details, + struct TALER_MerchantWireHashP *h_wire, + struct TALER_WireSaltP *salt, + bool *not_found, + bool *conflict); #endif diff --git a/src/backenddb/pg_activate_account.sql b/src/backenddb/pg_activate_account.sql @@ -0,0 +1,146 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_activate_account; +CREATE FUNCTION merchant_do_activate_account( + IN in_instance_name TEXT + ,IN in_h_wire BYTEA + ,IN in_salt BYTEA + ,IN in_full_payto TEXT + ,IN in_credit_facade_url TEXT -- can be NULL + ,IN in_credit_facade_credentials TEXT -- can be NULL + ,IN in_extra_wire_subject_metadata TEXT -- can be NULL + ,OUT out_h_wire BYTEA + ,OUT out_salt BYTEA + ,OUT out_not_found BOOL + ,OUT out_conflict BOOL +) +LANGUAGE plpgsql +AS $$ +DECLARE my_instance INT8; +DECLARE my_active BOOL; +DECLARE my_cfu TEXT; +DECLARE my_cfc TEXT; +DECLARE my_ewsm TEXT; +DECLARE my_h_wire BYTEA; +DECLARE my_salt BYTEA; +BEGIN + out_not_found = FALSE; + out_conflict = FALSE; + out_h_wire = in_h_wire; + out_salt = in_salt; + + SELECT merchant_serial + INTO my_instance + FROM merchant_instances + WHERE merchant_id=in_instance_name; + IF NOT FOUND + THEN + out_not_found = TRUE; + RETURN; + END IF; + + INSERT INTO merchant_accounts + AS ma + (merchant_serial + ,h_wire + ,salt + ,payto_uri + ,credit_facade_url + ,credit_facade_credentials + ,active + ,extra_wire_subject_metadata + ) VALUES ( + my_instance + ,in_h_wire + ,in_salt + ,in_full_payto + ,in_credit_facade_url + ,in_credit_facade_credentials::JSONB + ,TRUE + ,in_extra_wire_subject_metadata + ) ON CONFLICT DO NOTHING; + IF FOUND + THEN + -- Notify taler-merchant-kyccheck about the change in + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + RETURN; + END IF; + + SELECT h_wire + ,salt + ,active + ,credit_facade_url + ,credit_facade_credentials::TEXT + ,extra_wire_subject_metadata + INTO my_h_wire + ,my_salt + ,my_active + ,my_cfu + ,my_cfc + ,my_ewsm + FROM merchant_accounts + WHERE merchant_serial=my_instance + AND payto_uri=in_full_payto; + IF NOT FOUND + THEN + -- This should never happen (we had a conflict!) + -- Still, safe way is to return not found. + out_not_found = TRUE; + RETURN; + END IF; + + -- Check for conflict + IF (my_active AND + (ROW (my_cfu + ,my_cfc + ,my_ewsm) + IS DISTINCT FROM + ROW (in_credit_facade_url + ,in_credit_facade_credentials + ,in_extra_wire_subject_metadata))) + THEN + -- Active conflicting account, refuse! + out_conflict = TRUE; + RETURN; + END IF; + + -- Equivalent account exists, use its salt instead of the new salt + -- and just set it to active! + out_salt = my_salt; + out_h_wire = my_h_wire; + + -- Now check if existing account is already active + IF my_active + THEN + -- nothing to do + RETURN; + END IF; + + UPDATE merchant_accounts + SET active=TRUE + ,credit_facade_url=in_credit_facade_url + ,credit_facade_credentials=in_credit_facade_credentials::JSONB + ,extra_wire_subject_metadata=in_extra_wire_subject_metadata + WHERE h_wire=out_h_wire + AND merchant_serial=my_instance; + + -- Notify taler-merchant-kyccheck about the change in (active) + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + +END $$; diff --git a/src/backenddb/pg_inactivate_account.c b/src/backenddb/pg_inactivate_account.c @@ -36,18 +36,27 @@ TMH_PG_inactivate_account (void *cls, GNUNET_PQ_query_param_auto_from_type (h_wire), GNUNET_PQ_query_param_end }; + bool found; + struct GNUNET_PQ_ResultSpec rs[] = { + GNUNET_PQ_result_spec_bool ("found", + &found), + GNUNET_PQ_result_spec_end + }; + enum GNUNET_DB_QueryStatus qs; check_connection (pg); PREPARE (pg, "inactivate_account", - "UPDATE merchant_accounts SET" - " active=FALSE" - " WHERE h_wire=$2" - " AND merchant_serial=" - " (SELECT merchant_serial" - " FROM merchant_instances" - " WHERE merchant_id=$1)"); - return GNUNET_PQ_eval_prepared_non_select (pg->conn, - "inactivate_account", - params); + "SELECT out_found AS found" + " FROM merchant_do_inactivate_account" + " ($1,$2);"); + qs = GNUNET_PQ_eval_prepared_singleton_select (pg->conn, + "inactivate_account", + params, + rs); + if (qs < 0) + return qs; + if (! found) + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; + return qs; } diff --git a/src/backenddb/pg_inactivate_account.sql b/src/backenddb/pg_inactivate_account.sql @@ -0,0 +1,49 @@ +-- +-- This file is part of TALER +-- Copyright (C) 2026 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/> +-- + +DROP FUNCTION IF EXISTS merchant_do_inactivate_account; +CREATE FUNCTION merchant_do_inactivate_account ( + IN in_instance_name TEXT + ,IN in_h_wire BYTEA + ,OUT out_found BOOL +) +LANGUAGE plpgsql +AS $$ +DECLARE + my_instance INT8; +BEGIN + SELECT merchant_serial + INTO my_instance + FROM merchant_instances + WHERE merchant_id=in_instance_name; + IF NOT FOUND + THEN + out_found = FALSE; + RETURN; + END IF; + UPDATE merchant_accounts + SET active=FALSE + WHERE h_wire=in_h_wire + AND merchant_serial=my_instance; + out_found = FOUND; + IF out_found + THEN + -- Notify taler-merchant-kyccheck about the change in + -- accounts. (TALER_DBEVENT_MERCHANT_ACCOUNTS_CHANGED) + NOTIFY XDQM4Z4N0D3GX0H9JEXH70EBC2T3KY7HC0TJB0Z60D2H781RXR6AG; + END IF; + +END $$; diff --git a/src/backenddb/procedures.sql.in b/src/backenddb/procedures.sql.in @@ -40,6 +40,8 @@ SET search_path TO merchant; #include "pg_uri_escape.sql" #include "pg_merchant_kyc_trigger.sql" #include "pg_merchant_send_kyc_notification.sql" +#include "pg_activate_account.sql" +#include "pg_inactivate_account.sql" DROP PROCEDURE IF EXISTS merchant_do_gc; CREATE PROCEDURE merchant_do_gc(in_now INT8) diff --git a/src/include/taler/taler_merchantdb_plugin.h b/src/include/taler/taler_merchantdb_plugin.h @@ -2512,14 +2512,21 @@ struct TALER_MERCHANTDB_Plugin * Set an instance's account in our database to "active". * * @param cls closure - * @param merchant_id merchant backend instance ID - * @param h_wire hash of the wire account to set to active + * @param account_details details of the account to actiate + * @param[out] h_wire set to the hash of the salted payto URI + * @param[out] salt set to salt used for computing @a h_wire + * @param[out] not_found set to true if the instance was not found + * @param[out] conflict set to true if a conflicting account is active * @return database result code */ enum GNUNET_DB_QueryStatus - (*activate_account)(void *cls, - const char *merchant_id, - const struct TALER_MerchantWireHashP *h_wire); + (*activate_account)( + void *cls, + const struct TALER_MERCHANTDB_AccountDetails *account_details, + struct TALER_MerchantWireHashP *h_wire, + struct TALER_WireSaltP *salt, + bool *not_found, + bool *conflict); /**