diff --git a/contrib/gana b/contrib/gana
index b799c63db..313216ee1 160000
--- a/contrib/gana
+++ b/contrib/gana
@@ -1 +1 @@
-Subproject commit b799c63db9beda99e9151e7611a1ac4e810786ab
+Subproject commit 313216ee11fe604d39ea5f1b71b1ad96a60391d9
diff --git a/src/exchange/taler-exchange-httpd_purses_merge.c b/src/exchange/taler-exchange-httpd_purses_merge.c
new file mode 100644
index 000000000..07038cb6a
--- /dev/null
+++ b/src/exchange/taler-exchange-httpd_purses_merge.c
@@ -0,0 +1,455 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2022 Taler Systems SA
+
+ TALER is free software; you can redistribute it and/or modify it under the
+ terms of the GNU Affero General Public License as published by the Free Software
+ Foundation; either version 3, or (at your option) any later version.
+
+ TALER is distributed in the hope that it will be useful, but WITHOUT ANY
+ WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License along with
+ TALER; see the file COPYING. If not, see
+*/
+/**
+ * @file taler-exchange-httpd_purses_merge.c
+ * @brief Handle /purses/$PID/merge requests; parses the POST and JSON and
+ * verifies the reserve signature before handing things off
+ * to the database.
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include
+#include
+#include
+#include
+#include
+#include "taler_json_lib.h"
+#include "taler_mhd_lib.h"
+#include "taler-exchange-httpd_purses_merge.h"
+#include "taler-exchange-httpd_responses.h"
+#include "taler_exchangedb_lib.h"
+#include "taler-exchange-httpd_keys.h"
+
+
+/**
+ * Closure for #merge_transaction.
+ */
+struct PurseMergeContext
+{
+ /**
+ * Public key of the purse we are creating.
+ */
+ const struct TALER_PurseContractPublicKeyP *purse_pub;
+
+ /**
+ * Total amount to be put into the purse.
+ */
+ struct TALER_Amount target_amount;
+
+ /**
+ * Current amount in the purse.
+ */
+ struct TALER_Amount balance;
+
+ /**
+ * When should the purse expire.
+ */
+ struct GNUNET_TIME_Timestamp purse_expiration;
+
+ /**
+ * Our current time.
+ */
+ struct GNUNET_TIME_Timestamp exchange_timestamp;
+
+ /**
+ * Merge key for the purse.
+ */
+ struct TALER_PurseMergePublicKeyP merge_pub;
+
+ /**
+ * Signature of the reservce affiming this request.
+ */
+ struct TALER_ReserveSignatureP reserve_sig;
+
+ /**
+ * Signature of the client affiming the merge.
+ */
+ struct TALER_PurseMergeSignatureP merge_sig;
+
+ /**
+ * Public key of the reserve, as extracted from @e payto_uri.
+ */
+ struct TALER_ReservePublicKeyP reserve_pub;
+
+ /**
+ * Hash of the contract terms of the purse.
+ */
+ struct TALER_PrivateContractHashP h_contract_terms;
+
+ /**
+ * Fees that apply to this operation.
+ */
+ const struct TEH_GlobalFee *gf;
+
+ /**
+ * URI of the account the purse is to be merged into.
+ * Must be of the form 'payto://taler/$EXCHANGE_URL/RESERVE_PUB'.
+ */
+ const char *payto_uri;
+
+ /**
+ * Base URL of the exchange provider.
+ */
+ char *provider_url;
+
+ /**
+ * Minimum age for deposits into this purse.
+ */
+ uint32_t min_age;
+};
+
+
+/**
+ * Send confirmation of purse creation success to client.
+ *
+ * @param connection connection to the client
+ * @param pcc details about the request that succeeded
+ * @return MHD result code
+ */
+static MHD_RESULT
+reply_merge_success (struct MHD_Connection *connection,
+ const struct PurseMergeContext *pcc)
+{
+ struct TALER_ExchangePublicKeyP pub;
+ struct TALER_ExchangeSignatureP sig;
+ enum TALER_ErrorCode ec;
+ struct TALER_Amount merge_amount;
+
+ if (0 <=
+ TALER_amount_cmp (&pcc->balance,
+ &pcc->target_amount))
+ {
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_ACCEPTED,
+ TALER_JSON_pack_amount ("balance",
+ &pcc->balance));
+ }
+ // FIXME: check return value...
+ TALER_amount_subtract (&merge_amount,
+ &pcc->target_amount,
+ &gf->fees.merge);
+ if (TALER_EC_NONE !=
+ (ec = TALER_exchange_online_purse_merged_sign (
+ &TEH_keys_exchange_sign_,
+ pcc->exchange_timestamp,
+ pcc->purse_expiration,
+ &merge_amount,
+ pcc->purse_pub,
+ &pcc->merge_pub,
+ &pcc->h_contract_terms,
+ &pub,
+ &sig)))
+ {
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_ec (connection,
+ ec,
+ NULL);
+ }
+ return TALER_MHD_REPLY_JSON_PACK (
+ connection,
+ MHD_HTTP_OK,
+ TALER_JSON_pack_amount ("merge_amount",
+ &merge_amount),
+ GNUNET_JSON_pack_timestamp ("exchange_timestamp",
+ pcc->exchange_timestamp),
+ GNUNET_JSON_pack_data_auto ("exchange_sig",
+ &sig),
+ GNUNET_JSON_pack_data_auto ("exchange_pub",
+ &pub));
+}
+
+
+/**
+ * Execute database transaction for /purses/$PID/merge. Runs the transaction
+ * logic; IF it returns a non-error code, the transaction logic MUST NOT queue
+ * a MHD response. IF it returns an hard error, the transaction logic MUST
+ * queue a MHD response and set @a mhd_ret. IF it returns the soft error
+ * code, the function MAY be called again to retry and MUST not queue a MHD
+ * response.
+ *
+ * @param cls a `struct PurseMergeContext`
+ * @param connection MHD request context
+ * @param[out] mhd_ret set to MHD status on error
+ * @return transaction status
+ */
+static enum GNUNET_DB_QueryStatus
+merge_transaction (void *cls,
+ struct MHD_Connection *connection,
+ MHD_RESULT *mhd_ret)
+{
+ struct PurseMergeContext *pcc = cls;
+ enum GNUNET_DB_QueryStatus qs;
+ bool in_conflict = true;
+
+ qs = TEH_plugin->do_purse_merge (TEH_plugin->cls,
+ pcc->purse_pub,
+ &pcc->merge_sig,
+ pcc->merge_timestamp,
+ pcc->provider_url,
+ &pcc.reserve_pub);
+ if (qs < 0)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ return qs;
+ TALER_LOG_WARNING (
+ "Failed to store merge purse information in database\n");
+ *mhd_ret =
+ TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "purse merge");
+ return qs;
+ }
+
+
+ qs = TEH_plugin->do_account_merge (TEH_plugin->cls,
+ pcc->purse_pub,
+ &pcc->reserve_pub,
+ &pcc->reserve_sig);
+ if (qs < 0)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ return qs;
+ TALER_LOG_WARNING (
+ "Failed to store account purse information in database\n");
+ *mhd_ret =
+ TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_STORE_FAILED,
+ "account merge");
+ return qs;
+ }
+
+ return qs;
+}
+
+
+MHD_RESULT
+TEH_handler_purses_merge (
+ struct MHD_Connection *connection,
+ const struct TALER_PurseContractPublicKeyP *purse_pub,
+ const json_t *root)
+{
+ struct PurseMergeContext pcc = {
+ .purse_pub = purse_pub,
+ .exchange_timestamp = GNUNET_TIME_timestamp_get ()
+ };
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("payto_uri",
+ &pcc.payt_uri),
+ GNUNET_JSON_spec_fixed_auto ("reserve_sig",
+ &pcc.reserve_sig),
+ GNUNET_JSON_spec_fixed_auto ("merge_sig",
+ &pcc.merge_sig),
+ GNUNET_JSON_spec_timestamp ("merge_timestamp",
+ &pcc.merge_timestamp),
+ GNUNET_JSON_spec_end ()
+ };
+ struct TALER_PurseContractSignatureP purse_sig;
+ enum GNUNET_DB_QueryStatus qs;
+ bool http;
+
+ {
+ enum GNUNET_GenericReturnValue res;
+
+ res = TALER_MHD_parse_json_data (connection,
+ root,
+ spec);
+ if (GNUNET_SYSERR == res)
+ {
+ GNUNET_break (0);
+ return MHD_NO; /* hard failure */
+ }
+ if (GNUNET_NO == res)
+ {
+ GNUNET_break_op (0);
+ return MHD_YES; /* failure */
+ }
+ }
+
+ pcc.gf = TEH_keys_global_fee_by_time (TEH_keys_get_state (),
+ pcc.exchange_timestamp);
+ if (NULL == pcc.gf)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Cannot create purse: global fees not configured!\n");
+ return TALER_MHD_reply_with_error (connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_EXCHANGE_GENERIC_GLOBAL_FEES_MISSING,
+ NULL);
+ }
+ /* Fetch purse details */
+ qs = TEH_plugin->select_purse_request (TEH_plugin->cls,
+ pcc->purse_pub,
+ &pcc->merge_pub,
+ &pcc->purse_expiration,
+ &pcc->h_contract_terms,
+ &pcc->min_age,
+ &pcc->target_amount,
+ &pcc->balance,
+ &purse_sig);
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select purse request");
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "select purse request");
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_EXCHANGE_MERGE_PURSE_NOT_FOUND,
+ NULL);
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* continued below */
+ break;
+ }
+ /* parse 'payto_uri' into pcc.reserve_pub and provider_url */
+ if ( (0 != strncmp (pcc.payto_uri,
+ "payto://taler/",
+ strlen ("payto://taler/"))) &&
+ (0 != strncmp (pcc.payto_uri,
+ "payto://taler+http/",
+ strlen ("payto://taler+http/"))) )
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+
+ http = (0 == strncmp (pcc.payto_uri,
+ "payto://taler+http/",
+ strlen ("payto://taler+http/")));
+
+ {
+ const char *host = &pcc.payto_uri[http
+ ? strlen ("payto://taler+http/")
+ : strlen ("payto://taler/")];
+ const char *slash = strchr (host,
+ '/');
+
+ if (NULL == slash)
+ {
+ GNUNET_break_op (0);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+ GNUNET_asprintf (&pcc.provider_url,
+ "%s://%.*s/",
+ http ? "http" : "https",
+ (int) (slash - host),
+ host);
+ slash++;
+ if (GNUNET_OK !=
+ GNUNET_STRINGS_string_to_data (slash,
+ strlen (slash),
+ &pcc.reserve_pub,
+ sizeof (pcc.reserve_pub)))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "payto_uri");
+ }
+ slash++;
+ }
+ if (0 == strcmp (pcc.provider_url,
+ TEH_base_url))
+ {
+ /* we use NULL to represent 'self' as the provider */
+ GNUNET_free (pcc.provider_url);
+ }
+ /* check signatures */
+ if (GNUNET_OK !=
+ TALER_wallet_purse_merge_verify (
+ pcc.payto_url,
+ pcc.merge_timestamp,
+ &pcc.merge_pub,
+ &pcc.merge_sig))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_MERGE_SIGNATURE,
+ NULL);
+ }
+ if (GNUNET_OK !=
+ TALER_wallet_account_merge_verify (
+ pcc.merge_timestamp,
+ pcc.purse_pub,
+ pcc.purse_expiration,
+ &pcc.h_contract_terms,
+ &pcc.target_amount,
+ pcc.min_age,
+ &pcc.reserve_pub,
+ &pcc.reserve_sig))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (pcc.provider_url);
+ return TALER_MHD_reply_with_error (
+ connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_EXCHANGE_PURSE_MERGE_INVALID_RESERVE_SIGNATURE,
+ NULL);
+ }
+
+ /* execute transaction */
+ {
+ MHD_RESULT mhd_ret;
+
+ if (GNUNET_OK !=
+ TEH_DB_run_transaction (connection,
+ "execute purse merge",
+ TEH_MT_REQUEST_PURSE_MERGE,
+ &mhd_ret,
+ &merge_transaction,
+ &pcc))
+ {
+ GNUNET_free (pcc.provider_url);
+ return mhd_ret;
+ }
+ }
+
+ GNUNET_free (pcc.provider_url);
+ /* generate regular response */
+ return reply_merge_success (connection,
+ &pcc);
+}
+
+
+/* end of taler-exchange-httpd_purses_merge.c */