commit 9e3928b2983f6cc917d8909de58177339d3d1423
parent facdf0bbab81a9857ab14aec3f6f0a8565f85ed0
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date: Wed, 26 Mar 2025 19:35:09 +0100
main logic is here, updates are needed as well as checks of the makefile
Diffstat:
5 files changed, 365 insertions(+), 18 deletions(-)
diff --git a/src/backend/Makefile.am b/src/backend/Makefile.am
@@ -230,7 +230,8 @@ taler_merchant_httpd_LDADD = \
if HAVE_DONAU
taler_merchant_httpd_LDADD += \
- -ldonau
+ -ldonau \
+ -ldonaujson
taler_merchant_httpd_SOURCES += \
taler-merchant-httpd_private-get-donau-instances.c \
diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c
@@ -47,6 +47,11 @@
#include "taler_merchant_util.h"
#include "taler_merchantdb_plugin.h"
+#ifdef HAVE_DONAU_DONAU_SERVICE_H
+#include <donau/donau_service.h>
+#include <donau/donau_util.h>
+#include <donau/donau_json_lib.h>
+#endif
/**
* How often do we retry the (complex!) database transaction?
@@ -116,7 +121,6 @@ enum PayPhase
*/
PP_PAY_TRANSACTION,
- // FIXME: new (optional) phase for DONAU interaction here.
/**
* Communicate with DONAU to generate a donation receipt from the donor BUDIs.
*/
@@ -683,6 +687,37 @@ struct PayContext
} batch_deposits;
+ #ifdef HAVE_DONAU_DONAU_SERVICE_H
+ struct {
+ /**
+ * The id of the charity as saved on the donau.
+ */
+ uint64_t charity_id;
+
+ /**
+ * Private key of the charity(related to the private key of the merchant).
+ */
+ struct DONAU_CharityPrivateKeyP *charity_priv;
+
+ } charity;
+
+ struct {
+ /**
+ * Receipts for the donations from the Donau.
+ */
+ struct DONAU_BlindedDonationUnitSignature *sigs;
+
+ /**
+ * Number of receipts for the donations from the Donau.
+ */
+ unsigned int num_sigs;
+
+ /**
+ * The donation receipt in json format.
+ */
+ json_t *donau_sigs_json;
+ } donau_receipt;
+ #endif
};
@@ -1719,9 +1754,8 @@ phase_success_response (struct PayContext *pc)
token_sigs = (0 >= pc->validate_tokens.output_tokens_len)
? NULL
: build_token_sigs (pc);
- // FIXME: add signatures obtained from donau to response
- // theoretically, we could just add the json object received from donau
- // aka BlindedDonationReceiptSignatures
+ // FIXME: somehow have to convert from the sigs to the json,
+ // maybe signatures_to_JSON (donau-httpd-batch-issue.c) could be usefull
pay_end (pc,
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
@@ -1732,6 +1766,12 @@ phase_success_response (struct PayContext *pc)
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_steal ("token_sigs",
token_sigs)),
+ #ifdef HAVE_DONAU_DONAU_SERVICE_H
+ // FIXME: Check for the correctness of pack method
+ GNUNET_JSON_pack_allow_null (
+ GNUNET_JSON_pack_array_steal ("donau_sigs",
+ pc->donau_receipt.donau_sigs_json)),
+ #endif
GNUNET_JSON_pack_data_auto ("sig",
&sig)));
GNUNET_free (pos_confirmation);
@@ -1793,15 +1833,244 @@ phase_payment_notification (struct PayContext *pc)
pc->phase = PP_SUCCESS_RESPONSE;
}
+#ifdef HAVE_DONAU_DONAU_SERVICE_H
+
+/**
+ * Parse signatures to JSON.
+ *
+ * @param num_sig number of signatures
+ * @param signatures Blinded donation unit signatures
+ * @param[out] j_signatures JSON object
+ * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if we could not parse
+ * is malformed.
+ */
+static void
+signatures_to_JSON (const size_t num_sig,
+ struct DONAU_BlindedDonationUnitSignature *signatures,
+ json_t *j_signatures)
+{
+ for (size_t i = 0; i < num_sig; i++)
+ {
+ struct DONAU_BlindedDonationUnitSignature *signature = &signatures[i];
+ GNUNET_assert (
+ 0 == json_array_append (
+ j_signatures,
+ GNUNET_JSON_PACK (
+ DONAU_JSON_pack_blinded_donation_unit_sig ("blinded_signature",
+ signature))));
+ }
+}
+
+static void
+merchant_donau_issue_receipt_cb (void *cls,
+ const struct DONAU_BatchIssueResponse *resp)
+{
+ struct PayContext *pc = cls;
+
+ /* Usually, Donau replies asynchronously, so we expect the PayContext
+ * to be suspended. */
+ GNUNET_assert (GNUNET_YES == pc->suspended);
+
+ if (NULL == resp)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Donau response is NULL (internal or parse error)\n");
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR,
+ "Donau response missing");
+ return;
+ }
+
+ /* Extract overall HTTP status and Taler error code from 'resp->hr' */
+ unsigned int http_status = resp->hr.http_status;
+ enum TALER_ErrorCode ec = resp->hr.ec;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Donau responded with status=%u, ec=%u\n",
+ http_status,
+ ec);
+
+ /* If Donau gave 0 => means the reply was malformed or invalid. */
+ if (0 == http_status)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Donau replied with status 0 => invalid or parse error\n");
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR,
+ "Donau parse error");
+ return;
+ }
+
+ /* If Taler error code is non-zero => Donau signaled an error. */
+ if (TALER_EC_NONE != ec)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Donau responded with Taler error code %u\n",
+ ec);
+ resume_pay_with_error (pc,
+ ec,
+ "Donau returned an error");
+ return;
+ }
+
+ /* If the HTTP status is 200 (OK), we can read the union contents. */
+ if (MHD_HTTP_OK == http_status)
+ {
+ pc->donau_receipt.sigs =
+ resp->details.ok.blinded_sigs; /* array of signatures */
+
+ pc->donau_receipt.donau_sigs_json = json_array();
+ if (NULL == pc->donau_receipt.donau_sigs_json)
+ {
+ GNUNET_break_op (0);
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR,
+ "Failed to create Donau sigs JSON array");
+ return;
+ }
+
+ signatures_to_JSON (pc->donau_receipt.num_sigs,
+ pc->donau_receipt.sigs,
+ pc->donau_receipt.donau_sigs_json);
+
+ struct TALER_Amount total_issued = resp->details.ok.issued_amount;
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Donau accepted donation receipts with total_issued=%s\n",
+ TALER_amount2s (&total_issued));
+ }
+ else
+ {
+
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Donau responded with HTTP code %u\n",
+ http_status);
+
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR,
+ "Donau HTTP error (non-OK)");
+ return;
+ }
+
+ /* Everything OK, continue the normal /pay flow */
+ pc->phase = PP_PAYMENT_NOTIFICATION; /* go to next phase */
+ pay_resume (pc);
+}
+
+/**
+ * Parse a bkp encoded in JSON.
+ *
+ * @param[out] bkp where to return the result
+ * @param bkp_key_obj json to parse
+ * @return #GNUNET_OK if all is fine, #GNUNET_SYSERR if @a bkp_key_obj
+ * is malformed.
+ */
+static enum GNUNET_GenericReturnValue
+merchant_parse_json_bkp (struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkp,
+ const json_t *bkp_key_obj)
+{
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_fixed_auto ("h_donation_unit_pub",
+ &bkp->h_donation_unit_pub),
+ DONAU_JSON_spec_blinded_donation_identifier ("blinded_udi",
+ &bkp->blinded_udi),
+ GNUNET_JSON_spec_end ()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (bkp_key_obj,
+ spec,
+ NULL, NULL))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ /* FIXME: Check for duplicate blinded UDIs.*/
+ return GNUNET_OK;
+}
+
+#endif
+
static void
phase_generate_donation_receipt (struct PayContext *pc)
{
- //FIXME: implement
- // basically, we need to call the DONAU probably using the donau/donau_service.c
- // meaning that we can't bypass the usage of the ifdef HAVE_DONAU_DONAU_SERVICE_H
- // bohdan.emotion = SAD;
- pc->phase = PP_PAYMENT_NOTIFICATION;
+ #ifndef HAVE_DONAU_DONAU_SERVICE_H
+ /* If Donau is disabled at compile-time, skip. */
+ pc->phase = PP_PAYMENT_NOTIFICATION;
+ return;
+ #else
+ /* 1) Parse the BUDIs array from JSON into an array of
+ * DONAU_BlindedUniqueDonorIdentifierKeyPair. */
+ const json_t *budikeypairs = pc->parse_wallet_data.donau.budikeypairs;
+ size_t num_bkps = 0;
+ struct DONAU_BlindedUniqueDonorIdentifierKeyPair *bkps = NULL;
+
+ if ((NULL == budikeypairs) || (! json_is_array (budikeypairs)))
+ {
+ /* Missing or invalid budikeypairs array */
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "Missing or invalid 'budikeypairs' array");
+ return;
+ }
+
+ num_bkps = json_array_size (budikeypairs);
+ /* Looks strange, but theoretically must work, at least donau uses it*/
+ pc->donau_receipt.num_sigs = num_bkps;
+ if (0 == num_bkps)
+ {
+ /* Theoretically is part is never reached, how do we behave? */
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "No budikeypairs provided");
+ return;
+ }
+
+ bkps = GNUNET_new_array (num_bkps,
+ struct DONAU_BlindedUniqueDonorIdentifierKeyPair);
+
+ /* Parse each BUDI object */
+ for (size_t i = 0; i < num_bkps; i++)
+ {
+ const json_t *bkp_obj = json_array_get (budikeypairs, i);
+ if (GNUNET_SYSERR == merchant_parse_json_bkp (&bkps[i], bkp_obj))
+ {
+ GNUNET_break_op (0);
+ GNUNET_free (bkps);
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "Failed to parse budikeypairs");
+ return;
+ }
+ }
+
+ /* 2) Call DONAU_charity_issue_receipt() asynchronously. */
+ struct DONAU_BatchIssueReceiptHandle *birh =
+ DONAU_charity_issue_receipt (TMH_curl_ctx,
+ pc->parse_wallet_data.donau.donau_url,
+ pc->charity.charity_priv,
+ pc->charity.charity_id,
+ pc->parse_wallet_data.donau.donation_year,
+ num_bkps,
+ bkps,
+ &merchant_donau_issue_receipt_cb,
+ pc);
+ if (NULL == birh)
+ {
+ /* The request was invalid from the library’s perspective. */
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Failed to create Donau receipt request\n");
+ resume_pay_with_error (pc,
+ TALER_EC_GENERIC_CLIENT_INTERNAL_ERROR,
+ "Donau error");
+ GNUNET_free (bkps);
+ return;
+ }
+
+ /* As we do in the batch_issue for the exchange*/
+ MHD_suspend_connection (pc->connection);
+ pc->suspended = GNUNET_YES;
+ #endif
}
/**
@@ -2434,9 +2703,8 @@ phase_execute_pay_transaction (struct PayContext *pc)
}
}
- // FIXME: insert donau blinded inputs (into DB here!),
- // idempotency: if already exists, no problem!
- // I believe it must be the parse_wallet_data.donau.budikeypairs
+ // REVIEW: insert donau blinded inputs (into DB here!)
+ // REVIEW: fetching the charity private key and ID from the DB
if (NULL != pc->parse_wallet_data.donau.budikeypairs)
{
enum GNUNET_DB_QueryStatus qs;
@@ -2461,8 +2729,43 @@ phase_execute_pay_transaction (struct PayContext *pc)
return;
}
}
+
+ /* Part where we fetch info about the charity*/
+ qs = TMH_db->lookup_order_charity(
+ TMH_db->cls,
+ pc->parse_wallet_data.donau.donau_url,
+ &pc->charity.charity_id,
+ pc->charity.charity_priv);
+
+ if (qs < 0)
+ {
+ TMH_db->rollback (TMH_db->cls);
+
+ /* just fail. */
+ pay_end (pc,
+ TALER_MHD_reply_with_error (
+ pc->connection,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_GENERIC_DB_FETCH_FAILED,
+ "lookup_order_charity"));
+ return;
+ }
+ else if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ /* The given donau_url was not found in merchant_donau_instances
+ or the corresponding merchant_keys for the private key. */
+ TMH_db->rollback (TMH_db->cls);
+ pay_end (pc,
+ TALER_MHD_reply_with_error (
+ pc->connection,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "No matching Donau charity found for the given URL"));
+ return;
+ }
}
+
TMH_notify_order_change (hc->instance,
TMH_OSF_CLAIMED | TMH_OSF_PAID,
pc->check_contract.contract_terms->timestamp,
@@ -2516,7 +2819,7 @@ phase_execute_pay_transaction (struct PayContext *pc)
return;
}
}
- // FIXME: if we have donation receipts, do NEW phase
+ // REVIEW: if we have donation receipts, do NEW phase
// DONAU interaction here, otherwise skip DONAU phase
// and move to payment notification
// aka
@@ -2896,6 +3199,32 @@ handle_output_token(struct PayContext *pc,
return GNUNET_OK;
}
+/**
+ * Handle checks for contract output of type TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT.
+ * For now, this does nothing and simply returns GNUNET_OK.
+ *
+ * @param pc context for the pay request
+ * @param output the contract output describing the donation receipt requirement
+ * @param output_index index of this output in the contract's outputs array
+ * @return GNUNET_OK unconditionally (placeholder)
+ */
+static enum GNUNET_GenericReturnValue
+handle_output_donation_receipt (struct PayContext *pc,
+ const struct TALER_MERCHANT_ContractOutput *output,
+ unsigned int output_index)
+{
+ /* FIXME: Implement this function.
+ * check donau outputs are good choices
+ * (allowed donau, total amount below max, correct year, ...)
+ */
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "handle_output_donation_receipt: ignoring donation output index %u\n",
+ output_index);
+
+ return GNUNET_OK;
+}
+
+
/**
* Validate tokens and token envelopes. First, we check if all tokens listed
@@ -2974,8 +3303,6 @@ phase_validate_tokens (struct PayContext *pc)
const struct TALER_MERCHANT_ContractOutput *output
= &selected->outputs[i];
- // FIXME: check donau outputs are good choices
- // (allowed donau, total amount below max, correct year, ...)
switch(output->type){
case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN:
if (GNUNET_OK !=
@@ -2988,7 +3315,14 @@ phase_validate_tokens (struct PayContext *pc)
}
break;
case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT:
- //FIXME: Add function to process donations
+ if (GNUNET_OK !=
+ handle_output_donation_receipt (pc,
+ output,
+ i))
+ {
+ /* Error is already scheduled from handle_output_donation_receipt. */
+ return;
+ }
continue;
default:
continue;
@@ -3502,7 +3836,7 @@ phase_parse_wallet_data (struct PayContext *pc)
&budikeypairs),
NULL),
/* ---------------------------------------------------------- */
- // FIXME: extend spec for wallet to submit
+ // REVIEW: extend spec for wallet to submit
// - URL of selected donau
// - year
// - BUDIs with blinded donation receipts (donau-key-hash, blinded value)
diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am
@@ -207,6 +207,7 @@ libtaler_plugin_merchantdb_postgres_la_SOURCES = \
if HAVE_DONAU
libtaler_plugin_merchantdb_postgres_la_SOURCES += \
pg_lookup_donau_keys.h pg_lookup_donau_keys.c \
+ pg_lookup_order_charity.h pg_lookup_order_charity.c \
pg_upsert_donau_keys.h pg_upsert_donau_keys.c \
pg_insert_donau_instance.h pg_insert_donau_instance.c \
pg_select_donau_instance.h pg_select_donau_instance.c \
diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c
@@ -163,6 +163,7 @@
#include "pg_select_donau_instance.h"
#include "pg_delete_donau_instance.h"
#include "pg_lookup_donau_keys.h"
+#include "pg_lookup_order_charity.h"
#include "pg_upsert_donau_keys.h"
#endif
@@ -653,6 +654,8 @@ libtaler_plugin_merchantdb_postgres_init (void *cls)
= &TMH_PG_delete_donau_instance;
plugin->lookup_donau_keys
= &TMH_PG_lookup_donau_keys;
+ plugin->lookup_order_charity
+ = &TMH_PG_lookup_order_charity;
plugin->upsert_donau_keys
= &TMH_PG_upsert_donau_keys;
#endif
diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h
@@ -3899,6 +3899,14 @@ struct TALER_MERCHANTDB_Plugin
);
enum GNUNET_DB_QueryStatus
+ (*lookup_order_charity)(
+ void *cls,
+ const char *donau_url,
+ uint64_t *charity_id,
+ struct DONAU_CharityPrivateKeyP *charity_priv
+ );
+
+ enum GNUNET_DB_QueryStatus
(*insert_donau_instance)(
void *cls,
const char *donau_url,