merchant

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

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:
Msrc/backend/Makefile.am | 3++-
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 368+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Msrc/backenddb/Makefile.am | 1+
Msrc/backenddb/plugin_merchantdb_postgres.c | 3+++
Msrc/include/taler_merchantdb_plugin.h | 8++++++++
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,