commit 12951e288cba2b5fc647a12871f2ecce604e6bea
parent e4f7102c2ab178322f8d6f81746139f0d2230921
Author: Christian Grothoff <christian@grothoff.org>
Date: Sun, 20 Jul 2025 16:46:03 +0200
attempt to fix #10194
Diffstat:
2 files changed, 1325 insertions(+), 1057 deletions(-)
diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c
@@ -43,7 +43,7 @@
* #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in
* merchant_api_config.c!
*/
-#define MERCHANT_PROTOCOL_VERSION "21:0:0"
+#define MERCHANT_PROTOCOL_VERSION "20:0:8"
/**
diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c
@@ -1,6 +1,6 @@
/*
This file is part of TALER
- (C) 2014-2024 Taler Systems SA
+ (C) 2014-2025 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
@@ -157,6 +157,47 @@ struct RekeyExchange
/**
+ * Data structure where we evaluate the viability of a given
+ * wire method for this order.
+ */
+struct WireMethodCandidate
+{
+ /**
+ * Kept in a DLL.
+ */
+ struct WireMethodCandidate *next;
+
+ /**
+ * Kept in a DLL.
+ */
+ struct WireMethodCandidate *prev;
+
+ /**
+ * The wire method we are evaluating.
+ */
+ const struct TMH_WireMethod *wm;
+
+ /**
+ * List of exchanges to use when we use this wire method.
+ */
+ json_t *exchanges;
+
+ /**
+ * Array of maximum amounts that could be paid over all available exchanges
+ * for this @a wm. Used to determine if this order creation requests exceeds
+ * legal limits.
+ */
+ struct TALER_Amount *total_exchange_limits;
+
+ /**
+ * Length of the @e total_exchange_limits array.
+ */
+ unsigned int num_total_exchange_limits;
+
+};
+
+
+/**
* Information we keep per order we are processing.
*/
struct OrderContext
@@ -232,19 +273,6 @@ struct OrderContext
} parse_request;
-
- /**
- * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase.
- */
- struct
- {
- /**
- * Wire method (and our bank account) we have selected
- * to be included for this order.
- */
- const struct TMH_WireMethod *wm;
- } add_payment_details;
-
/**
* Information set in the ORDER_PHASE_PARSE_ORDER phase.
*/
@@ -431,52 +459,78 @@ struct OrderContext
} merge_inventory;
/**
- * Information set in the ORDER_PHASE_SET_EXCHANGES phase.
+ * Information set in the ORDER_PHASE_ADD_PAYMENT_DETAILS phase.
*/
struct
{
+
/**
- * Array of exchanges we find acceptable for this
- * order.
+ * DLL of wire methods under evaluation.
*/
- json_t *exchanges;
+ struct WireMethodCandidate *wmc_head;
/**
- * Forced requests to /keys to update our exchange
- * information.
+ * DLL of wire methods under evaluation.
*/
- struct RekeyExchange *pending_reload_head;
+ struct WireMethodCandidate *wmc_tail;
/**
- * Forced requests to /keys to update our exchange
- * information.
+ * Array of maximum amounts that appear in the contract choices
+ * per currency.
+ * Determines the maximum amounts that a client could pay for this
+ * order and which we must thus make sure is acceptable for the
+ * selected wire method/account if possible.
*/
- struct RekeyExchange *pending_reload_tail;
+ struct TALER_Amount *max_choice_limits;
/**
- * Did we previously force reloading of /keys from
- * all exchanges? Set to 'true' to prevent us from
- * doing it again (and again...).
+ * Length of the @e max_choice_limits array.
*/
- bool forced_reload;
+ unsigned int num_max_choice_limits;
+
+ /**
+ * Set to true if we may need an exchange. True if any amount is non-zero.
+ */
+ bool need_exchange;
+
+ } add_payment_details;
+
+ /**
+ * Information set in the ORDER_PHASE_SELECT_WIRE_METHOD phase.
+ */
+ struct
+ {
+
+ /**
+ * Array of exchanges we find acceptable for this order and wire method.
+ */
+ json_t *exchanges;
/**
- * Set to true once we are sure that we have at
- * least one good exchange.
+ * Wire method (and our bank account) we have selected
+ * to be included for this order.
*/
- bool exchange_good;
+ const struct TMH_WireMethod *wm;
+
+ } select_wire_method;
+
+ /**
+ * Information set in the ORDER_PHASE_SET_EXCHANGES phase.
+ */
+ struct
+ {
/**
- * Array of maximum amounts that could be paid over all
- * available exchanges. Used to determine if this
- * order creation requests exceeds legal limits.
+ * Forced requests to /keys to update our exchange
+ * information.
*/
- struct TALER_Amount *total_exchange_limits;
+ struct RekeyExchange *pending_reload_head;
/**
- * Length of the @e total_exchange_limits array.
+ * Forced requests to /keys to update our exchange
+ * information.
*/
- unsigned int num_total_exchange_limits;
+ struct RekeyExchange *pending_reload_tail;
/**
* How long do we wait at most until giving up on getting keys?
@@ -489,6 +543,19 @@ struct OrderContext
struct GNUNET_SCHEDULER_Task *wakeup_task;
/**
+ * Did we previously force reloading of /keys from
+ * all exchanges? Set to 'true' to prevent us from
+ * doing it again (and again...).
+ */
+ bool forced_reload;
+
+ /**
+ * Set to true once we have attempted to load exchanges
+ * for the first time.
+ */
+ bool exchanges_tried;
+
+ /**
* Details depending on the contract version.
*/
union
@@ -638,6 +705,7 @@ struct OrderContext
ORDER_PHASE_MERGE_INVENTORY,
ORDER_PHASE_ADD_PAYMENT_DETAILS,
ORDER_PHASE_SET_EXCHANGES,
+ ORDER_PHASE_SELECT_WIRE_METHOD,
ORDER_PHASE_SET_MAX_FEE,
ORDER_PHASE_SERIALIZE_ORDER,
ORDER_PHASE_SALT_FORGETTABLE,
@@ -687,6 +755,66 @@ TMH_force_orders_resume ()
/**
+ * Add the given @a val to the @a array. Adds the
+ * amount to a given entry in @a array if one with the same
+ * currency exists, otherwise extends the @a array.
+ *
+ * @param[in,out] array pointer to array of amounts
+ * @param[in,out] array_len length of @a array
+ * @param val amount to add
+ * @param cap cap for the sums to enforce, can be NULL
+ */
+static void
+add_to_currency_vector (struct TALER_Amount **array,
+ unsigned int *array_len,
+ const struct TALER_Amount *val,
+ const struct TALER_Amount *cap)
+{
+ for (unsigned int i = 0; i<*array_len; i++)
+ {
+ struct TALER_Amount *ai = &(*array)[i];
+
+ if (GNUNET_OK ==
+ TALER_amount_cmp_currency (ai,
+ val))
+ {
+ enum TALER_AmountArithmeticResult aar;
+
+ aar = TALER_amount_add (ai,
+ ai,
+ val);
+ /* If we have a cap, we tolerate the overflow */
+ GNUNET_assert ( (aar >= 0) ||
+ ( (TALER_AAR_INVALID_RESULT_OVERFLOW == aar) &&
+ (NULL != cap) ) );
+ if (TALER_AAR_INVALID_RESULT_OVERFLOW == aar)
+ {
+ *ai = *cap;
+ }
+ else if (NULL != cap)
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_min (ai,
+ ai,
+ cap));
+ return;
+ }
+ }
+ GNUNET_array_append (*array,
+ *array_len,
+ *val);
+ if (NULL != cap)
+ {
+ struct TALER_Amount *ai = &(*array)[(*array_len) - 1];
+
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_min (ai,
+ ai,
+ cap));
+ }
+}
+
+
+/**
* Update the phase of @a oc based on @a mret.
*
* @param[in,out] oc order to update phase for
@@ -748,6 +876,26 @@ reply_with_error (struct OrderContext *oc,
/**
+ * Clean up memory used by @a wmc.
+ *
+ * @param[in,out] oc order context the WMC is part of
+ * @param[in] wmc wire method candidate to free
+ */
+static void
+free_wmc (struct OrderContext *oc,
+ struct WireMethodCandidate *wmc)
+{
+ GNUNET_CONTAINER_DLL_remove (oc->add_payment_details.wmc_head,
+ oc->add_payment_details.wmc_tail,
+ wmc);
+ GNUNET_array_grow (wmc->total_exchange_limits,
+ wmc->num_total_exchange_limits,
+ 0);
+ GNUNET_free (wmc);
+}
+
+
+/**
* Clean up memory used by @a cls.
*
* @param[in] cls the `struct OrderContext` to clean up
@@ -758,6 +906,9 @@ clean_order (void *cls)
struct OrderContext *oc = cls;
struct RekeyExchange *rx;
+ while (NULL != oc->add_payment_details.wmc_head)
+ free_wmc (oc,
+ oc->add_payment_details.wmc_head);
while (NULL != (rx = oc->set_exchanges.pending_reload_head))
{
GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head,
@@ -767,17 +918,19 @@ clean_order (void *cls)
GNUNET_free (rx->url);
GNUNET_free (rx);
}
+ GNUNET_array_grow (oc->add_payment_details.max_choice_limits,
+ oc->add_payment_details.num_max_choice_limits,
+ 0);
if (NULL != oc->set_exchanges.wakeup_task)
{
GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task);
oc->set_exchanges.wakeup_task = NULL;
}
- if (NULL != oc->set_exchanges.exchanges)
+ if (NULL != oc->select_wire_method.exchanges)
{
- json_decref (oc->set_exchanges.exchanges);
- oc->set_exchanges.exchanges = NULL;
+ json_decref (oc->select_wire_method.exchanges);
+ oc->select_wire_method.exchanges = NULL;
}
- GNUNET_free (oc->set_exchanges.total_exchange_limits);
switch (oc->parse_order.version)
{
case TALER_MERCHANT_CONTRACT_VERSION_0:
@@ -855,6 +1008,8 @@ clean_order (void *cls)
}
+/* ***************** ORDER_PHASE_EXECUTE_ORDER **************** */
+
/**
* Execute the database transaction to setup the order.
*
@@ -1035,7 +1190,7 @@ execute_transaction (struct OrderContext *oc)
* @param[in,out] oc order context
*/
static void
-execute_order (struct OrderContext *oc)
+phase_execute_order (struct OrderContext *oc)
{
const struct TALER_MERCHANTDB_InstanceSettings *settings =
&oc->hc->instance->settings;
@@ -1229,6 +1384,9 @@ execute_order (struct OrderContext *oc)
}
+/* ***************** ORDER_PHASE_CHECK_CONTRACT **************** */
+
+
/**
* Check that the contract is now well-formed. Upon success, continue
* processing with execute_order().
@@ -1236,7 +1394,7 @@ execute_order (struct OrderContext *oc)
* @param[in,out] oc order context
*/
static void
-check_contract (struct OrderContext *oc)
+phase_check_contract (struct OrderContext *oc)
{
struct TALER_PrivateContractHashP h_control;
@@ -1273,6 +1431,9 @@ check_contract (struct OrderContext *oc)
}
+/* ***************** ORDER_PHASE_SALT_FORGETTABLE **************** */
+
+
/**
* Modify the final contract terms adding salts for
* items that are forgettable.
@@ -1280,7 +1441,7 @@ check_contract (struct OrderContext *oc)
* @param[in,out] oc order context
*/
static void
-salt_forgettable (struct OrderContext *oc)
+phase_salt_forgettable (struct OrderContext *oc)
{
if (GNUNET_OK !=
TALER_JSON_contract_seed_forgettable (oc->parse_request.order,
@@ -1298,6 +1459,8 @@ salt_forgettable (struct OrderContext *oc)
}
+/* ***************** ORDER_PHASE_SERIALIZE_ORDER **************** */
+
/**
* Get rounded time interval. @a start is calculated by rounding
* @a ts down to the nearest multiple of @a precision.
@@ -1959,7 +2122,7 @@ output_contract_choices (struct OrderContext *oc)
* @param[in,out] oc order context
*/
static void
-serialize_order (struct OrderContext *oc)
+phase_serialize_order (struct OrderContext *oc)
{
const struct TALER_MERCHANTDB_InstanceSettings *settings =
&oc->hc->instance->settings;
@@ -2037,9 +2200,9 @@ serialize_order (struct OrderContext *oc)
GNUNET_JSON_pack_array_incref ("products",
oc->merge_inventory.products),
GNUNET_JSON_pack_data_auto ("h_wire",
- &oc->add_payment_details.wm->h_wire),
+ &oc->select_wire_method.wm->h_wire),
GNUNET_JSON_pack_string ("wire_method",
- oc->add_payment_details.wm->wire_method),
+ oc->select_wire_method.wm->wire_method),
GNUNET_JSON_pack_string ("order_id",
oc->parse_order.order_id),
GNUNET_JSON_pack_timestamp ("timestamp",
@@ -2062,7 +2225,7 @@ serialize_order (struct OrderContext *oc)
GNUNET_JSON_pack_data_auto ("merchant_pub",
&oc->hc->instance->merchant_pub),
GNUNET_JSON_pack_array_incref ("exchanges",
- oc->set_exchanges.exchanges),
+ oc->select_wire_method.exchanges),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref ("extra",
(json_t *) oc->parse_order.extra))
@@ -2129,6 +2292,9 @@ serialize_order (struct OrderContext *oc)
}
+/* ***************** ORDER_PHASE_SET_MAX_FEE **************** */
+
+
/**
* Set @a max_fee in @a oc based on @a max_stefan_fee value if not overridden
* by @a client_fee. If neither is set, set the fee to zero using currency
@@ -2178,7 +2344,7 @@ compute_fee (struct OrderContext *oc,
* @param[in,out] oc order context
*/
static void
-set_max_fee (struct OrderContext *oc)
+phase_set_max_fee (struct OrderContext *oc)
{
switch (oc->parse_order.version)
{
@@ -2210,6 +2376,159 @@ set_max_fee (struct OrderContext *oc)
}
+/* ***************** ORDER_PHASE_SELECT_WIRE_METHOD **************** */
+
+/**
+ * Check that the @a brutto amount is at or below the
+ * limits we have for the respective wire method candidate.
+ *
+ * @param wmc wire method candidate to check
+ * @param brutto amount to check
+ * @param true if the amount is OK, false if it is too high
+ */
+static bool
+check_limits (struct WireMethodCandidate *wmc,
+ const struct TALER_Amount *brutto)
+{
+ for (unsigned int i = 0; i<wmc->num_total_exchange_limits; i++)
+ {
+ const struct TALER_Amount *total_exchange_limit
+ = &wmc->total_exchange_limits[i];
+
+ if (GNUNET_OK !=
+ TALER_amount_cmp_currency (brutto,
+ total_exchange_limit))
+ continue;
+ if (1 !=
+ TALER_amount_cmp (brutto,
+ total_exchange_limit))
+ return true;
+ }
+ return false;
+}
+
+
+/**
+ * Phase to select a wire method that will be acceptable for the order.
+ * If none is "perfect" (allows all choices), might jump back to the
+ * previous phase to force "/keys" downloads to see if that helps.
+ *
+ * @param[in,out] oc order context
+ */
+static void
+phase_select_wire_method (struct OrderContext *oc)
+{
+ const struct TALER_Amount *ea;
+ struct WireMethodCandidate *best = NULL;
+ unsigned int max_choices = 0;
+ unsigned int want_choices;
+
+ for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head;
+ NULL != wmc;
+ wmc = wmc->next)
+ {
+ unsigned int num_choices = 0;
+
+ switch (oc->parse_order.version)
+ {
+ case TALER_MERCHANT_CONTRACT_VERSION_0:
+ want_choices = 1;
+ ea = &oc->parse_order.details.v0.brutto;
+ if (TALER_amount_is_zero (ea) ||
+ check_limits (wmc,
+ ea))
+ num_choices++;
+ break;
+ case TALER_MERCHANT_CONTRACT_VERSION_1:
+ want_choices = oc->parse_choices.choices_len;
+ for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++)
+ {
+ ea = &oc->parse_choices.choices[i].amount;
+ if (TALER_amount_is_zero (ea) ||
+ check_limits (wmc,
+ ea))
+ num_choices++;
+ }
+ break;
+ default:
+ GNUNET_assert (0);
+ }
+ if (num_choices > max_choices)
+ {
+ best = wmc;
+ max_choices = num_choices;
+ }
+ }
+
+ if ( (want_choices > max_choices) &&
+ (! oc->set_exchanges.forced_reload) )
+ {
+ /* Not all choices in the contract can work with these
+ exchanges, try again with forcing /keys download */
+ for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head;
+ NULL != wmc;
+ wmc = wmc->next)
+ {
+ json_array_clear (wmc->exchanges);
+ GNUNET_array_grow (wmc->total_exchange_limits,
+ wmc->num_total_exchange_limits,
+ 0);
+ }
+ oc->phase = ORDER_PHASE_SET_EXCHANGES;
+ return;
+ }
+
+ if ( (NULL == best) &&
+ (NULL != oc->parse_request.payment_target) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Cannot create order: lacking suitable exchanges for payment target `%s'\n",
+ oc->parse_request.payment_target);
+ reply_with_error (
+ oc,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD,
+ oc->parse_request.payment_target);
+ return;
+ }
+
+ if (NULL == best)
+ {
+ /* We actually do not have ANY workable exchange(s) */
+ reply_with_error (
+ oc,
+ MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS,
+ NULL);
+ return;
+ }
+
+ if (want_choices > max_choices)
+ {
+ /* Some choices are unpayable */
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_WARNING,
+ "Creating order, but some choices do not work with the selected wire method\n");
+ }
+ if ( (0 == json_array_size (best->exchanges)) &&
+ (oc->add_payment_details.need_exchange) )
+ {
+ /* We did not find any reasonable exchange */
+ GNUNET_log (
+ GNUNET_ERROR_TYPE_WARNING,
+ "Creating order, but only for choices without payment\n");
+ }
+
+ oc->select_wire_method.wm
+ = best->wm;
+ oc->select_wire_method.exchanges
+ = json_incref (best->exchanges);
+ oc->phase++;
+}
+
+
+/* ***************** ORDER_PHASE_SET_EXCHANGES **************** */
+
/**
* Exchange `/keys` processing is done, resume handling
* the order.
@@ -2220,8 +2539,7 @@ static void
resume_with_keys (struct OrderContext *oc)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Resuming order processing after /keys downloads (now have %u accounts)\n",
- (unsigned int) json_array_size (oc->set_exchanges.exchanges));
+ "Resuming order processing after /keys downloads\n");
GNUNET_assert (GNUNET_YES == oc->suspended);
GNUNET_CONTAINER_DLL_remove (oc_head,
oc_tail,
@@ -2321,10 +2639,12 @@ update_stefan (struct OrderContext *oc,
* too low and we failed to create an order.
*
* @param oc order context
+ * @param wmc wire method candidate to notify for
* @param exchange_url exchange to notify about
*/
static void
notify_kyc_required (const struct OrderContext *oc,
+ const struct WireMethodCandidate *wmc,
const char *exchange_url)
{
struct GNUNET_DB_EventHeaderP es = {
@@ -2335,8 +2655,8 @@ notify_kyc_required (const struct OrderContext *oc,
char *extra;
hws = GNUNET_STRINGS_data_to_string_alloc (
- &oc->add_payment_details.wm->h_wire,
- sizeof (oc->add_payment_details.wm->h_wire));
+ &wmc->wm->h_wire,
+ sizeof (wmc->wm->h_wire));
GNUNET_asprintf (&extra,
"%s %s",
@@ -2352,35 +2672,54 @@ notify_kyc_required (const struct OrderContext *oc,
/**
- * Compute the set of exchanges that would be acceptable
- * for this order.
+ * Checks the limits that apply for this @a exchange and
+ * the @a wmc and if the exchange is acceptable at all, adds it
+ * to the list of exchanges for the @a wmc.
*
- * @param cls our `struct OrderContext`
- * @param url base URL of an exchange (not used)
+ * @param oc context of the order
* @param exchange internal handle for the exchange
- * @param max_needed maximum amount needed in this currency
+ * @param exchange_url base URL of this exchange
+ * @param wmc wire method to evaluate this exchange for
+ * @return true if the exchange is acceptable for the contract
*/
-static void
-get_acceptable (void *cls,
- const char *url,
+static bool
+get_acceptable (struct OrderContext *oc,
const struct TMH_Exchange *exchange,
- const struct TALER_Amount *max_needed)
+ const char *exchange_url,
+ struct WireMethodCandidate *wmc)
{
- struct OrderContext *oc = cls;
+ const struct TALER_Amount *max_needed = NULL;
unsigned int priority = 42; /* make compiler happy */
json_t *j_exchange;
enum GNUNET_GenericReturnValue res;
struct TALER_Amount max_amount;
+ for (unsigned int i = 0; i<oc->add_payment_details.num_max_choice_limits; i++)
+ {
+ struct TALER_Amount *val = &oc->add_payment_details.max_choice_limits[i];
+
+ if (0 == strcasecmp (val->currency,
+ TMH_EXCHANGES_get_currency (exchange)))
+ {
+ max_needed = val;
+ break;
+ }
+ }
+ if (NULL == max_needed)
+ {
+ /* exchange currency not relevant for any of our choices, skip it */
+ return false;
+ }
+
max_amount = *max_needed;
res = TMH_exchange_check_debit (
oc->hc->instance->settings.id,
exchange,
- oc->add_payment_details.wm,
+ wmc->wm,
&max_amount);
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Exchange %s evaluated at %d with max %s\n",
- url,
+ exchange_url,
res,
TALER_amount2s (&max_amount));
if (TALER_amount_is_zero (&max_amount))
@@ -2390,20 +2729,20 @@ get_acceptable (void *cls,
/* Trigger re-checking the current deposit limit when
* paying non-zero amount with zero deposit limit */
notify_kyc_required (oc,
- url);
+ wmc,
+ exchange_url);
}
/* If deposit is impossible, we don't list the
* exchange in the contract terms. */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Exchange %s deposit limit is zero, skipping it\n",
- url);
- return;
+ exchange_url);
+ return false;
}
switch (res)
{
case GNUNET_OK:
priority = 1024; /* high */
- oc->set_exchanges.exchange_good = true;
break;
case GNUNET_NO:
if (oc->set_exchanges.forced_reload)
@@ -2420,47 +2759,12 @@ get_acceptable (void *cls,
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Exchange %s deposit limit is %s, adding it!\n",
- url,
+ exchange_url,
TALER_amount2s (&max_amount));
- {
- bool found = false;
-
- for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++)
- {
- struct TALER_Amount *limit
- = &oc->set_exchanges.total_exchange_limits[i];
- if (GNUNET_OK ==
- TALER_amount_cmp_currency (limit,
- &max_amount))
- {
- GNUNET_assert (0 <=
- TALER_amount_add (limit,
- limit,
- &max_amount));
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_min (limit,
- limit,
- max_needed));
- found = true;
- }
- }
- if (! found)
- {
- struct TALER_Amount limit;
-
- GNUNET_assert (GNUNET_OK ==
- TALER_amount_min (&limit,
- &max_amount,
- max_needed));
- GNUNET_array_append (oc->set_exchanges.total_exchange_limits,
- oc->set_exchanges.num_total_exchange_limits,
- limit);
- }
- }
j_exchange = GNUNET_JSON_PACK (
GNUNET_JSON_pack_string ("url",
- url),
+ exchange_url),
GNUNET_JSON_pack_uint64 ("priority",
priority),
TALER_JSON_pack_amount ("max_contribution",
@@ -2468,9 +2772,16 @@ get_acceptable (void *cls,
GNUNET_JSON_pack_data_auto ("master_pub",
TMH_EXCHANGES_get_master_pub (exchange)));
GNUNET_assert (NULL != j_exchange);
+ /* Add exchange to list of exchanges for this wire method
+ candidate */
GNUNET_assert (0 ==
- json_array_append_new (oc->set_exchanges.exchanges,
+ json_array_append_new (wmc->exchanges,
j_exchange));
+ add_to_currency_vector (&wmc->total_exchange_limits,
+ &wmc->num_total_exchange_limits,
+ &max_amount,
+ max_needed);
+ return true;
}
@@ -2492,6 +2803,7 @@ keys_cb (
struct OrderContext *oc = rx->oc;
const struct TALER_MERCHANTDB_InstanceSettings *settings =
&oc->hc->instance->settings;
+ bool applicable = false;
rx->fo = NULL;
GNUNET_CONTAINER_DLL_remove (oc->set_exchanges.pending_reload_head,
@@ -2502,63 +2814,38 @@ keys_cb (
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Failed to download %skeys\n",
rx->url);
+ goto cleanup;
}
- else
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Got response for %skeys\n",
+ rx->url);
+
+ /* Evaluate the use of this exchange for each wire method candidate */
+ for (unsigned int j = 0; j<keys->accounts_len; j++)
{
- bool currency_ok = false;
- struct TALER_Amount max_needed;
+ struct TALER_FullPayto full_payto = keys->accounts[j].fpayto_uri;
+ char *wire_method = TALER_payto_get_method (full_payto.full_payto);
- switch (oc->parse_order.version)
+ for (struct WireMethodCandidate *wmc = oc->add_payment_details.wmc_head;
+ NULL != wmc;
+ wmc = wmc->next)
{
- case TALER_MERCHANT_CONTRACT_VERSION_0:
- if (0 == strcasecmp (keys->currency,
- oc->parse_order.details.v0.brutto.currency))
+ if (0 == strcmp (wmc->wm->wire_method,
+ wire_method) )
{
- max_needed = oc->parse_order.details.v0.brutto;
- currency_ok = true;
+ applicable |= get_acceptable (oc,
+ exchange,
+ rx->url,
+ wmc);
}
- break;
- case TALER_MERCHANT_CONTRACT_VERSION_1:
- for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++)
- {
- const struct TALER_Amount *amount
- = &oc->parse_choices.choices[i].amount;
-
- if (0 == strcasecmp (keys->currency,
- amount->currency))
- {
- if (currency_ok)
- {
- TALER_amount_max (&max_needed,
- &max_needed,
- amount);
- }
- else
- {
- max_needed = *amount;
- currency_ok = true;
- }
- }
- }
- break;
- default:
- GNUNET_assert (0);
- }
- if ( (currency_ok) &&
- (! TALER_amount_is_zero (&max_needed)) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Got response for %skeys\n",
- rx->url);
- if (settings->use_stefan)
- update_stefan (oc,
- keys);
- get_acceptable (oc,
- rx->url,
- exchange,
- &max_needed);
}
+ GNUNET_free (wire_method);
}
+ if (applicable &&
+ settings->use_stefan)
+ update_stefan (oc,
+ keys);
+cleanup:
GNUNET_free (rx->url);
GNUNET_free (rx);
if (NULL != oc->set_exchanges.pending_reload_head)
@@ -2624,102 +2911,49 @@ wakeup_timeout (void *cls)
/**
- * Check that the @a brutto amount is at or below the exchange
- * limits we have for the respective currency.
- *
- * @param oc order context to check
- * @param brutto amount to check
- * @param true if the amount is OK, false if it is too high
- */
-static bool
-check_exchange_limits (const struct OrderContext *oc,
- struct TALER_Amount *brutto)
-{
- for (unsigned int i = 0; i<oc->set_exchanges.num_total_exchange_limits; i++)
- {
- const struct TALER_Amount *total_exchange_limit
- = &oc->set_exchanges.total_exchange_limits[i];
-
- if (GNUNET_OK !=
- TALER_amount_cmp_currency (brutto,
- total_exchange_limit))
- continue;
- if (1 !=
- TALER_amount_cmp (brutto,
- total_exchange_limit))
- return true;
- }
-
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Cannot create order: %s is above the sum of hard limits from supported exchanges\n",
- TALER_amount2s (brutto));
- return false;
-}
-
-
-/**
- * Set list of acceptable exchanges in @a oc. Upon success, continue
- * processing with set_max_fee().
+ * Set list of acceptable exchanges in @a oc. Upon success, continues
+ * processing with add_payment_details().
*
* @param[in,out] oc order context
* @return true to suspend execution
*/
static bool
-set_exchanges (struct OrderContext *oc)
+phase_set_exchanges (struct OrderContext *oc)
{
- bool need_exchange;
-
if (NULL != oc->set_exchanges.wakeup_task)
{
GNUNET_SCHEDULER_cancel (oc->set_exchanges.wakeup_task);
oc->set_exchanges.wakeup_task = NULL;
}
- switch (oc->parse_order.version)
- {
- case TALER_MERCHANT_CONTRACT_VERSION_0:
- need_exchange = ! TALER_amount_is_zero (
- &oc->parse_order.details.v0.brutto);
- break;
- case TALER_MERCHANT_CONTRACT_VERSION_1:
- need_exchange = false;
- for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++)
- if (! TALER_amount_is_zero (&oc->parse_choices.choices[i].amount))
- {
- need_exchange = true;
- break;
- }
- break;
- default:
- GNUNET_assert (0);
- }
- if (! need_exchange)
+
+ if (! oc->add_payment_details.need_exchange)
{
/* Total amount is zero, so we don't actually need exchanges! */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order total is zero, no need for exchanges\n");
- oc->set_exchanges.exchanges = json_array ();
- GNUNET_assert (NULL != oc->set_exchanges.exchanges);
- oc->phase++;
+ oc->select_wire_method.exchanges = json_array ();
+ GNUNET_assert (NULL != oc->select_wire_method.exchanges);
+ /* Pick first one, doesn't matter as the amount is zero */
+ oc->select_wire_method.wm = oc->hc->instance->wm_head;
+ oc->phase = ORDER_PHASE_SET_MAX_FEE;
return false;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Trying to find exchanges\n");
- if (NULL == oc->set_exchanges.exchanges)
- {
- oc->set_exchanges.keys_timeout
- = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT);
- oc->set_exchanges.exchanges = json_array ();
- GNUNET_assert (NULL != oc->set_exchanges.exchanges);
- TMH_exchange_get_trusted (&get_exchange_keys,
- oc);
- }
- else if (! oc->set_exchanges.exchange_good)
+ if (NULL == oc->set_exchanges.pending_reload_head)
{
- if (! oc->set_exchanges.forced_reload)
+ if (! oc->set_exchanges.exchanges_tried)
+ {
+ oc->set_exchanges.exchanges_tried = true;
+ oc->set_exchanges.keys_timeout
+ = GNUNET_TIME_relative_to_absolute (MAX_KEYS_WAIT);
+ TMH_exchange_get_trusted (&get_exchange_keys,
+ oc);
+ }
+ else if (! oc->set_exchanges.forced_reload)
{
+ /* Try one more time with forcing /keys download */
oc->set_exchanges.forced_reload = true;
- GNUNET_assert (0 ==
- json_array_clear (oc->set_exchanges.exchanges));
TMH_exchange_get_trusted (&get_exchange_keys,
oc);
}
@@ -2754,616 +2988,276 @@ set_exchanges (struct OrderContext *oc)
oc);
return true; /* reloads pending */
}
- if (0 == json_array_size (oc->set_exchanges.exchanges))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Cannot create order: lacking trusted exchanges\n");
- reply_with_error (
- oc,
- MHD_HTTP_CONFLICT,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGES_FOR_WIRE_METHOD,
- oc->add_payment_details.wm->wire_method);
- return false;
- }
+ oc->phase++;
+ return false;
+}
- {
- bool ok;
- struct TALER_Amount ea;
- switch (oc->parse_order.version)
+/* ***************** ORDER_PHASE_ADD_PAYMENT_DETAILS **************** */
+
+/**
+ * Process the @a payment_target and add the details of how the
+ * order could be paid to @a order. On success, continue
+ * processing with add_payment_fees().
+ *
+ * @param[in,out] oc order context
+ */
+static void
+phase_add_payment_details (struct OrderContext *oc)
+{
+ /* First, determine the maximum amounts that could be paid per currency */
+ switch (oc->parse_order.version)
+ {
+ case TALER_MERCHANT_CONTRACT_VERSION_0:
+ GNUNET_array_append (oc->add_payment_details.max_choice_limits,
+ oc->add_payment_details.num_max_choice_limits,
+ oc->parse_order.details.v0.brutto);
+ if (! TALER_amount_is_zero (
+ &oc->parse_order.details.v0.brutto))
{
- case TALER_MERCHANT_CONTRACT_VERSION_0:
- ea = oc->parse_order.details.v0.brutto;
- ok = check_exchange_limits (oc,
- &ea);
- break;
- case TALER_MERCHANT_CONTRACT_VERSION_1:
- ok = true;
- for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++)
+ oc->add_payment_details.need_exchange = true;
+ }
+ break;
+ case TALER_MERCHANT_CONTRACT_VERSION_1:
+ for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++)
+ {
+ const struct TALER_Amount *amount
+ = &oc->parse_choices.choices[i].amount;
+ bool found = false;
+
+ if (! TALER_amount_is_zero (amount))
{
- ea = oc->parse_choices.choices[i].amount;
- if (! check_exchange_limits (oc,
- &ea))
+ oc->add_payment_details.need_exchange = true;
+ }
+ for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits;
+ j++)
+ {
+ struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j];
+ if (GNUNET_YES ==
+ TALER_amount_cmp_currency (mx,
+ amount))
{
- ok = false;
+ TALER_amount_max (mx,
+ mx,
+ amount);
+ found = true;
break;
}
}
- break;
- default:
- GNUNET_assert (0);
+ if (! found)
+ {
+ GNUNET_array_append (oc->add_payment_details.max_choice_limits,
+ oc->add_payment_details.num_max_choice_limits,
+ *amount);
+ }
}
+ break;
+ default:
+ GNUNET_assert (0);
+ }
- if (! ok)
- {
- reply_with_error (
- oc,
- MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS,
- TALER_amount2s (&ea));
- return false;
- }
+ /* Then, create a candidate for each available wire method */
+ for (struct TMH_WireMethod *wm = oc->hc->instance->wm_head;
+ NULL != wm;
+ wm = wm->next)
+ {
+ struct WireMethodCandidate *wmc;
+
+ /* Locate wire method that has a matching payment target */
+ if (! wm->active)
+ continue; /* ignore inactive methods */
+ if ( (NULL != oc->parse_request.payment_target) &&
+ (0 != strcasecmp (oc->parse_request.payment_target,
+ wm->wire_method) ) )
+ continue; /* honor client preference */
+ wmc = GNUNET_new (struct WireMethodCandidate);
+ wmc->wm = wm;
+ wmc->exchanges = json_array ();
+ GNUNET_assert (NULL != wmc->exchanges);
+ GNUNET_CONTAINER_DLL_insert (oc->add_payment_details.wmc_head,
+ oc->add_payment_details.wmc_tail,
+ wmc);
}
- if (! oc->set_exchanges.exchange_good)
+ if (NULL == oc->add_payment_details.wmc_head)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Creating order, but possibly without usable trusted exchanges\n");
+ "No wire method available for instance '%s'\n",
+ oc->hc->instance->settings.id);
+ reply_with_error (oc,
+ MHD_HTTP_NOT_FOUND,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE,
+ oc->parse_request.payment_target);
+ return;
}
+
+ /* next, we'll evaluate available exchanges */
oc->phase++;
- return false;
}
+/* ***************** ORDER_PHASE_MERGE_INVENTORY **************** */
+
+
/**
- * Parse the order field of the request. Upon success, continue
- * processing with parse_choices().
+ * Merge the inventory products into products, querying the
+ * database about the details of those products. Upon success,
+ * continue processing by calling add_payment_details().
*
- * @param[in,out] oc order context
+ * @param[in,out] oc order context to process
*/
static void
-parse_order (struct OrderContext *oc)
+phase_merge_inventory (struct OrderContext *oc)
{
- const struct TALER_MERCHANTDB_InstanceSettings *settings =
- &oc->hc->instance->settings;
- const char *merchant_base_url = NULL;
- uint64_t version = 0;
- const json_t *jmerchant = NULL;
- const char *order_id = NULL;
- struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_uint64 ("version",
- &version),
- NULL),
- GNUNET_JSON_spec_string ("summary",
- &oc->parse_order.summary),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_array_const ("products",
- &oc->parse_order.products),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_object_const ("summary_i18n",
- &oc->parse_order.summary_i18n),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_string ("order_id",
- &order_id),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_string ("fulfillment_message",
- &oc->parse_order.fulfillment_message),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_object_const ("fulfillment_message_i18n",
- &oc->parse_order.fulfillment_message_i18n),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_string ("fulfillment_url",
- &oc->parse_order.fulfillment_url),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_string ("public_reorder_url",
- &oc->parse_order.public_reorder_url),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- TALER_JSON_spec_web_url ("merchant_base_url",
- &merchant_base_url),
- NULL),
- /* For sanity check, this field must NOT be present */
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_object_const ("merchant",
- &jmerchant),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_timestamp ("timestamp",
- &oc->parse_order.timestamp),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_timestamp ("refund_deadline",
- &oc->parse_order.refund_deadline),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_timestamp ("pay_deadline",
- &oc->parse_order.pay_deadline),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
- &oc->parse_order.wire_deadline),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_object_const ("delivery_location",
- &oc->parse_order.delivery_location),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_timestamp ("delivery_date",
- &oc->parse_order.delivery_date),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_uint32 ("minimum_age",
- &oc->parse_order.minimum_age),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_relative_time ("auto_refund",
- &oc->parse_order.auto_refund),
- NULL),
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_object_const ("extra",
- &oc->parse_order.extra),
- NULL),
- GNUNET_JSON_spec_end ()
- };
- enum GNUNET_GenericReturnValue ret;
-
- oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS;
- oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS;
- ret = TALER_MHD_parse_json_data (oc->connection,
- oc->parse_request.order,
- spec);
- if (GNUNET_OK != ret)
- {
- GNUNET_break_op (0);
- finalize_order2 (oc,
- ret);
- return;
- }
- if (NULL != order_id)
+ /**
+ * parse_request.inventory_products => instructions to add products to contract terms
+ * parse_order.products => contains products that are not from the backend-managed inventory.
+ */
+ if (NULL != oc->parse_order.products)
+ oc->merge_inventory.products
+ = json_deep_copy (oc->parse_order.products);
+ else
+ oc->merge_inventory.products
+ = json_array ();
+ /* Populate products from inventory product array and database */
{
- size_t len = strlen (order_id);
-
- for (size_t i = 0; i<len; i++)
+ GNUNET_assert (NULL != oc->merge_inventory.products);
+ for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++)
{
- char c = order_id[i];
+ const struct InventoryProduct *ip
+ = &oc->parse_request.inventory_products[i];
+ struct TALER_MERCHANTDB_ProductDetails pd;
+ enum GNUNET_DB_QueryStatus qs;
+ size_t num_categories = 0;
+ uint64_t *categories = NULL;
- if (! ( ( (c >= 'A') &&
- (c <= 'Z') ) ||
- ( (c >= 'a') &&
- (c <= 'z') ) ||
- ( (c >= '0') &&
- (c <= '9') ) ||
- (c == '-') ||
- (c == '_') ||
- (c == '.') ||
- (c == ':') ) )
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Invalid character `%c' in order ID `%s'\n",
- c,
- order_id);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_CURRENCY_MISMATCH,
- "Invalid character in order_id");
- return;
- }
- }
- }
- switch (version)
- {
- case 0:
- {
- bool no_fee;
- const json_t *choices = NULL;
- struct GNUNET_JSON_Specification specv0[] = {
- TALER_JSON_spec_amount_any (
- "amount",
- &oc->parse_order.details.v0.brutto),
- GNUNET_JSON_spec_mark_optional (
- TALER_JSON_spec_amount_any (
- "max_fee",
- &oc->parse_order.details.v0.max_fee),
- &no_fee),
- /* for sanity check, must be *absent*! */
- GNUNET_JSON_spec_mark_optional (
- GNUNET_JSON_spec_array_const ("choices",
- &choices),
- NULL),
- GNUNET_JSON_spec_end ()
- };
-
- ret = TALER_MHD_parse_json_data (oc->connection,
- oc->parse_request.order,
- specv0);
- if (GNUNET_OK != ret)
- {
- GNUNET_break_op (0);
- finalize_order2 (oc,
- ret);
- return;
- }
- if ( (! no_fee) &&
- (GNUNET_OK !=
- TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto,
- &oc->parse_order.details.v0.max_fee)) )
- {
- GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_CURRENCY_MISMATCH,
- "different currencies used for 'max_fee' and 'amount' currency");
- return;
- }
- if (! TMH_test_exchange_configured_for_currency (
- oc->parse_order.details.v0.brutto.currency))
+ qs = TMH_db->lookup_product (TMH_db->cls,
+ oc->hc->instance->settings.id,
+ ip->product_id,
+ &pd,
+ &num_categories,
+ &categories);
+ if (qs <= 0)
{
- GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
+ enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
+ unsigned int http_status = 0;
+
+ switch (qs)
+ {
+ case GNUNET_DB_STATUS_HARD_ERROR:
+ GNUNET_break (0);
+ http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ ec = TALER_EC_GENERIC_DB_FETCH_FAILED;
+ break;
+ case GNUNET_DB_STATUS_SOFT_ERROR:
+ GNUNET_break (0);
+ http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
+ ec = TALER_EC_GENERIC_DB_SOFT_FAILURE;
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Product %s from order unknown\n",
+ ip->product_id);
+ http_status = MHD_HTTP_NOT_FOUND;
+ ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN;
+ break;
+ case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
+ /* case listed to make compilers happy */
+ GNUNET_assert (0);
+ }
reply_with_error (oc,
- MHD_HTTP_CONFLICT,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY,
- oc->parse_order.details.v0.brutto.currency);
+ http_status,
+ ec,
+ ip->product_id);
return;
}
- if (NULL != choices)
+ GNUNET_free (categories);
+ oc->parse_order.minimum_age
+ = GNUNET_MAX (oc->parse_order.minimum_age,
+ pd.minimum_age);
{
- GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR,
- "choices array must be null for v0 contracts");
- return;
- }
- oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0;
- break;
- }
- case 1:
- {
- struct GNUNET_JSON_Specification specv1[] = {
- GNUNET_JSON_spec_array_const (
- "choices",
- &oc->parse_order.details.v1.choices),
- GNUNET_JSON_spec_end ()
- };
+ json_t *p;
- ret = TALER_MHD_parse_json_data (oc->connection,
- oc->parse_request.order,
- specv1);
- if (GNUNET_OK != ret)
- {
- GNUNET_break_op (0);
- finalize_order2 (oc,
- ret);
- return;
+ p = GNUNET_JSON_PACK (
+ GNUNET_JSON_pack_string ("product_name",
+ pd.product_name),
+ GNUNET_JSON_pack_string ("description",
+ pd.description),
+ GNUNET_JSON_pack_object_steal ("description_i18n",
+ pd.description_i18n),
+ GNUNET_JSON_pack_string ("unit",
+ pd.unit),
+ TALER_JSON_pack_amount ("price",
+ &pd.price),
+ GNUNET_JSON_pack_array_steal ("taxes",
+ pd.taxes),
+ GNUNET_JSON_pack_string ("image",
+ pd.image),
+ GNUNET_JSON_pack_uint64 (
+ "quantity",
+ ip->quantity));
+ GNUNET_assert (NULL != p);
+ GNUNET_assert (0 ==
+ json_array_append_new (oc->merge_inventory.products,
+ p));
}
- oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1;
- break;
+ GNUNET_free (pd.description);
+ GNUNET_free (pd.unit);
+ GNUNET_free (pd.image);
+ json_decref (pd.address);
}
- default:
+ }
+ /* check if final product list is well-formed */
+ if (! TMH_products_array_valid (oc->merge_inventory.products))
+ {
GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
reply_with_error (oc,
MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_VERSION_MALFORMED,
- "invalid version specified in order, supported are null, '0' or '1'");
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "order:products");
return;
}
+ oc->phase++;
+}
- /* Add order_id if it doesn't exist. */
- if (NULL != order_id)
- {
- oc->parse_order.order_id = GNUNET_strdup (order_id);
- }
- else
- {
- char buf[256];
- time_t timer;
- struct tm *tm_info;
- size_t off;
- uint64_t rand;
- char *last;
-
- time (&timer);
- tm_info = localtime (&timer);
- if (NULL == tm_info)
- {
- GNUNET_JSON_parse_free (spec);
- reply_with_error (
- oc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME,
- NULL);
- return;
- }
- off = strftime (buf,
- sizeof (buf) - 1,
- "%Y.%j",
- tm_info);
- /* Check for error state of strftime */
- GNUNET_assert (0 != off);
- buf[off++] = '-';
- rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK,
- UINT64_MAX);
- last = GNUNET_STRINGS_data_to_string (&rand,
- sizeof (uint64_t),
- &buf[off],
- sizeof (buf) - off);
- GNUNET_assert (NULL != last);
- *last = '\0';
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Assigning order ID `%s' server-side\n",
- buf);
+/* ***************** ORDER_PHASE_PARSE_CHOICES **************** */
- oc->parse_order.order_id = GNUNET_strdup (buf);
- GNUNET_assert (NULL != oc->parse_order.order_id);
- }
+/**
+ * Parse contract choices. Upon success, continue
+ * processing with merge_inventory().
+ *
+ * @param[in,out] oc order context
+ */
+static void
+phase_parse_choices (struct OrderContext *oc)
+{
+ const json_t *jchoices;
- /* Patch fulfillment URL with order_id (implements #6467). */
- if (NULL != oc->parse_order.fulfillment_url)
+ switch (oc->parse_order.version)
{
- const char *pos;
+ case TALER_MERCHANT_CONTRACT_VERSION_0:
+ oc->phase++;
+ return;
+ case TALER_MERCHANT_CONTRACT_VERSION_1:
+ /* handle below */
+ break;
+ default:
+ GNUNET_assert (0);
+ }
- pos = strstr (oc->parse_order.fulfillment_url,
- "${ORDER_ID}");
- if (NULL != pos)
- {
- /* replace ${ORDER_ID} with the real order_id */
- char *nurl;
+ jchoices = oc->parse_order.details.v1.choices;
- /* We only allow one placeholder */
- if (strstr (pos + strlen ("${ORDER_ID}"),
- "${ORDER_ID}"))
- {
- GNUNET_break_op (0);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "fulfillment_url");
- return;
- }
-
- GNUNET_asprintf (&nurl,
- "%.*s%s%s",
- /* first output URL until ${ORDER_ID} */
- (int) (pos - oc->parse_order.fulfillment_url),
- oc->parse_order.fulfillment_url,
- /* replace ${ORDER_ID} with the right order_id */
- oc->parse_order.order_id,
- /* append rest of original URL */
- pos + strlen ("${ORDER_ID}"));
-
- oc->parse_order.fulfillment_url = GNUNET_strdup (nurl);
-
- GNUNET_free (nurl);
- }
- }
-
- /* Check soundness of refund deadline, and that a timestamp
- * is actually present. */
- {
- struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
-
- /* Add timestamp if it doesn't exist (or is zero) */
- if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time))
- {
- oc->parse_order.timestamp = now;
- }
-
- /* If no refund_deadline given, set one based on refund_delay. */
- if (GNUNET_TIME_absolute_is_never (
- oc->parse_order.refund_deadline.abs_time))
- {
- if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Refund delay is zero, no refunds are possible for this order\n");
- oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS;
- }
- else
- {
- oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp (
- oc->parse_request.refund_delay);
- }
- }
-
- if ( (! GNUNET_TIME_absolute_is_zero (
- oc->parse_order.delivery_date.abs_time)) &&
- (GNUNET_TIME_absolute_is_past (
- oc->parse_order.delivery_date.abs_time)) )
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST,
- NULL);
- return;
- }
- }
-
- if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) ||
- (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) )
- {
- oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp (
- settings->default_pay_delay);
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Pay deadline was zero (or never), setting to %s\n",
- GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline));
- }
- else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time))
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST,
- NULL);
- return;
- }
- GNUNET_log (GNUNET_ERROR_TYPE_INFO,
- "Pay deadline is %s\n",
- GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline));
- if ( (! GNUNET_TIME_absolute_is_zero (
- oc->parse_order.refund_deadline.abs_time)) &&
- (GNUNET_TIME_absolute_is_past (
- oc->parse_order.refund_deadline.abs_time)) )
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST,
- NULL);
- return;
- }
-
- if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time))
- {
- struct GNUNET_TIME_Timestamp t;
-
- t = GNUNET_TIME_relative_to_timestamp (
- GNUNET_TIME_relative_max (settings->default_wire_transfer_delay,
- oc->parse_request.refund_delay));
- oc->parse_order.wire_deadline = GNUNET_TIME_timestamp_max (
- oc->parse_order.refund_deadline,
- t);
- if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time))
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER,
- "order:wire_transfer_deadline");
- return;
- }
- }
- if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline,
- <,
- oc->parse_order.refund_deadline))
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE,
- "order:wire_transfer_deadline;order:refund_deadline");
- return;
- }
-
- if (NULL != merchant_base_url)
- {
- if (('\0' == *merchant_base_url) ||
- ('/' != merchant_base_url[strlen (merchant_base_url) - 1]))
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
- "merchant_base_url is not valid");
- return;
- }
- oc->parse_order.merchant_base_url
- = GNUNET_strdup (merchant_base_url);
- }
- else
- {
- char *url;
-
- url = make_merchant_base_url (oc->connection,
- settings->id);
- if (NULL == url)
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MISSING,
- "order:merchant_base_url");
- return;
- }
- oc->parse_order.merchant_base_url = url;
- }
-
- if ( (NULL != oc->parse_order.products) &&
- (! TMH_products_array_valid (oc->parse_order.products)) )
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "order.products");
- return;
- }
-
- /* Merchant information must not already be present */
- if (NULL != jmerchant)
- {
- GNUNET_break_op (0);
- reply_with_error (
- oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
- "'merchant' field already set, but must be provided by backend");
- return;
- }
-
- if ( (NULL != oc->parse_order.delivery_location) &&
- (! TMH_location_object_valid (oc->parse_order.delivery_location)) )
+ if (! json_is_array (jchoices))
+ GNUNET_assert (0);
+ if (0 == json_array_size (jchoices))
{
- GNUNET_break_op (0);
reply_with_error (oc,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "delivery_location");
- return;
- }
-
- oc->phase++;
-}
-
-
-/**
- * Parse contract choices. Upon success, continue
- * processing with merge_inventory().
- *
- * @param[in,out] oc order context
- */
-static void
-parse_choices (struct OrderContext *oc)
-{
- const json_t *jchoices;
-
- switch (oc->parse_order.version)
- {
- case TALER_MERCHANT_CONTRACT_VERSION_0:
- oc->phase++;
+ "choices");
return;
- case TALER_MERCHANT_CONTRACT_VERSION_1:
- /* handle below */
- break;
- default:
- GNUNET_assert (0);
}
-
- jchoices = oc->parse_order.details.v1.choices;
-
- if (! json_is_array (jchoices))
- GNUNET_assert (0);
-
GNUNET_array_grow (oc->parse_choices.choices,
oc->parse_choices.choices_len,
json_array_size (jchoices));
@@ -3418,307 +3312,675 @@ parse_choices (struct OrderContext *oc)
TALER_amount_cmp_currency (&choice->amount,
&choice->max_fee)) )
{
- GNUNET_break_op (0);
- GNUNET_JSON_parse_free (spec);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_CURRENCY_MISMATCH,
- "different currencies used for 'max_fee' and 'amount' currency");
- return;
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_CURRENCY_MISMATCH,
+ "different currencies used for 'max_fee' and 'amount' currency");
+ return;
+ }
+
+ if (! TMH_test_exchange_configured_for_currency (
+ choice->amount.currency))
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY,
+ choice->amount.currency);
+ return;
+ }
+
+ if (NULL != jinputs)
+ {
+ const json_t *jinput;
+ size_t idx;
+ json_array_foreach ((json_t *) jinputs, idx, jinput)
+ {
+ struct TALER_MERCHANT_ContractInput input = {
+ .details.token.count = 1
+ };
+
+ if (GNUNET_OK !=
+ TALER_MERCHANT_parse_choice_input ((json_t *) jinput,
+ &input,
+ idx,
+ true))
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "input");
+ return;
+ }
+
+ switch (input.type)
+ {
+ case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID:
+ GNUNET_assert (0);
+ break;
+ case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN:
+ /* Ignore inputs tokens with 'count' field set to 0 */
+ if (0 == input.details.token.count)
+ continue;
+
+ if (GNUNET_OK !=
+ add_input_token_family (oc,
+ input.details.token.token_family_slug))
+
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
+ input.details.token.token_family_slug);
+ return;
+ }
+
+ GNUNET_array_append (choice->inputs,
+ choice->inputs_len,
+ input);
+ continue;
+ }
+ GNUNET_assert (0);
+ }
+ }
+
+ if (NULL != joutputs)
+ {
+ const json_t *joutput;
+ size_t idx;
+ json_array_foreach ((json_t *) joutputs, idx, joutput)
+ {
+ struct TALER_MERCHANT_ContractOutput output = {
+ .details.token.count = 1
+ };
+
+ if (GNUNET_OK !=
+ TALER_MERCHANT_parse_choice_output ((json_t *) joutput,
+ &output,
+ idx,
+ true))
+ {
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "output");
+ return;
+ }
+
+ switch (output.type)
+ {
+ case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID:
+ GNUNET_assert (0);
+ break;
+ case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT:
+ GNUNET_break (0); /* FIXME-#9059: not yet implemented! */
+ break;
+ case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN:
+ /* Ignore inputs tokens with 'count' field set to 0 */
+ if (0 == output.details.token.count)
+ continue;
+
+ if (0 == output.details.token.valid_at.abs_time.abs_value_us)
+ output.details.token.valid_at
+ = GNUNET_TIME_timestamp_get ();
+ if (GNUNET_OK !=
+ add_output_token_family (oc,
+ output.details.token.token_family_slug,
+ output.details.token.valid_at,
+ &output.details.token.key_index))
+
+ {
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
+ output.details.token.token_family_slug);
+ return;
+ }
+
+ GNUNET_array_append (choice->outputs,
+ choice->outputs_len,
+ output);
+ continue;
+ }
+ GNUNET_assert (0);
+ }
+ }
+ }
+ oc->phase++;
+}
+
+
+/* ***************** ORDER_PHASE_PARSE_ORDER **************** */
+
+
+/**
+ * Parse the order field of the request. Upon success, continue
+ * processing with parse_choices().
+ *
+ * @param[in,out] oc order context
+ */
+static void
+phase_parse_order (struct OrderContext *oc)
+{
+ const struct TALER_MERCHANTDB_InstanceSettings *settings =
+ &oc->hc->instance->settings;
+ const char *merchant_base_url = NULL;
+ uint64_t version = 0;
+ const json_t *jmerchant = NULL;
+ const char *order_id = NULL;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint64 ("version",
+ &version),
+ NULL),
+ GNUNET_JSON_spec_string ("summary",
+ &oc->parse_order.summary),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("products",
+ &oc->parse_order.products),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("summary_i18n",
+ &oc->parse_order.summary_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("order_id",
+ &order_id),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("fulfillment_message",
+ &oc->parse_order.fulfillment_message),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("fulfillment_message_i18n",
+ &oc->parse_order.fulfillment_message_i18n),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("fulfillment_url",
+ &oc->parse_order.fulfillment_url),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_string ("public_reorder_url",
+ &oc->parse_order.public_reorder_url),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_web_url ("merchant_base_url",
+ &merchant_base_url),
+ NULL),
+ /* For sanity check, this field must NOT be present */
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("merchant",
+ &jmerchant),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_timestamp ("timestamp",
+ &oc->parse_order.timestamp),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_timestamp ("refund_deadline",
+ &oc->parse_order.refund_deadline),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_timestamp ("pay_deadline",
+ &oc->parse_order.pay_deadline),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
+ &oc->parse_order.wire_deadline),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("delivery_location",
+ &oc->parse_order.delivery_location),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_timestamp ("delivery_date",
+ &oc->parse_order.delivery_date),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_uint32 ("minimum_age",
+ &oc->parse_order.minimum_age),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_relative_time ("auto_refund",
+ &oc->parse_order.auto_refund),
+ NULL),
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_object_const ("extra",
+ &oc->parse_order.extra),
+ NULL),
+ GNUNET_JSON_spec_end ()
+ };
+ enum GNUNET_GenericReturnValue ret;
+
+ oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS;
+ oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS;
+ ret = TALER_MHD_parse_json_data (oc->connection,
+ oc->parse_request.order,
+ spec);
+ if (GNUNET_OK != ret)
+ {
+ GNUNET_break_op (0);
+ finalize_order2 (oc,
+ ret);
+ return;
+ }
+ if (NULL != order_id)
+ {
+ size_t len = strlen (order_id);
+
+ for (size_t i = 0; i<len; i++)
+ {
+ char c = order_id[i];
+
+ if (! ( ( (c >= 'A') &&
+ (c <= 'Z') ) ||
+ ( (c >= 'a') &&
+ (c <= 'z') ) ||
+ ( (c >= '0') &&
+ (c <= '9') ) ||
+ (c == '-') ||
+ (c == '_') ||
+ (c == '.') ||
+ (c == ':') ) )
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Invalid character `%c' in order ID `%s'\n",
+ c,
+ order_id);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_CURRENCY_MISMATCH,
+ "Invalid character in order_id");
+ return;
+ }
+ }
+ }
+ switch (version)
+ {
+ case 0:
+ {
+ bool no_fee;
+ const json_t *choices = NULL;
+ struct GNUNET_JSON_Specification specv0[] = {
+ TALER_JSON_spec_amount_any (
+ "amount",
+ &oc->parse_order.details.v0.brutto),
+ GNUNET_JSON_spec_mark_optional (
+ TALER_JSON_spec_amount_any (
+ "max_fee",
+ &oc->parse_order.details.v0.max_fee),
+ &no_fee),
+ /* for sanity check, must be *absent*! */
+ GNUNET_JSON_spec_mark_optional (
+ GNUNET_JSON_spec_array_const ("choices",
+ &choices),
+ NULL),
+ GNUNET_JSON_spec_end ()
+ };
+
+ ret = TALER_MHD_parse_json_data (oc->connection,
+ oc->parse_request.order,
+ specv0);
+ if (GNUNET_OK != ret)
+ {
+ GNUNET_break_op (0);
+ finalize_order2 (oc,
+ ret);
+ return;
+ }
+ if ( (! no_fee) &&
+ (GNUNET_OK !=
+ TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto,
+ &oc->parse_order.details.v0.max_fee)) )
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_CURRENCY_MISMATCH,
+ "different currencies used for 'max_fee' and 'amount' currency");
+ return;
+ }
+ if (! TMH_test_exchange_configured_for_currency (
+ oc->parse_order.details.v0.brutto.currency))
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_CONFLICT,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY,
+ oc->parse_order.details.v0.brutto.currency);
+ return;
+ }
+ if (NULL != choices)
+ {
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR,
+ "choices array must be null for v0 contracts");
+ return;
+ }
+ oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0;
+ break;
+ }
+ case 1:
+ {
+ struct GNUNET_JSON_Specification specv1[] = {
+ GNUNET_JSON_spec_array_const (
+ "choices",
+ &oc->parse_order.details.v1.choices),
+ GNUNET_JSON_spec_end ()
+ };
+
+ ret = TALER_MHD_parse_json_data (oc->connection,
+ oc->parse_request.order,
+ specv1);
+ if (GNUNET_OK != ret)
+ {
+ GNUNET_break_op (0);
+ finalize_order2 (oc,
+ ret);
+ return;
+ }
+ oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1;
+ break;
}
+ default:
+ GNUNET_break_op (0);
+ GNUNET_JSON_parse_free (spec);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_VERSION_MALFORMED,
+ "invalid version specified in order, supported are null, '0' or '1'");
+ return;
+ }
- if (! TMH_test_exchange_configured_for_currency (
- choice->amount.currency))
+ /* Add order_id if it doesn't exist. */
+ if (NULL != order_id)
+ {
+ oc->parse_order.order_id = GNUNET_strdup (order_id);
+ }
+ else
+ {
+ char buf[256];
+ time_t timer;
+ struct tm *tm_info;
+ size_t off;
+ uint64_t rand;
+ char *last;
+
+ time (&timer);
+ tm_info = localtime (&timer);
+ if (NULL == tm_info)
{
- GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
- reply_with_error (oc,
- MHD_HTTP_CONFLICT,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY,
- choice->amount.currency);
+ reply_with_error (
+ oc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_LOCALTIME,
+ NULL);
return;
}
+ off = strftime (buf,
+ sizeof (buf) - 1,
+ "%Y.%j",
+ tm_info);
+ /* Check for error state of strftime */
+ GNUNET_assert (0 != off);
+ buf[off++] = '-';
+ rand = GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK,
+ UINT64_MAX);
+ last = GNUNET_STRINGS_data_to_string (&rand,
+ sizeof (uint64_t),
+ &buf[off],
+ sizeof (buf) - off);
+ GNUNET_assert (NULL != last);
+ *last = '\0';
- if (NULL != jinputs)
- {
- const json_t *jinput;
- size_t idx;
- json_array_foreach ((json_t *) jinputs, idx, jinput)
- {
- struct TALER_MERCHANT_ContractInput input = {
- .details.token.count = 1
- };
-
- if (GNUNET_OK !=
- TALER_MERCHANT_parse_choice_input ((json_t *) jinput,
- &input,
- idx,
- true))
- {
- GNUNET_break_op (0);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "input");
- return;
- }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Assigning order ID `%s' server-side\n",
+ buf);
- switch (input.type)
- {
- case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID:
- GNUNET_assert (0);
- break;
- case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN:
- /* Ignore inputs tokens with 'count' field set to 0 */
- if (0 == input.details.token.count)
- continue;
+ oc->parse_order.order_id = GNUNET_strdup (buf);
+ GNUNET_assert (NULL != oc->parse_order.order_id);
+ }
- if (GNUNET_OK !=
- add_input_token_family (oc,
- input.details.token.token_family_slug))
+ /* Patch fulfillment URL with order_id (implements #6467). */
+ if (NULL != oc->parse_order.fulfillment_url)
+ {
+ const char *pos;
- {
- GNUNET_break_op (0);
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
- input.details.token.token_family_slug);
- return;
- }
+ pos = strstr (oc->parse_order.fulfillment_url,
+ "${ORDER_ID}");
+ if (NULL != pos)
+ {
+ /* replace ${ORDER_ID} with the real order_id */
+ char *nurl;
- GNUNET_array_append (choice->inputs,
- choice->inputs_len,
- input);
- continue;
- }
- GNUNET_assert (0);
+ /* We only allow one placeholder */
+ if (strstr (pos + strlen ("${ORDER_ID}"),
+ "${ORDER_ID}"))
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "fulfillment_url");
+ return;
}
- }
- if (NULL != joutputs)
- {
- const json_t *joutput;
- size_t idx;
- json_array_foreach ((json_t *) joutputs, idx, joutput)
- {
- struct TALER_MERCHANT_ContractOutput output = {
- .details.token.count = 1
- };
+ GNUNET_asprintf (&nurl,
+ "%.*s%s%s",
+ /* first output URL until ${ORDER_ID} */
+ (int) (pos - oc->parse_order.fulfillment_url),
+ oc->parse_order.fulfillment_url,
+ /* replace ${ORDER_ID} with the right order_id */
+ oc->parse_order.order_id,
+ /* append rest of original URL */
+ pos + strlen ("${ORDER_ID}"));
- if (GNUNET_OK !=
- TALER_MERCHANT_parse_choice_output ((json_t *) joutput,
- &output,
- idx,
- true))
- {
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "output");
- return;
- }
+ oc->parse_order.fulfillment_url = GNUNET_strdup (nurl);
- switch (output.type)
- {
- case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID:
- GNUNET_assert (0);
- break;
- case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT:
- GNUNET_break (0); /* FIXME-#9059: not yet implemented! */
- break;
- case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN:
- /* Ignore inputs tokens with 'count' field set to 0 */
- if (0 == output.details.token.count)
- continue;
+ GNUNET_free (nurl);
+ }
+ }
- if (0 == output.details.token.valid_at.abs_time.abs_value_us)
- output.details.token.valid_at
- = GNUNET_TIME_timestamp_get ();
- if (GNUNET_OK !=
- add_output_token_family (oc,
- output.details.token.token_family_slug,
- output.details.token.valid_at,
- &output.details.token.key_index))
+ /* Check soundness of refund deadline, and that a timestamp
+ * is actually present. */
+ {
+ struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
- {
- reply_with_error (oc,
- MHD_HTTP_BAD_REQUEST,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
- output.details.token.token_family_slug);
- return;
- }
+ /* Add timestamp if it doesn't exist (or is zero) */
+ if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time))
+ {
+ oc->parse_order.timestamp = now;
+ }
- GNUNET_array_append (choice->outputs,
- choice->outputs_len,
- output);
- continue;
- }
- GNUNET_assert (0);
+ /* If no refund_deadline given, set one based on refund_delay. */
+ if (GNUNET_TIME_absolute_is_never (
+ oc->parse_order.refund_deadline.abs_time))
+ {
+ if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay))
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Refund delay is zero, no refunds are possible for this order\n");
+ oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS;
+ }
+ else
+ {
+ oc->parse_order.refund_deadline = GNUNET_TIME_relative_to_timestamp (
+ oc->parse_request.refund_delay);
}
}
+
+ if ( (! GNUNET_TIME_absolute_is_zero (
+ oc->parse_order.delivery_date.abs_time)) &&
+ (GNUNET_TIME_absolute_is_past (
+ oc->parse_order.delivery_date.abs_time)) )
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_DELIVERY_DATE_IN_PAST,
+ NULL);
+ return;
+ }
}
- oc->phase++;
-}
-
-/**
- * Process the @a payment_target and add the details of how the
- * order could be paid to @a order. On success, continue
- * processing with set_exchanges().
- *
- * @param[in,out] oc order context
- */
-static void
-add_payment_details (struct OrderContext *oc)
-{
- struct TMH_WireMethod *wm;
-
- wm = oc->hc->instance->wm_head;
- /* Locate wire method that has a matching payment target */
- while ( (NULL != wm) &&
- ( (! wm->active) ||
- ( (NULL != oc->parse_request.payment_target) &&
- (0 != strcasecmp (oc->parse_request.payment_target,
- wm->wire_method) ) ) ) )
- wm = wm->next;
- if (NULL == wm)
+ if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) ||
+ (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) )
{
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "No wire method available for instance '%s'\n",
- oc->hc->instance->settings.id);
- reply_with_error (oc,
- MHD_HTTP_NOT_FOUND,
- TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_INSTANCE_CONFIGURATION_LACKS_WIRE,
- oc->parse_request.payment_target);
+ oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp (
+ settings->default_pay_delay);
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Pay deadline was zero (or never), setting to %s\n",
+ GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline));
+ }
+ else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time))
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PAY_DEADLINE_IN_PAST,
+ NULL);
+ return;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Pay deadline is %s\n",
+ GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline));
+ if ( (! GNUNET_TIME_absolute_is_zero (
+ oc->parse_order.refund_deadline.abs_time)) &&
+ (GNUNET_TIME_absolute_is_past (
+ oc->parse_order.refund_deadline.abs_time)) )
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_DEADLINE_IN_PAST,
+ NULL);
return;
}
- oc->add_payment_details.wm = wm;
- oc->phase++;
-}
+ if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time))
+ {
+ struct GNUNET_TIME_Timestamp t;
-/**
- * Merge the inventory products into products, querying the
- * database about the details of those products. Upon success,
- * continue processing by calling add_payment_details().
- *
- * @param[in,out] oc order context to process
- */
-static void
-merge_inventory (struct OrderContext *oc)
-{
- /**
- * parse_request.inventory_products => instructions to add products to contract terms
- * parse_order.products => contains products that are not from the backend-managed inventory.
- */
- if (NULL != oc->parse_order.products)
- oc->merge_inventory.products
- = json_deep_copy (oc->parse_order.products);
- else
- oc->merge_inventory.products
- = json_array ();
- /* Populate products from inventory product array and database */
+ t = GNUNET_TIME_relative_to_timestamp (
+ GNUNET_TIME_relative_max (settings->default_wire_transfer_delay,
+ oc->parse_request.refund_delay));
+ oc->parse_order.wire_deadline = GNUNET_TIME_timestamp_max (
+ oc->parse_order.refund_deadline,
+ t);
+ if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time))
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_WIRE_DEADLINE_IS_NEVER,
+ "order:wire_transfer_deadline");
+ return;
+ }
+ }
+ if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline,
+ <,
+ oc->parse_order.refund_deadline))
{
- GNUNET_assert (NULL != oc->merge_inventory.products);
- for (unsigned int i = 0; i<oc->parse_request.inventory_products_length; i++)
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_REFUND_AFTER_WIRE_DEADLINE,
+ "order:wire_transfer_deadline;order:refund_deadline");
+ return;
+ }
+
+ if (NULL != merchant_base_url)
+ {
+ if (('\0' == *merchant_base_url) ||
+ ('/' != merchant_base_url[strlen (merchant_base_url) - 1]))
{
- const struct InventoryProduct *ip
- = &oc->parse_request.inventory_products[i];
- struct TALER_MERCHANTDB_ProductDetails pd;
- enum GNUNET_DB_QueryStatus qs;
- size_t num_categories = 0;
- uint64_t *categories = NULL;
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
+ "merchant_base_url is not valid");
+ return;
+ }
+ oc->parse_order.merchant_base_url
+ = GNUNET_strdup (merchant_base_url);
+ }
+ else
+ {
+ char *url;
- qs = TMH_db->lookup_product (TMH_db->cls,
- oc->hc->instance->settings.id,
- ip->product_id,
- &pd,
- &num_categories,
- &categories);
- if (qs <= 0)
- {
- enum TALER_ErrorCode ec = TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE;
- unsigned int http_status = 0;
+ url = make_merchant_base_url (oc->connection,
+ settings->id);
+ if (NULL == url)
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MISSING,
+ "order:merchant_base_url");
+ return;
+ }
+ oc->parse_order.merchant_base_url = url;
+ }
- switch (qs)
- {
- case GNUNET_DB_STATUS_HARD_ERROR:
- GNUNET_break (0);
- http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
- ec = TALER_EC_GENERIC_DB_FETCH_FAILED;
- break;
- case GNUNET_DB_STATUS_SOFT_ERROR:
- GNUNET_break (0);
- http_status = MHD_HTTP_INTERNAL_SERVER_ERROR;
- ec = TALER_EC_GENERIC_DB_SOFT_FAILURE;
- break;
- case GNUNET_DB_STATUS_SUCCESS_NO_RESULTS:
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Product %s from order unknown\n",
- ip->product_id);
- http_status = MHD_HTTP_NOT_FOUND;
- ec = TALER_EC_MERCHANT_GENERIC_PRODUCT_UNKNOWN;
- break;
- case GNUNET_DB_STATUS_SUCCESS_ONE_RESULT:
- /* case listed to make compilers happy */
- GNUNET_assert (0);
- }
- reply_with_error (oc,
- http_status,
- ec,
- ip->product_id);
- return;
- }
- GNUNET_free (categories);
- oc->parse_order.minimum_age
- = GNUNET_MAX (oc->parse_order.minimum_age,
- pd.minimum_age);
- {
- json_t *p;
+ if ( (NULL != oc->parse_order.products) &&
+ (! TMH_products_array_valid (oc->parse_order.products)) )
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_GENERIC_PARAMETER_MALFORMED,
+ "order.products");
+ return;
+ }
- p = GNUNET_JSON_PACK (
- GNUNET_JSON_pack_string ("product_name",
- pd.product_name),
- GNUNET_JSON_pack_string ("description",
- pd.description),
- GNUNET_JSON_pack_object_steal ("description_i18n",
- pd.description_i18n),
- GNUNET_JSON_pack_string ("unit",
- pd.unit),
- TALER_JSON_pack_amount ("price",
- &pd.price),
- GNUNET_JSON_pack_array_steal ("taxes",
- pd.taxes),
- GNUNET_JSON_pack_string ("image",
- pd.image),
- GNUNET_JSON_pack_uint64 (
- "quantity",
- ip->quantity));
- GNUNET_assert (NULL != p);
- GNUNET_assert (0 ==
- json_array_append_new (oc->merge_inventory.products,
- p));
- }
- GNUNET_free (pd.description);
- GNUNET_free (pd.unit);
- GNUNET_free (pd.image);
- json_decref (pd.address);
- }
+ /* Merchant information must not already be present */
+ if (NULL != jmerchant)
+ {
+ GNUNET_break_op (0);
+ reply_with_error (
+ oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR,
+ "'merchant' field already set, but must be provided by backend");
+ return;
}
- /* check if final product list is well-formed */
- if (! TMH_products_array_valid (oc->merge_inventory.products))
+
+ if ( (NULL != oc->parse_order.delivery_location) &&
+ (! TMH_location_object_valid (oc->parse_order.delivery_location)) )
{
GNUNET_break_op (0);
reply_with_error (oc,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
- "order:products");
+ "delivery_location");
return;
}
+
oc->phase++;
}
+/* ***************** ORDER_PHASE_PARSE_REQUEST **************** */
+
/**
* Parse the client request. Upon success,
* continue processing by calling parse_order().
@@ -3726,7 +3988,7 @@ merge_inventory (struct OrderContext *oc)
* @param[in,out] oc order context to process
*/
static void
-parse_request (struct OrderContext *oc)
+phase_parse_request (struct OrderContext *oc)
{
const json_t *ip = NULL;
const json_t *uuid = NULL;
@@ -3939,6 +4201,9 @@ parse_request (struct OrderContext *oc)
}
+/* ***************** Main handler **************** */
+
+
MHD_RESULT
TMH_private_post_orders (
const struct TMH_RequestHandler *rh,
@@ -3963,38 +4228,41 @@ TMH_private_post_orders (
switch (oc->phase)
{
case ORDER_PHASE_PARSE_REQUEST:
- parse_request (oc);
+ phase_parse_request (oc);
break;
case ORDER_PHASE_PARSE_ORDER:
- parse_order (oc);
+ phase_parse_order (oc);
break;
case ORDER_PHASE_PARSE_CHOICES:
- parse_choices (oc);
+ phase_parse_choices (oc);
break;
case ORDER_PHASE_MERGE_INVENTORY:
- merge_inventory (oc);
+ phase_merge_inventory (oc);
break;
case ORDER_PHASE_ADD_PAYMENT_DETAILS:
- add_payment_details (oc);
+ phase_add_payment_details (oc);
break;
case ORDER_PHASE_SET_EXCHANGES:
- if (set_exchanges (oc))
+ if (phase_set_exchanges (oc))
return MHD_YES;
break;
+ case ORDER_PHASE_SELECT_WIRE_METHOD:
+ phase_select_wire_method (oc);
+ break;
case ORDER_PHASE_SET_MAX_FEE:
- set_max_fee (oc);
+ phase_set_max_fee (oc);
break;
case ORDER_PHASE_SERIALIZE_ORDER:
- serialize_order (oc);
+ phase_serialize_order (oc);
break;
case ORDER_PHASE_CHECK_CONTRACT:
- check_contract (oc);
+ phase_check_contract (oc);
break;
case ORDER_PHASE_SALT_FORGETTABLE:
- salt_forgettable (oc);
+ phase_salt_forgettable (oc);
break;
case ORDER_PHASE_EXECUTE_ORDER:
- execute_order (oc);
+ phase_execute_order (oc);
break;
case ORDER_PHASE_FINISHED_MHD_YES:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,