merchant

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

commit 0717d80bce5c48881adc503f80f7ca4e4ad8c5b4
parent ed39875bdd918584cd2f3e48797522dede1ab29e
Author: Florian Dold <dold@taler.net>
Date:   Sat, 29 Aug 2026 20:12:22 +0200

wallet templates: return merchant and compatible exchanges

Issue: https://bugs.taler.net/n/8909

Diffstat:
Msrc/backend/taler-merchant-httpd.c | 2++
Msrc/backend/taler-merchant-httpd_get-config.c | 2+-
Msrc/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c | 602+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h | 7+++++++
Msrc/backend/taler-merchant-httpd_helper.c | 17+++++++++++++++++
Msrc/backend/taler-merchant-httpd_helper.h | 10++++++++++
Msrc/backend/taler-merchant-httpd_post-private-orders.c | 45+--------------------------------------------
Msrc/include/taler/merchant/get-templates-TEMPLATE_ID.h | 62+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/include/taler/taler_merchant_testing_lib.h | 4++++
Msrc/lib/merchant_api_get-config.c | 4++--
Msrc/lib/merchant_api_get-templates-TEMPLATE_ID.c | 228++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/testing/test_merchant_api.c | 21+++++++++++++++++++++
Msrc/testing/testing_api_cmd_wallet_get_template.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 954 insertions(+), 125 deletions(-)

diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -41,6 +41,7 @@ #include "taler-merchant-httpd_post-challenge-ID.h" #include "taler-merchant-httpd_get-orders-ORDER_ID.h" #include "taler-merchant-httpd_get-sessions-SESSION_ID.h" +#include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h" #include "taler-merchant-httpd_get-exchanges.h" #include "taler-merchant-httpd_get-webui.h" #include "taler-merchant-httpd_get-terms.h" @@ -241,6 +242,7 @@ do_shutdown (void *cls) TMH_force_kac_resume (); TMH_force_orders_resume (); TMH_force_get_sessions_ID_resume (); + TMH_force_get_templates_ID_resume (); TMH_force_get_orders_resume_typst (); TMH_force_ac_resume (); TMH_force_pc_resume (); 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 "40:0:28" +#define MERCHANT_PROTOCOL_VERSION "41:0:29" /** diff --git a/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.c @@ -20,6 +20,7 @@ */ #include "platform.h" #include "taler-merchant-httpd_get-templates-TEMPLATE_ID.h" +#include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include <taler/taler_json_lib.h> #include "merchant-database/iterate_categories_by_ids.h" @@ -30,6 +31,132 @@ /** + * How long do we wait at most for exchange /keys data? + */ +#define MAX_KEYS_WAIT \ + GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_MILLISECONDS, 2500) + + +struct GetTemplateContext; + + +/** + * Pending lookup of one exchange's keys. + */ +struct ExchangeLookup +{ + /** + * Kept in a DLL. + */ + struct ExchangeLookup *next; + + /** + * Kept in a DLL. + */ + struct ExchangeLookup *prev; + + /** + * Parent request. + */ + struct GetTemplateContext *gtc; + + /** + * Exchange base URL. + */ + char *url; + + /** + * Pending keys operation. + */ + struct TMH_EXCHANGES_KeysOperation *ko; +}; + + +/** + * Context for a wallet-facing template lookup. + */ +struct GetTemplateContext +{ + /** + * Kept in a DLL while suspended. + */ + struct GetTemplateContext *next; + + /** + * Kept in a DLL while suspended. + */ + struct GetTemplateContext *prev; + + /** + * Request handler context. + */ + struct TMH_HandlerContext *hc; + + /** + * Connection handling this request. + */ + struct MHD_Connection *connection; + + /** + * Pending exchange lookups. + */ + struct ExchangeLookup *lookup_head; + + /** + * Pending exchange lookups. + */ + struct ExchangeLookup *lookup_tail; + + /** + * Task that limits exchange lookup latency. + */ + struct GNUNET_SCHEDULER_Task *timeout_task; + + /** + * Wallet-facing template contract. + */ + json_t *template_contract; + + /** + * Editable defaults, NULL if absent. + */ + json_t *editable_defaults; + + /** + * Merchant information snapshot. + */ + json_t *merchant; + + /** + * Compatible exchange candidates found so far. + */ + json_t *exchange_candidates; + + /** + * Merchant public key snapshot. + */ + struct TALER_MerchantPublicKeyP merchant_pub; + + /** + * #GNUNET_YES while suspended, #GNUNET_NO otherwise, and + * #GNUNET_SYSERR when resumed for shutdown. + */ + enum GNUNET_GenericReturnValue suspended; +}; + + +/** + * Head of suspended template requests. + */ +static struct GetTemplateContext *gtc_head; + +/** + * Tail of suspended template requests. + */ +static struct GetTemplateContext *gtc_tail; + + +/** * Determine the currency the client must pay in, if the template * requires a particular one without fixing the amount. * @@ -55,6 +182,301 @@ get_required_currency (const json_t *template_contract) /** + * Test whether @a method is already in @a methods. + * + * @param methods JSON array of strings + * @param method method to find + * @return true if present + */ +static bool +wire_method_present (const json_t *methods, + const char *method) +{ + const json_t *entry; + size_t index; + + json_array_foreach (methods, index, entry) + { + if (0 == strcasecmp (json_string_value (entry), + method)) + return true; + } + return false; +} + + +/** + * Test whether an exchange has an account using @a wire_method. + * + * @param keys exchange keys + * @param wire_method wire method to find + * @return true if the method is supported + */ +static bool +exchange_has_wire_method (const struct TALER_EXCHANGE_Keys *keys, + const char *wire_method) +{ + for (unsigned int i = 0; i<keys->accounts_len; i++) + { + struct TALER_FullPayto full_payto = keys->accounts[i].fpayto_uri; + char *method; + bool matches; + + method = TALER_payto_get_method (full_payto.full_payto); + matches = (0 == strcasecmp (method, + wire_method)); + GNUNET_free (method); + if (matches) + return true; + } + return false; +} + + +/** + * Add an exchange candidate based on current key data. + * + * @param gtc request context + * @param url exchange base URL + * @param keys exchange keys + * @param exchange exchange handle + */ +static void +add_exchange_candidate (struct GetTemplateContext *gtc, + const char *url, + const struct TALER_EXCHANGE_Keys *keys, + const struct TMH_Exchange *exchange) +{ + const char *currency; + json_t *wire_methods; + + if (NULL == keys) + return; + currency = TMH_EXCHANGES_get_currency (exchange); + if ( (NULL == currency) || + (0 != strcasecmp (currency, + keys->currency)) ) + return; + wire_methods = json_array (); + GNUNET_assert (NULL != wire_methods); + for (const struct TMH_WireMethod *wm = gtc->hc->instance->wm_head; + NULL != wm; + wm = wm->next) + { + struct TALER_NormalizedPayto np; + bool allowed; + + if (! wm->active) + continue; + if (! exchange_has_wire_method (keys, + wm->wire_method)) + continue; + np = TALER_payto_normalize (wm->payto_uri); + allowed = TALER_EXCHANGE_keys_test_account_allowed (keys, + false, + np); + GNUNET_free (np.normalized_payto); + if ( (! allowed) || + wire_method_present (wire_methods, + wm->wire_method) ) + continue; + GNUNET_assert (0 == + json_array_append_new (wire_methods, + json_string (wm->wire_method))); + } + if (0 == json_array_size (wire_methods)) + { + json_decref (wire_methods); + return; + } + GNUNET_assert (0 == + json_array_append_new ( + gtc->exchange_candidates, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("base_url", + url), + GNUNET_JSON_pack_string ("currency", + currency), + GNUNET_JSON_pack_data_auto ( + "master_pub", + TMH_EXCHANGES_get_master_pub (exchange)), + GNUNET_JSON_pack_array_steal ("wire_methods", + wire_methods)))); +} + + +/** + * Resume a template request after exchange discovery. + * + * @param gtc request to resume + */ +static void +resume_template_request (struct GetTemplateContext *gtc) +{ + if (GNUNET_YES != gtc->suspended) + return; + if (NULL != gtc->timeout_task) + { + GNUNET_SCHEDULER_cancel (gtc->timeout_task); + gtc->timeout_task = NULL; + } + GNUNET_CONTAINER_DLL_remove (gtc_head, + gtc_tail, + gtc); + gtc->suspended = GNUNET_NO; + MHD_resume_connection (gtc->connection); + TALER_MHD_daemon_trigger (); +} + + +/** + * Exchange keys lookup completed. + * + * @param cls a `struct ExchangeLookup *` + * @param keys exchange keys, NULL on failure + * @param exchange exchange handle + */ +static void +exchange_keys_cb (void *cls, + struct TALER_EXCHANGE_Keys *keys, + struct TMH_Exchange *exchange) +{ + struct ExchangeLookup *lookup = cls; + struct GetTemplateContext *gtc = lookup->gtc; + + lookup->ko = NULL; + GNUNET_CONTAINER_DLL_remove (gtc->lookup_head, + gtc->lookup_tail, + lookup); + add_exchange_candidate (gtc, + lookup->url, + keys, + exchange); + GNUNET_free (lookup->url); + GNUNET_free (lookup); + if (NULL == gtc->lookup_head) + resume_template_request (gtc); +} + + +/** + * Start key discovery for one trusted exchange. + * + * @param cls a `struct GetTemplateContext *` + * @param url exchange base URL + * @param exchange exchange handle, unused + */ +static void +start_exchange_lookup (void *cls, + const char *url, + const struct TMH_Exchange *exchange) +{ + struct GetTemplateContext *gtc = cls; + struct ExchangeLookup *lookup; + + (void) exchange; + lookup = GNUNET_new (struct ExchangeLookup); + lookup->gtc = gtc; + lookup->url = GNUNET_strdup (url); + lookup->ko = TMH_EXCHANGES_keys4exchange (url, + false, + &exchange_keys_cb, + lookup); + if (NULL == lookup->ko) + { + GNUNET_free (lookup->url); + GNUNET_free (lookup); + return; + } + GNUNET_CONTAINER_DLL_insert (gtc->lookup_head, + gtc->lookup_tail, + lookup); +} + + +/** + * Stop waiting for exchange keys and return the partial candidate list. + * + * @param cls a `struct GetTemplateContext *` + */ +static void +exchange_lookup_timeout (void *cls) +{ + struct GetTemplateContext *gtc = cls; + + gtc->timeout_task = NULL; + while (NULL != gtc->lookup_head) + { + struct ExchangeLookup *lookup = gtc->lookup_head; + + GNUNET_CONTAINER_DLL_remove (gtc->lookup_head, + gtc->lookup_tail, + lookup); + TMH_EXCHANGES_keys4exchange_cancel (lookup->ko); + GNUNET_free (lookup->url); + GNUNET_free (lookup); + } + resume_template_request (gtc); +} + + +/** + * Cleanup a template request context. + * + * @param cls a `struct GetTemplateContext *` + */ +static void +get_template_cleanup (void *cls) +{ + struct GetTemplateContext *gtc = cls; + + if (GNUNET_YES == gtc->suspended) + GNUNET_CONTAINER_DLL_remove (gtc_head, + gtc_tail, + gtc); + if (NULL != gtc->timeout_task) + GNUNET_SCHEDULER_cancel (gtc->timeout_task); + while (NULL != gtc->lookup_head) + { + struct ExchangeLookup *lookup = gtc->lookup_head; + + GNUNET_CONTAINER_DLL_remove (gtc->lookup_head, + gtc->lookup_tail, + lookup); + TMH_EXCHANGES_keys4exchange_cancel (lookup->ko); + GNUNET_free (lookup->url); + GNUNET_free (lookup); + } + if (NULL != gtc->template_contract) + json_decref (gtc->template_contract); + if (NULL != gtc->editable_defaults) + json_decref (gtc->editable_defaults); + if (NULL != gtc->merchant) + json_decref (gtc->merchant); + if (NULL != gtc->exchange_candidates) + json_decref (gtc->exchange_candidates); + GNUNET_free (gtc); +} + + +void +TMH_force_get_templates_ID_resume (void) +{ + while (NULL != gtc_head) + { + struct GetTemplateContext *gtc = gtc_head; + + GNUNET_CONTAINER_DLL_remove (gtc_head, + gtc_tail, + gtc); + gtc->suspended = GNUNET_SYSERR; + MHD_resume_connection (gtc->connection); + TALER_MHD_daemon_trigger (); + } +} + + +/** * Context for building inventory template payloads. */ struct InventoryPayloadContext @@ -284,10 +706,10 @@ add_inventory_unit (void *cls, * @param connection HTTP connection * @param mi merchant instance * @param tp template details - * @return MHD result + * @return newly allocated template contract, NULL if an error was queued */ -static enum MHD_Result -handle_get_templates_inventory ( +static json_t * +build_inventory_template_contract ( struct MHD_Connection *connection, const struct TMH_MerchantInstance *mi, const struct TALER_MERCHANTDB_TemplateDetails *tp) @@ -333,11 +755,12 @@ handle_get_templates_inventory ( "Invalid inventory template_contract for field %s\n", err_name); inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( + (void) TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, err_name); + return NULL; } } @@ -397,11 +820,12 @@ handle_get_templates_inventory ( { GNUNET_break (0); inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( + (void) TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "iterate_inventory_products"); + return NULL; } } else if ( (0 < num_product_ids) || @@ -424,11 +848,12 @@ handle_get_templates_inventory ( inventory_payload_cleanup (&ipc); GNUNET_free (product_ids); GNUNET_free (category_ids); - return TALER_MHD_reply_with_error ( + (void) TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "iterate_inventory_products_filtered"); + return NULL; } } GNUNET_free (product_ids); @@ -451,11 +876,12 @@ handle_get_templates_inventory ( { GNUNET_break (0); inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( + (void) TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "iterate_categories_by_ids"); + return NULL; } } @@ -476,11 +902,12 @@ handle_get_templates_inventory ( { GNUNET_break (0); inventory_payload_cleanup (&ipc); - return TALER_MHD_reply_with_error ( + (void) TALER_MHD_reply_with_error ( connection, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_GENERIC_DB_FETCH_FAILED, "iterate_custom_units_by_names"); + return NULL; } } @@ -509,24 +936,8 @@ handle_get_templates_inventory ( json_object_set_new (template_contract, "inventory_payload", inventory_payload)); - { - enum MHD_Result ret; - - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp->editable_defaults)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("required_currency", - get_required_currency ( - tp->template_contract))), - GNUNET_JSON_pack_object_steal ("template_contract", - template_contract)); - inventory_payload_cleanup (&ipc); - return ret; - } + inventory_payload_cleanup (&ipc); + return template_contract; } @@ -536,71 +947,110 @@ TMH_get_templates_ID ( struct MHD_Connection *connection, struct TMH_HandlerContext *hc) { + struct GetTemplateContext *gtc = hc->ctx; struct TMH_MerchantInstance *mi = hc->instance; - struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; - enum GNUNET_DB_QueryStatus qs; + (void) rh; GNUNET_assert (NULL != mi); - qs = TALER_MERCHANTDB_get_template (TMH_db, - mi->settings.id, - hc->infix, - &tp); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "get_template"); - } - if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + if (NULL == gtc) { - return TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_NOT_FOUND, - TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, - hc->infix); - } - { - enum MHD_Result ret; + struct TALER_MERCHANTDB_TemplateDetails tp = { 0 }; + enum GNUNET_DB_QueryStatus qs; + gtc = GNUNET_new (struct GetTemplateContext); + gtc->hc = hc; + gtc->connection = connection; + gtc->exchange_candidates = json_array (); + GNUNET_assert (NULL != gtc->exchange_candidates); + gtc->merchant = TMH_instance_metadata_to_json (mi); + GNUNET_assert (NULL != gtc->merchant); + gtc->merchant_pub = mi->merchant_pub; + hc->ctx = gtc; + hc->cc = &get_template_cleanup; + + qs = TALER_MERCHANTDB_get_template (TMH_db, + mi->settings.id, + hc->infix, + &tp); + if (0 > qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "get_template"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + return TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_NOT_FOUND, + TALER_EC_MERCHANT_GENERIC_TEMPLATE_UNKNOWN, + hc->infix); + } + if (NULL != tp.editable_defaults) + gtc->editable_defaults = json_incref (tp.editable_defaults); switch (TALER_MERCHANT_template_type_from_contract (tp.template_contract)) { case TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART: - ret = handle_get_templates_inventory (connection, - mi, - &tp); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; + gtc->template_contract = build_inventory_template_contract (connection, + mi, + &tp); + break; case TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER: case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: - ret = TALER_MHD_REPLY_JSON_PACK ( - connection, - MHD_HTTP_OK, - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("editable_defaults", - tp.editable_defaults)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("required_currency", - get_required_currency ( - tp.template_contract))), - GNUNET_JSON_pack_object_incref ("template_contract", - tp.template_contract)); - TALER_MERCHANTDB_template_details_free (&tp); - return ret; + gtc->template_contract = json_incref (tp.template_contract); + break; case TALER_MERCHANT_TEMPLATE_TYPE_INVALID: + GNUNET_break_op (0); + (void) TALER_MHD_reply_with_error ( + connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "template_type"); break; } - GNUNET_break_op (0); - ret = TALER_MHD_reply_with_error ( - connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "template_type"); TALER_MERCHANTDB_template_details_free (&tp); - return ret; + if (NULL == gtc->template_contract) + return MHD_YES; + + TMH_exchange_get_trusted (&start_exchange_lookup, + gtc); + if (NULL != gtc->lookup_head) + { + gtc->timeout_task = GNUNET_SCHEDULER_add_delayed ( + MAX_KEYS_WAIT, + &exchange_lookup_timeout, + gtc); + GNUNET_CONTAINER_DLL_insert (gtc_head, + gtc_tail, + gtc); + gtc->suspended = GNUNET_YES; + MHD_suspend_connection (connection); + return MHD_YES; + } } + if (GNUNET_SYSERR == gtc->suspended) + return MHD_NO; + return TALER_MHD_REPLY_JSON_PACK ( + connection, + MHD_HTTP_OK, + GNUNET_JSON_pack_object_incref ("template_contract", + gtc->template_contract), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("editable_defaults", + gtc->editable_defaults)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("required_currency", + get_required_currency ( + gtc->template_contract))), + GNUNET_JSON_pack_object_incref ("merchant", + gtc->merchant), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &gtc->merchant_pub), + GNUNET_JSON_pack_array_incref ("exchange_candidates", + gtc->exchange_candidates)); } diff --git a/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h b/src/backend/taler-merchant-httpd_get-templates-TEMPLATE_ID.h @@ -25,6 +25,13 @@ /** + * Resume all suspended template requests during shutdown. + */ +void +TMH_force_get_templates_ID_resume (void); + + +/** * Handle a GET "/templates/$ID" request. * * @param rh context of the handler diff --git a/src/backend/taler-merchant-httpd_helper.c b/src/backend/taler-merchant-httpd_helper.c @@ -33,6 +33,23 @@ #include "merchant-database/event_notify.h" +json_t * +TMH_instance_metadata_to_json (const struct TMH_MerchantInstance *mi) +{ + const struct TALER_MERCHANTDB_InstanceSettings *settings = &mi->settings; + struct TALER_MERCHANT_MetaData md = { + .name = settings->name, + .website = settings->website, + .email = settings->email, + .logo = settings->logo, + .address = settings->address, + .jurisdiction = settings->jurisdiction + }; + + return TALER_MERCHANT_metadata_to_json (&md); +} + + void TMH_quantity_defaults_from_unit (const struct TMH_MerchantInstance *mi, const char *unit, diff --git a/src/backend/taler-merchant-httpd_helper.h b/src/backend/taler-merchant-httpd_helper.h @@ -29,6 +29,16 @@ #include "taler-merchant-httpd.h" #include "taler/taler_merchant_util.h" + +/** + * Serialize the public merchant metadata of an instance. + * + * @param mi merchant instance + * @return newly allocated JSON object + */ +json_t * +TMH_instance_metadata_to_json (const struct TMH_MerchantInstance *mi); + /** * check @a accounts for well-formedness * diff --git a/src/backend/taler-merchant-httpd_post-private-orders.c b/src/backend/taler-merchant-httpd_post-private-orders.c @@ -2120,53 +2120,10 @@ output_contract_choices (struct OrderContext *oc) static void phase_serialize_order (struct OrderContext *oc) { - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; json_t *merchant; - merchant = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - settings->name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - settings->website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("email", - settings->email)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - settings->logo))); + merchant = TMH_instance_metadata_to_json (oc->hc->instance); GNUNET_assert (NULL != merchant); - { - json_t *loca; - - /* Handle merchant address */ - loca = settings->address; - if (NULL != loca) - { - loca = json_deep_copy (loca); - GNUNET_assert (NULL != loca); - GNUNET_assert (0 == - json_object_set_new (merchant, - "address", - loca)); - } - } - { - json_t *juri; - - /* Handle merchant jurisdiction */ - juri = settings->jurisdiction; - if (NULL != juri) - { - juri = json_deep_copy (juri); - GNUNET_assert (NULL != juri); - GNUNET_assert (0 == - json_object_set_new (merchant, - "jurisdiction", - juri)); - } - } oc->serialize_order.contract = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ( diff --git a/src/include/taler/merchant/get-templates-TEMPLATE_ID.h b/src/include/taler/merchant/get-templates-TEMPLATE_ID.h @@ -23,6 +23,39 @@ #define _TALER_MERCHANT__GET_TEMPLATES_TEMPLATE_ID_H #include <taler/merchant/common.h> +#include <taler/taler_merchant_util.h> + + +/** + * Exchange that may be usable for an order instantiated from a template. + */ +struct TALER_MERCHANT_TemplateExchangeCandidate +{ + /** + * Exchange base URL. + */ + const char *base_url; + + /** + * Currency for which the merchant trusts this exchange. + */ + const char *currency; + + /** + * Exchange master public key. + */ + struct TALER_MasterPublicKeyP master_pub; + + /** + * Number of entries in @e wire_methods. + */ + unsigned int num_wire_methods; + + /** + * Active merchant wire methods compatible with this exchange. + */ + const char **wire_methods; +}; /** @@ -66,10 +99,37 @@ struct TALER_MERCHANT_GetTemplatesResponse /** * Required currency, or NULL if not provided. - * FIXME: currently not provided by the backend. Fix backend! */ const char *required_currency; + /** + * Merchant metadata, or NULL when talking to a backend before + * protocol v41. + */ + const struct TALER_MERCHANT_MetaData *merchant; + + /** + * Current merchant public key, or NULL when talking to a backend + * before protocol v41. + */ + const struct TALER_MerchantPublicKeyP *merchant_pub; + + /** + * True if the optional @e exchange_candidates field was present. + */ + bool exchange_candidates_provided; + + /** + * Number of entries in @e exchange_candidates. + */ + unsigned int num_exchange_candidates; + + /** + * Provisional exchange candidates. Only valid during the callback. + */ + const struct TALER_MERCHANT_TemplateExchangeCandidate + *exchange_candidates; + } ok; } details; diff --git a/src/include/taler/taler_merchant_testing_lib.h b/src/include/taler/taler_merchant_testing_lib.h @@ -545,6 +545,8 @@ TALER_TESTING_cmd_merchant_post_categories ( * @param expected_product_id3 third expected product id (can be NULL) * @param expected_category_id1 first expected category id * @param expected_category_id2 second expected category id + * @param expected_exchange_url exchange candidate URL to require (can be NULL) + * @param expected_wire_method wire method to require for the candidate * @param http_status expected HTTP response code. * @return the command. */ @@ -563,6 +565,8 @@ TALER_TESTING_cmd_merchant_wallet_get_template ( const char *expected_product_id3, uint64_t expected_category_id1, uint64_t expected_category_id2, + const char *expected_exchange_url, + const char *expected_wire_method, unsigned int http_status); 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 40 +#define MERCHANT_PROTOCOL_CURRENT 41 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 16 +#define MERCHANT_PROTOCOL_AGE 17 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_get-templates-TEMPLATE_ID.c b/src/lib/merchant_api_get-templates-TEMPLATE_ID.c @@ -31,6 +31,173 @@ /** + * Maximum number of exchange candidates or wire methods accepted in a reply. + */ +#define MAX_CANDIDATE_ENTRIES 1024 + + +/** + * Free merchant metadata parsed for a callback. + * + * @param merchant metadata to free + */ +static void +free_merchant_metadata (struct TALER_MERCHANT_MetaData *merchant) +{ + GNUNET_free (merchant->name); + GNUNET_free (merchant->website); + GNUNET_free (merchant->email); + GNUNET_free (merchant->logo); + if (NULL != merchant->address) + json_decref (merchant->address); + if (NULL != merchant->jurisdiction) + json_decref (merchant->jurisdiction); +} + + +/** + * Parse merchant metadata. + * + * @param object merchant JSON object + * @param[out] merchant parsed metadata + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_merchant_metadata (const json_t *object, + struct TALER_MERCHANT_MetaData *merchant) +{ + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string_copy ("name", + &merchant->name), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_web_url_copy ("website", + &merchant->website), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("email", + &merchant->email), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("logo", + &merchant->logo), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("address", + &merchant->address), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("jurisdiction", + &merchant->jurisdiction), + NULL), + GNUNET_JSON_spec_end () + }; + + return GNUNET_JSON_parse (object, + spec, + NULL, + NULL); +} + + +/** + * Free parsed exchange candidates. + * + * @param num_candidates number of candidates + * @param candidates candidate array + */ +static void +free_exchange_candidates ( + unsigned int num_candidates, + struct TALER_MERCHANT_TemplateExchangeCandidate *candidates) +{ + for (unsigned int i = 0; i<num_candidates; i++) + GNUNET_free (candidates[i].wire_methods); + GNUNET_free (candidates); +} + + +/** + * Parse exchange candidates from a template response. + * + * @param array candidate JSON array + * @param[out] num_candidates number of parsed entries + * @param[out] candidates parsed entries + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_exchange_candidates ( + const json_t *array, + unsigned int *num_candidates, + struct TALER_MERCHANT_TemplateExchangeCandidate **candidates) +{ + size_t array_size = json_array_size (array); + struct TALER_MERCHANT_TemplateExchangeCandidate *result; + + if ( (array_size > MAX_CANDIDATE_ENTRIES) || + (array_size != (unsigned int) array_size) ) + return GNUNET_SYSERR; + result = GNUNET_new_array ( + array_size, + struct TALER_MERCHANT_TemplateExchangeCandidate); + for (unsigned int i = 0; i<array_size; i++) + { + struct TALER_MERCHANT_TemplateExchangeCandidate *candidate = &result[i]; + const json_t *wire_methods; + size_t num_methods; + const char **methods; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_web_url ("base_url", + &candidate->base_url), + GNUNET_JSON_spec_string ("currency", + &candidate->currency), + GNUNET_JSON_spec_fixed_auto ("master_pub", + &candidate->master_pub), + GNUNET_JSON_spec_array_const ("wire_methods", + &wire_methods), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (array, + i), + spec, + NULL, + NULL)) + goto fail; + num_methods = json_array_size (wire_methods); + if ( (0 == num_methods) || + (num_methods > MAX_CANDIDATE_ENTRIES) || + (num_methods != (unsigned int) num_methods) ) + goto fail; + methods = GNUNET_new_array (num_methods, + const char *); + for (unsigned int j = 0; j<num_methods; j++) + { + const json_t *method = json_array_get (wire_methods, + j); + + if (! json_is_string (method)) + { + GNUNET_free (methods); + goto fail; + } + methods[j] = json_string_value (method); + } + candidate->num_wire_methods = (unsigned int) num_methods; + candidate->wire_methods = methods; + } + *num_candidates = (unsigned int) array_size; + *candidates = result; + return GNUNET_OK; + +fail: + free_exchange_candidates ((unsigned int) array_size, + result); + return GNUNET_SYSERR; +} + + +/** * Handle for a GET /templates/$TEMPLATE_ID operation (wallet-facing). */ struct TALER_MERCHANT_GetTemplatesHandle @@ -87,6 +254,10 @@ handle_get_template_finished (void *cls, { struct TALER_MERCHANT_GetTemplatesHandle *gth = cls; const json_t *json = response; + struct TALER_MERCHANT_MetaData merchant = { 0 }; + struct TALER_MerchantPublicKeyP merchant_pub; + struct TALER_MERCHANT_TemplateExchangeCandidate *candidates = NULL; + unsigned int num_candidates = 0; struct TALER_MERCHANT_GetTemplatesResponse wtgr = { .hr.http_status = (unsigned int) response_code, .hr.reply = json @@ -100,6 +271,11 @@ handle_get_template_finished (void *cls, { case MHD_HTTP_OK: { + const json_t *exchange_candidates = NULL; + const json_t *merchant_object = NULL; + bool exchange_candidates_missing; + bool merchant_missing; + bool merchant_pub_missing; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_object_const ("template_contract", &wtgr.details.ok.template_contract), @@ -108,12 +284,23 @@ handle_get_template_finished (void *cls, "editable_defaults", &wtgr.details.ok.editable_defaults), NULL), - /* FIXME: never actually set by the merchant in the current code! */ GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ( "required_currency", &wtgr.details.ok.required_currency), NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_const ("merchant", + &merchant_object), + &merchant_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &merchant_pub), + &merchant_pub_missing), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("exchange_candidates", + &exchange_candidates), + &exchange_candidates_missing), GNUNET_JSON_spec_end () }; @@ -126,6 +313,42 @@ handle_get_template_finished (void *cls, wtgr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; break; } + { + if (merchant_missing != merchant_pub_missing) + { + wtgr.hr.http_status = 0; + wtgr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + if (! merchant_missing) + { + if (GNUNET_OK != + parse_merchant_metadata (merchant_object, + &merchant)) + { + wtgr.hr.http_status = 0; + wtgr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + wtgr.details.ok.merchant = &merchant; + wtgr.details.ok.merchant_pub = &merchant_pub; + } + } + if (! exchange_candidates_missing) + { + if (GNUNET_OK != + parse_exchange_candidates (exchange_candidates, + &num_candidates, + &candidates)) + { + wtgr.hr.http_status = 0; + wtgr.hr.ec = TALER_EC_GENERIC_INVALID_RESPONSE; + break; + } + wtgr.details.ok.exchange_candidates_provided = true; + wtgr.details.ok.num_exchange_candidates = num_candidates; + wtgr.details.ok.exchange_candidates = candidates; + } break; } case MHD_HTTP_UNAUTHORIZED: @@ -147,6 +370,9 @@ handle_get_template_finished (void *cls, } gth->cb (gth->cb_cls, &wtgr); + free_exchange_candidates (num_candidates, + candidates); + free_merchant_metadata (&merchant); TALER_MERCHANT_get_templates_cancel (gth); } diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -1822,6 +1822,23 @@ run (void *cls, "template-1", MHD_HTTP_OK, "post-templates-t1"), + TALER_TESTING_cmd_merchant_wallet_get_template ( + "wallet-get-template-fixed", + merchant_url, + "template-1", + 0, + NULL, + NULL, + false, + 0, + NULL, + NULL, + NULL, + 0, + 0, + EXCHANGE_URL, + "x-taler-bank", + MHD_HTTP_OK), TALER_TESTING_cmd_merchant_post_templates2 ( "post-templates-pay-forever", merchant_url, @@ -2190,6 +2207,8 @@ run (void *cls, NULL, 0, 0, + EXCHANGE_URL, + "x-taler-bank", MHD_HTTP_OK), TALER_TESTING_cmd_merchant_post_using_templates2 ( "using-templates-inv-one", @@ -2498,6 +2517,8 @@ run (void *cls, "inv-unit-product-3", 1, 2, + EXCHANGE_URL, + "x-taler-bank", MHD_HTTP_OK), TALER_TESTING_cmd_merchant_post_using_templates2 ( "using-templates-inv-cat-prod-unit", diff --git a/src/testing/testing_api_cmd_wallet_get_template.c b/src/testing/testing_api_cmd_wallet_get_template.c @@ -104,6 +104,16 @@ struct WalletGetTemplateState uint64_t expected_category_id2; /** + * Exchange candidate URL expected in the response (optional). + */ + const char *expected_exchange_url; + + /** + * Wire method expected for @e expected_exchange_url. + */ + const char *expected_wire_method; + + /** * Expected HTTP response code. */ unsigned int http_status; @@ -154,11 +164,71 @@ wallet_get_template_cb (struct WalletGetTemplateState *wgs, const json_t *template_contract = tgr->details.ok.template_contract; const json_t *inventory_payload; const json_t *products; + bool expect_inventory; + + if ( (NULL == tgr->details.ok.merchant) || + (NULL == tgr->details.ok.merchant_pub) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing merchant identity in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + if (NULL != wgs->expected_exchange_url) + { + bool found_candidate = false; + + if (! tgr->details.ok.exchange_candidates_provided) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Missing exchange_candidates in wallet template\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + for (unsigned int i = 0; + i<tgr->details.ok.num_exchange_candidates; + i++) + { + const struct TALER_MERCHANT_TemplateExchangeCandidate *candidate + = &tgr->details.ok.exchange_candidates[i]; + bool found_method = (NULL == wgs->expected_wire_method); + + if (0 != strcmp (candidate->base_url, + wgs->expected_exchange_url)) + continue; + if (NULL != wgs->expected_wire_method) + for (unsigned int j = 0; j<candidate->num_wire_methods; j++) + if (0 == strcasecmp (candidate->wire_methods[j], + wgs->expected_wire_method)) + found_method = true; + if (found_method) + { + found_candidate = true; + break; + } + } + if (! found_candidate) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Expected compatible exchange candidate missing\n"); + TALER_TESTING_interpreter_fail (wgs->is); + return; + } + } inventory_payload = json_object_get (template_contract, "inventory_payload"); + expect_inventory = ( (0 < wgs->expected_products_len) || + (NULL != wgs->expected_product_id) || + (NULL != wgs->expected_product_id2) || + (NULL != wgs->expected_product_id3) || + (NULL != wgs->expected_unit_name_short_i18n) || + (0 != wgs->expected_category_id1) || + (0 != wgs->expected_category_id2) ); if (! json_is_object (inventory_payload)) { + if (! expect_inventory) + goto done; GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Missing inventory_payload in wallet template\n"); TALER_TESTING_interpreter_fail (wgs->is); @@ -347,6 +417,7 @@ wallet_get_template_cb (struct WalletGetTemplateState *wgs, } } } +done: TALER_TESTING_interpreter_next (wgs->is); } @@ -421,6 +492,8 @@ TALER_TESTING_cmd_merchant_wallet_get_template ( const char *expected_product_id3, uint64_t expected_category_id1, uint64_t expected_category_id2, + const char *expected_exchange_url, + const char *expected_wire_method, unsigned int http_status) { struct WalletGetTemplateState *wgs; @@ -440,6 +513,8 @@ TALER_TESTING_cmd_merchant_wallet_get_template ( wgs->expected_product_id3 = expected_product_id3; wgs->expected_category_id1 = expected_category_id1; wgs->expected_category_id2 = expected_category_id2; + wgs->expected_exchange_url = expected_exchange_url; + wgs->expected_wire_method = expected_wire_method; wgs->http_status = http_status; { struct TALER_TESTING_Command cmd = {