merchant

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

commit 801f18909fc696d37a160551233f0749db6f7d68
parent 31e40f3859569a924eff59a1b99f4c5b4e5188c4
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 24 Dec 2024 14:12:00 +0100

-fix order creation logic for contract v1

Diffstat:
Msrc/backend/taler-merchant-httpd_contract.h | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 1478++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Msrc/include/taler_merchant_testing_lib.h | 4++--
Msrc/testing/test_merchant_api.c | 8++++----
Msrc/testing/test_merchant_order_creation.sh | 4++--
Msrc/testing/testing_api_cmd_post_orders.c | 167++++++++++++++++++++++++++++++-------------------------------------------------
6 files changed, 1119 insertions(+), 640 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_contract.h b/src/backend/taler-merchant-httpd_contract.h @@ -220,6 +220,9 @@ struct TALER_MerchantContractOutput */ unsigned int count; + // FIXME: add support for clients picking a validity + // period in the future for output tokens! + } token; } details; @@ -380,10 +383,9 @@ struct TALER_MerchantContractTokenFamily } details; }; + /** - * Struct to hold contract terms in v0 and v1 format. v0 contracts are modelled - * as a v1 contract with a single choice and no inputs and outputs. Use the - * version field to explicitly differentiate between v0 and v1 contracts. + * Struct to hold contract terms. */ struct TALER_MerchantContract { @@ -436,15 +438,8 @@ struct TALER_MerchantContract * Jurisdiction of the business */ json_t *jurisdiction; - } merchant; - /** - * Price to be paid for the transaction. Could be 0. The price is in addition - * to other instruments, such as rations and tokens. - * The exchange will subtract deposit fees from that amount - * before transferring it to the merchant. - */ - struct TALER_Amount brutto; + } merchant; /** * Summary of the contract. @@ -531,44 +526,81 @@ struct TALER_MerchantContract enum TALER_MerchantContractVersion version; /** - * Array of possible specific contracts the wallet/customer may choose - * from by selecting the respective index when signing the deposit - * confirmation. + * Details depending on the @e version. */ - struct TALER_MerchantContractChoice *choices; + union + { - /** - * Length of the @e choices array. - */ - unsigned int choices_len; + /** + * Details for v0 contracts. + */ + struct + { - /** - * Array of token authorities. - */ - struct TALER_MerchantContractTokenFamily *token_authorities; + /** + * Price to be paid for the transaction. Could be 0. The price is in addition + * to other instruments, such as rations and tokens. + * The exchange will subtract deposit fees from that amount + * before transferring it to the merchant. + */ + struct TALER_Amount brutto; - /** - * Length of the @e token_authorities array. - */ - unsigned int token_authorities_len; + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; - /** - * Maximum fee as given by the client request. - */ - struct TALER_Amount max_fee; + } v0; + + /** + * Details for v1 contracts. + */ + struct + { + + /** + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. + */ + struct TALER_MerchantContractChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Array of token authorities. + */ + struct TALER_MerchantContractTokenFamily *token_authorities; + + /** + * Length of the @e token_authorities array. + */ + unsigned int token_authorities_len; + + } v1; + + } details; + + // FIXME: Add exchanges array? - // TODO: Add exchanges array }; + enum TALER_MerchantContractInputType TMH_contract_input_type_from_string (const char *str); + enum TALER_MerchantContractOutputType TMH_contract_output_type_from_string (const char *str); + const char * TMH_string_from_contract_input_type (enum TALER_MerchantContractInputType t); + const char * TMH_string_from_contract_output_type (enum TALER_MerchantContractOutputType t); @@ -590,12 +622,14 @@ TMH_serialize_contract (const struct TALER_MerchantContract *contract, json_t *exchanges, json_t **out); + enum GNUNET_GenericReturnValue TMH_serialize_contract_v0 (const struct TALER_MerchantContract *contract, const struct TMH_MerchantInstance *instance, json_t *exchanges, json_t **out); + enum GNUNET_GenericReturnValue TMH_serialize_contract_v1 (const struct TALER_MerchantContract *contract, const struct TMH_MerchantInstance *instance, diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -247,10 +247,6 @@ struct OrderContext */ struct { - /** - * Version of the contract terms. - */ - enum TALER_MerchantContractVersion version; /** * Our order ID. @@ -295,11 +291,6 @@ struct OrderContext const char *public_reorder_url; /** - * Array of contract choices. Is null for v0 contracts. - */ - const json_t *choices; - - /** * Merchant base URL. */ char *merchant_base_url; @@ -335,17 +326,6 @@ struct OrderContext const json_t *delivery_location; /** - * Gross amount value of the contract. Used to - * compute @e max_stefan_fee. - */ - struct TALER_Amount brutto; - - /** - * Maximum fee as given by the client request. - */ - struct TALER_Amount max_fee; - - /** * Specifies for how long the wallet should try to get an * automatic refund for the purchase. */ @@ -367,6 +347,45 @@ struct OrderContext */ uint32_t minimum_age; + /** + * Version of the contract terms. + */ + enum TALER_MerchantContractVersion version; + + /** + * Details present depending on @e version. + */ + union + { + /** + * Details only present for v0. + */ + struct + { + /** + * Gross amount value of the contract. Used to + * compute @e max_stefan_fee. + */ + struct TALER_Amount brutto; + + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; + } v0; + + /** + * Details only present for v1. + */ + struct + { + /** + * Array of contract choices. Is null for v0 contracts. + */ + const json_t *choices; + } v1; + } details; + } parse_order; /** @@ -445,18 +464,16 @@ struct OrderContext bool exchange_good; /** - * Maximum fee for @e order based on STEFAN curves. - * Used to set @e max_fee if not provided as part of - * @e order. + * Array of maximum amounts that could be paid over all + * available exchanges. Used to determine if this + * order creation requests exceeds legal limits. */ - struct TALER_Amount max_stefan_fee; + struct TALER_Amount *total_exchange_limits; /** - * Maximum amount that could be paid over all - * available exchanges. Used to determine if this - * order creation requests exceeds legal limits. + * Length of the @e total_exchange_limits array. */ - struct TALER_Amount total_exchange_limit; + unsigned int num_total_exchange_limits; /** * How long do we wait at most until giving up on getting keys? @@ -468,6 +485,43 @@ struct OrderContext */ struct GNUNET_SCHEDULER_Task *wakeup_task; + /** + * Details depending on the contract version. + */ + union + { + + /** + * Details for contract v0. + */ + struct + { + /** + * Maximum fee for @e order based on STEFAN curves. + * Used to set @e max_fee if not provided as part of + * @e order. + */ + struct TALER_Amount max_stefan_fee; + + } v0; + + /** + * Details for contract v1. + */ + struct + { + /** + * Maximum fee for @e order based on STEFAN curves by + * contract choice. + * Used to set @e max_fee if not provided as part of + * @e order. + */ + struct TALER_Amount *max_stefan_fees; + + } v1; + + } details; + } set_exchanges; /** @@ -475,10 +529,37 @@ struct OrderContext */ struct { + /** - * Maximum fee + * Details depending on the contract version. */ - struct TALER_Amount max_fee; + union + { + + /** + * Details for contract v0. + */ + struct + { + /** + * Maximum fee + */ + struct TALER_Amount max_fee; + } v0; + + /** + * Details for contract v1. + */ + struct + { + /** + * Maximum fees by contract choice. + */ + struct TALER_Amount *max_fees; + + } v1; + + } details; } set_max_fee; /** @@ -693,6 +774,16 @@ clean_order (void *cls) json_decref (oc->set_exchanges.exchanges); oc->set_exchanges.exchanges = NULL; } + GNUNET_free (oc->set_exchanges.total_exchange_limits); + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + break; + case TALER_MCV_V1: + GNUNET_free (oc->set_max_fee.details.v1.max_fees); + GNUNET_free (oc->set_exchanges.details.v1.max_stefan_fees); + break; + } if (NULL != oc->merge_inventory.products) { json_decref (oc->merge_inventory.products); @@ -1790,86 +1881,38 @@ add_output_token_family (struct OrderContext *oc, /** - * Serialize order into @a oc->serialize_order.contract, - * ready to be stored in the database. Upon success, continue - * processing with check_contract(). + * Build JSON array that represents all of the token families + * in the contract. * - * @param[in,out] oc order context + * @param[in] v1-style order + * @return JSON array with token families for the contract */ -static void -serialize_order (struct OrderContext *oc) +static json_t * +output_token_families (struct OrderContext *oc) { - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; - json_t *merchant; json_t *token_families = json_object (); - json_t *choices = json_array (); - - 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))); - 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)); - } - } + GNUNET_assert (NULL != token_families); for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) { - json_t *keys = json_array (); - struct TALER_MerchantContractTokenFamily *family + const struct TALER_MerchantContractTokenFamily *family = &oc->parse_choices.token_families[i]; + json_t *keys; json_t *jfamily; + keys = json_array (); + GNUNET_assert (NULL != keys); for (unsigned int j = 0; j<family->keys_len; j++) { - struct TALER_MerchantContractTokenFamilyKey key = family->keys[j]; - + const struct TALER_MerchantContractTokenFamilyKey *key + = &family->keys[j]; json_t *jkey = GNUNET_JSON_PACK ( - /* TODO: Remove h_pub. */ - GNUNET_JSON_pack_data_auto ("h_pub", - &key.pub.public_key->pub_key_hash), TALER_JSON_pack_token_pub ("public_key", - &key.pub), + &key->pub), GNUNET_JSON_pack_timestamp ("valid_after", - key.valid_after), + key->valid_after), GNUNET_JSON_pack_timestamp ("valid_before", - key.valid_before) + key->valid_before) ); GNUNET_assert (0 == @@ -1877,7 +1920,7 @@ serialize_order (struct OrderContext *oc) jkey)); } - /* TODO: Add 'details' field. */ + /* FIXME: Add 'details' field. */ jfamily = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("name", family->name), @@ -1892,11 +1935,28 @@ serialize_order (struct OrderContext *oc) family->critical) ); - GNUNET_assert (0 == json_object_set_new (token_families, - family->slug, - jfamily)); + GNUNET_assert (0 == + json_object_set_new (token_families, + family->slug, + jfamily)); } + return token_families; +} + + +/** + * Build JSON array that represents all of the contract choices + * in the contract. + * + * @param[in] v1-style order + * @return JSON array with token families for the contract + */ +static json_t * +output_contract_choices (struct OrderContext *oc) +{ + json_t *choices = json_array (); + GNUNET_assert (NULL != choices); for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { const struct TALER_MerchantContractChoice *choice @@ -1912,15 +1972,15 @@ serialize_order (struct OrderContext *oc) = &choice->inputs[j]; json_t *jinput; - /* For now, only tokens are supported */ + /* For now, only tokens are supported for inputs */ GNUNET_assert (TALER_MCIT_TOKEN == input->type); jinput = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("kind", - TMH_string_from_contract_input_type (input-> - type)), + TMH_string_from_contract_input_type ( + input->type)), GNUNET_JSON_pack_string ("token_family_slug", input->details.token.token_family_slug), - GNUNET_JSON_pack_int64 ("number", + GNUNET_JSON_pack_int64 ("count", input->details.token.count) ); @@ -1935,19 +1995,47 @@ serialize_order (struct OrderContext *oc) json_t *joutput; /* For now, only tokens are supported */ - GNUNET_assert (TALER_MCOT_TOKEN == output->type); - - joutput = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("kind", - TMH_string_from_contract_output_type (output-> - type)), - GNUNET_JSON_pack_string ("token_family_slug", - output->details.token.token_family_slug), - GNUNET_JSON_pack_int64 ("number", - output->details.token.count), - GNUNET_JSON_pack_int64 ("key_index", - output->details.token.key_index) - ); + switch (output->type) + { + case TALER_MCOT_INVALID: + /* How did we get here? */ + GNUNET_assert (0); + /* mostly to make compiler happy... */ + finalize_order (oc, + MHD_NO); + json_decref (choices); + return NULL; + case TALER_MCOT_TOKEN: + joutput = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("kind", + TMH_string_from_contract_output_type ( + output->type)), + GNUNET_JSON_pack_string ("token_family_slug", + output->details.token.token_family_slug), + GNUNET_JSON_pack_int64 ("count", + output->details.token.count), + GNUNET_JSON_pack_int64 ("key_index", + output->details.token.key_index) + ); + break; + case TALER_MCOT_COIN: + /* Not implemented, how did we get here? */ + GNUNET_break (0); + reply_with_error (oc, + MHD_HTTP_NOT_IMPLEMENTED, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "currency conversion not supported"); + json_decref (choices); + return NULL; + case TALER_MCOT_TAX_RECEIPT: + // FIXME: generate JSON for DONAU here instead of killing + // the connection! + GNUNET_break (0); + finalize_order (oc, + MHD_NO); + json_decref (choices); + return NULL; + } GNUNET_assert (0 == json_array_append_new (outputs, @@ -1957,6 +2045,10 @@ serialize_order (struct OrderContext *oc) { json_t *jchoice = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &choice->amount), + TALER_JSON_pack_amount ("max_fee", + &oc->set_max_fee.details.v1.max_fees[i]), GNUNET_JSON_pack_array_incref ("inputs", inputs), GNUNET_JSON_pack_array_incref ("outputs", @@ -1967,6 +2059,67 @@ serialize_order (struct OrderContext *oc) json_array_append_new (choices, jchoice)); } + } /* for all choices */ + return choices; +} + + +/** + * Serialize order into @a oc->serialize_order.contract, + * ready to be stored in the database. Upon success, continue + * processing with check_contract(). + * + * @param[in,out] oc order context + */ +static void +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))); + 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 ( @@ -1975,8 +2128,9 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_string ("summary", oc->parse_order.summary), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("summary_i18n", - (json_t *) oc->parse_order.summary_i18n)), + GNUNET_JSON_pack_object_incref ( + "summary_i18n", + (json_t *) oc->parse_order.summary_i18n)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("public_reorder_url", oc->parse_order.public_reorder_url)), @@ -1984,10 +2138,9 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_string ("fulfillment_message", oc->parse_order.fulfillment_message)), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("fulfillment_message_i18n", - (json_t *) oc->parse_order. - fulfillment_message_i18n)) - , + GNUNET_JSON_pack_object_incref ( + "fulfillment_message_i18n", + (json_t *) oc->parse_order.fulfillment_message_i18n)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", oc->parse_order.fulfillment_url)), @@ -2012,9 +2165,9 @@ serialize_order (struct OrderContext *oc) GNUNET_JSON_pack_timestamp ("delivery_date", oc->parse_order.delivery_date)), GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("delivery_location", - (json_t *) oc->parse_order. - delivery_location)), + GNUNET_JSON_pack_object_incref ( + "delivery_location", + (json_t *) oc->parse_order.delivery_location)), GNUNET_JSON_pack_string ("merchant_base_url", oc->parse_order.merchant_base_url), GNUNET_JSON_pack_object_steal ("merchant", @@ -2023,43 +2176,61 @@ serialize_order (struct OrderContext *oc) &oc->hc->instance->merchant_pub), GNUNET_JSON_pack_array_incref ("exchanges", oc->set_exchanges.exchanges), - TALER_JSON_pack_amount ("max_fee", - &oc->set_max_fee.max_fee), - TALER_JSON_pack_amount ("amount", - &oc->parse_order.brutto), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("choices", - choices) - ), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("token_families", - token_families) - ), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("extra", (json_t *) oc->parse_order.extra)) ); + { + json_t *xtra; + + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + xtra = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("max_fee", + &oc->set_max_fee.details.v0.max_fee), + TALER_JSON_pack_amount ("amount", + &oc->parse_order.details.v0.brutto)); + break; + case TALER_MCV_V1: + { + json_t *token_families = output_token_families (oc); + json_t *choices = output_contract_choices (oc); + + if ( (NULL == token_families) || + (NULL == choices) ) + { + GNUNET_break (0); + return; + } + xtra = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("choices", + choices), + GNUNET_JSON_pack_object_steal ("token_families", + token_families)); + break; + } + default: + GNUNET_assert (0); + } + GNUNET_assert (0 == + json_object_update (oc->serialize_order.contract, + xtra)); + json_decref (xtra); + } + + /* Pack does not work here, because it doesn't set zero-values for timestamps */ GNUNET_assert (0 == json_object_set_new (oc->serialize_order.contract, "refund_deadline", GNUNET_JSON_from_timestamp ( oc->parse_order.refund_deadline))); - - GNUNET_log ( - GNUNET_ERROR_TYPE_INFO, - "Refund deadline for contact is %llu\n", - (unsigned long long) oc->parse_order.refund_deadline.abs_time.abs_value_us); - GNUNET_log ( - GNUNET_ERROR_TYPE_INFO, - "Wallet timestamp for contact is %llu\n", - (unsigned long long) oc->parse_order.timestamp.abs_time.abs_value_us); - - /* Pack does not work here, because it sets zero-values for relative times */ /* auto_refund should only be set if it is not 0 */ if (! GNUNET_TIME_relative_is_zero (oc->parse_order.auto_refund)) { + /* Pack does not work here, because it sets zero-values for relative times */ GNUNET_assert (0 == json_object_set_new (oc->serialize_order.contract, "auto_refund", @@ -2072,36 +2243,78 @@ serialize_order (struct OrderContext *oc) /** - * Set max_fee in @a oc based on STEFAN value if - * not yet present. Upon success, continue - * processing with serialize_order(). + * 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 + * from @a brutto. * * @param[in,out] oc order context + * @param brutto brutto amount to compute fee for + * @param client_fee client-given fee override (or invalid) + * @param max_stefan_fee maximum STEFAN fee of any exchange + * @param max_fee set to the maximum stefan fee */ static void -set_max_fee (struct OrderContext *oc) +compute_fee (struct OrderContext *oc, + const struct TALER_Amount *brutto, + const struct TALER_Amount *client_fee, + const struct TALER_Amount *max_stefan_fee, + struct TALER_Amount *max_fee) { - const struct TALER_MERCHANTDB_InstanceSettings *settings = - &oc->hc->instance->settings; + const struct TALER_MERCHANTDB_InstanceSettings *settings + = &oc->hc->instance->settings; - if (GNUNET_OK != - TALER_amount_is_valid (&oc->parse_order.max_fee)) + if (GNUNET_OK == + TALER_amount_is_valid (client_fee)) { - struct TALER_Amount stefan; - - if ( (settings->use_stefan) && - (GNUNET_OK == - TALER_amount_is_valid (&oc->set_exchanges.max_stefan_fee)) ) - stefan = oc->set_exchanges.max_stefan_fee; - else - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (oc->parse_order.brutto.currency, - &stefan)); - oc->set_max_fee.max_fee = stefan; + *max_fee = *client_fee; + return; } - else + if ( (settings->use_stefan) && + (GNUNET_OK == + TALER_amount_is_valid (max_stefan_fee)) ) + { + *max_fee = *max_stefan_fee; + return; + } + GNUNET_assert ( + GNUNET_OK == + TALER_amount_set_zero (brutto->currency, + max_fee)); +} + + +/** + * Initialize "set_max_fee" in @a oc based on STEFAN value or client + * preference. Upon success, continue processing in next phase. + * + * @param[in,out] oc order context + */ +static void +set_max_fee (struct OrderContext *oc) +{ + switch (oc->parse_order.version) { - oc->set_max_fee.max_fee = oc->parse_order.max_fee; + case TALER_MCV_V0: + compute_fee (oc, + &oc->parse_order.details.v0.brutto, + &oc->parse_order.details.v0.max_fee, + &oc->set_exchanges.details.v0.max_stefan_fee, + &oc->set_max_fee.details.v0.max_fee); + break; + case TALER_MCV_V1: + oc->set_max_fee.details.v1.max_fees + = GNUNET_new_array (oc->parse_choices.choices_len, + struct TALER_Amount); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + compute_fee (oc, + &oc->parse_choices.choices[i].amount, + &oc->parse_choices.choices[i].max_fee, + &oc->set_exchanges.details.v1.max_stefan_fees[i], + &oc->set_max_fee.details.v1.max_fees[i]); + break; + default: + GNUNET_break (0); + break; } oc->phase++; } @@ -2130,63 +2343,103 @@ resume_with_keys (struct OrderContext *oc) /** - * Update MAX STEFAN fees based on @a keys. + * Given a @a brutto amount for exchange with @a keys, set the + * @a stefan_fee. Note that @a stefan_fee is updated to the maximum + * of the input and the computed fee. * - * @param[in,out] oc order context to update - * @param keys keys to derive STEFAN fees from + * @param[in,out] oc order context + * @param brutto some brutto amount the client is to pay + * @param[in,out] stefan_fee set to STEFAN fee to be paid by the merchant */ static void -update_stefan (struct OrderContext *oc, - const struct TALER_EXCHANGE_Keys *keys) +compute_stefan_fee (const struct TALER_EXCHANGE_Keys *keys, + const struct TALER_Amount *brutto, + struct TALER_Amount *stefan_fee) { struct TALER_Amount net; if (GNUNET_SYSERR != TALER_EXCHANGE_keys_stefan_b2n (keys, - &oc->parse_order.brutto, + brutto, &net)) { struct TALER_Amount fee; TALER_EXCHANGE_keys_stefan_round (keys, &net); - if (-1 == TALER_amount_cmp (&oc->parse_order.brutto, + if (-1 == TALER_amount_cmp (brutto, &net)) { /* brutto < netto! */ /* => after rounding, there is no real difference */ - net = oc->parse_order.brutto; + net = *brutto; } GNUNET_assert (0 <= TALER_amount_subtract (&fee, - &oc->parse_order.brutto, + brutto, &net)); if ( (GNUNET_OK != - TALER_amount_is_valid (&oc->set_exchanges.max_stefan_fee)) || - (-1 == TALER_amount_cmp (&oc->set_exchanges.max_stefan_fee, + TALER_amount_is_valid (stefan_fee)) || + (-1 == TALER_amount_cmp (stefan_fee, &fee)) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Updated STEFAN-based fee to %s\n", TALER_amount2s (&fee)); - oc->set_exchanges.max_stefan_fee = fee; + *stefan_fee = fee; } } } /** + * Update MAX STEFAN fees based on @a keys. + * + * @param[in,out] oc order context to update + * @param keys keys to derive STEFAN fees from + */ +static void +update_stefan (struct OrderContext *oc, + const struct TALER_EXCHANGE_Keys *keys) +{ + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + compute_stefan_fee (keys, + &oc->parse_order.details.v0.brutto, + &oc->set_exchanges.details.v0.max_stefan_fee); + break; + case TALER_MCV_V1: + oc->set_exchanges.details.v1.max_stefan_fees + = GNUNET_new_array (oc->parse_choices.choices_len, + struct TALER_Amount); + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + if (0 == strcasecmp (keys->currency, + oc->parse_choices.choices[i].amount.currency)) + compute_stefan_fee (keys, + &oc->parse_choices.choices[i].amount, + &oc->set_exchanges.details.v1.max_stefan_fees[i]); + break; + default: + GNUNET_assert (0); + } +} + + +/** * Compute the set of exchanges that would be acceptable * for this order. * * @param cls our `struct OrderContext` * @param url base URL of an exchange (not used) * @param exchange internal handle for the exchange + * @param max_needed maximum amount needed in this currency */ static void get_acceptable (void *cls, const char *url, - const struct TMH_Exchange *exchange) + const struct TMH_Exchange *exchange, + const struct TALER_Amount *max_needed) { struct OrderContext *oc = cls; unsigned int priority = 42; /* make compiler happy */ @@ -2194,7 +2447,7 @@ get_acceptable (void *cls, enum GNUNET_GenericReturnValue res; struct TALER_Amount max_amount; - max_amount = oc->parse_order.brutto; + max_amount = *max_needed; res = TMH_exchange_check_debit ( oc->hc->instance->settings.id, exchange, @@ -2205,8 +2458,7 @@ get_acceptable (void *cls, url, res, TALER_amount2s (&max_amount)); - if ( (! TALER_amount_is_zero (&max_amount)) && - (TALER_amount_is_zero (&max_amount)) ) + if (TALER_amount_is_zero (&max_amount)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", @@ -2236,20 +2488,47 @@ get_acceptable (void *cls, "Exchange %s deposit limit is %s, adding it!\n", url, TALER_amount2s (&max_amount)); - GNUNET_assert (0 <= - TALER_amount_add ( - &oc->set_exchanges.total_exchange_limit, - &oc->set_exchanges.total_exchange_limit, - &max_amount)); - GNUNET_assert (GNUNET_OK == - TALER_amount_min (&oc->set_exchanges.total_exchange_limit, - &oc->set_exchanges.total_exchange_limit, - &oc->parse_order.brutto)); - j_exchange = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("url", - url), - GNUNET_JSON_pack_uint64 ("priority", - priority), + { + 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), + GNUNET_JSON_pack_uint64 ("priority", + priority), TALER_JSON_pack_amount ("max_contribution", &max_amount), GNUNET_JSON_pack_data_auto ("master_pub", @@ -2292,17 +2571,59 @@ keys_cb ( } else { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Got response for %skeys\n", - rx->url); - if ( (settings->use_stefan) && - (GNUNET_OK != - TALER_amount_is_valid (&oc->parse_order.max_fee)) ) - update_stefan (oc, - keys); - get_acceptable (oc, - rx->url, - exchange); + bool currency_ok = false; + struct TALER_Amount max_needed; + + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + if (0 == strcasecmp (keys->currency, + oc->parse_order.details.v0.brutto.currency)) + { + max_needed = oc->parse_order.details.v0.brutto; + currency_ok = true; + } + break; + case TALER_MCV_V1: + 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 (rx->url); GNUNET_free (rx); @@ -2410,6 +2731,40 @@ 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(). * @@ -2419,12 +2774,32 @@ wakeup_timeout (void *cls) static bool 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; } - if (TALER_amount_is_zero (&oc->parse_order.brutto)) + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + need_exchange = ! TALER_amount_is_zero ( + &oc->parse_order.details.v0.brutto); + break; + case TALER_MCV_V1: + 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) { /* Total amount is zero, so we don't actually need exchanges! */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -2435,21 +2810,13 @@ set_exchanges (struct OrderContext *oc) return false; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order total is %s, trying to find exchanges\n", - TALER_amount2s (&oc->parse_order.brutto)); - /* Note: re-building 'oc->set_exchanges.exchanges' every time here might be a - tad expensive; could likely consider caching the result if it starts to - matter. */ + "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); - GNUNET_assert ( - GNUNET_OK == - TALER_amount_set_zero (oc->parse_order.brutto.currency, - &oc->set_exchanges.total_exchange_limit)); TMH_exchange_get_trusted (&get_exchange_keys, oc); } @@ -2505,32 +2872,52 @@ set_exchanges (struct OrderContext *oc) oc->add_payment_details.wm->wire_method); return false; } - if (1 == - TALER_amount_cmp (&oc->parse_order.brutto, - &oc->set_exchanges.total_exchange_limit)) + { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Cannot create order: %s is the sum of hard limits from supported exchanges\n", - TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); - notify_kyc_required (oc); - reply_with_error ( - oc, - MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_AMOUNT_EXCEEDS_LEGAL_LIMITS, - TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); - return false; + bool ok; + struct TALER_Amount ea; + + switch (oc->parse_order.version) + { + case TALER_MCV_V0: + ea = oc->parse_order.details.v0.brutto; + ok = check_exchange_limits (oc, + &ea); + break; + case TALER_MCV_V1: + ok = true; + for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) + { + ea = oc->parse_choices.choices[i].amount; + if (! check_exchange_limits (oc, + &ea)) + { + ok = false; + break; + } + } + break; + default: + GNUNET_assert (0); + } + + if (! ok) + { + notify_kyc_required (oc); + 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; + } } + if (! oc->set_exchanges.exchange_good) { GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Creating order, but possibly without usable trusted exchanges\n"); } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Can create order: %s is the sum of hard limits from supported exchanges\n", - TALER_amount2s (&oc->set_exchanges.total_exchange_limit)); - } oc->phase++; return false; } @@ -2550,18 +2937,12 @@ parse_order (struct OrderContext *oc) const char *merchant_base_url = NULL; uint64_t version = 0; const json_t *jmerchant = NULL; - /* auto_refund only needs to be type-checked, - * mostly because in GNUnet relative times can't - * be negative. */ - bool no_fee; - const char *oid; + const char *order_id; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint64 ("version", &version), NULL), - TALER_JSON_spec_amount_any ("amount", - &oc->parse_order.brutto), GNUNET_JSON_spec_string ("summary", &oc->parse_order.summary), GNUNET_JSON_spec_mark_optional ( @@ -2574,7 +2955,7 @@ parse_order (struct OrderContext *oc) NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("order_id", - &oid), + &order_id), NULL), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_string ("fulfillment_message", @@ -2593,13 +2974,10 @@ parse_order (struct OrderContext *oc) &oc->parse_order.public_reorder_url), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("choices", - &oc->parse_order.choices), - 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), @@ -2621,10 +2999,6 @@ parse_order (struct OrderContext *oc) &oc->parse_order.wire_deadline), NULL), GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("max_fee", - &oc->parse_order.max_fee), - &no_fee), - GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_object_const ("delivery_location", &oc->parse_order.delivery_location), NULL), @@ -2660,36 +3034,100 @@ parse_order (struct OrderContext *oc) ret); return; } - if (0 == version) + switch (version) { - oc->parse_order.version = TALER_MCV_V0; - if (NULL != oc->parse_order.choices) + case 0: { - 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; + 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); + // FIXME: use CONFLICT and a different EC! + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "no trusted exchange for this 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_MCV_V0; + break; } - } - else if (1 == version) - { - oc->parse_order.version = TALER_MCV_V1; - if (NULL == oc->parse_order.choices) + case 1: { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.choices is required in v1 contracts"); - return; + 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_MCV_V1; + break; } - } - else - { + default: GNUNET_break_op (0); GNUNET_JSON_parse_free (spec); reply_with_error (oc, @@ -2698,35 +3136,11 @@ parse_order (struct OrderContext *oc) "invalid version specified in order, supported are null, '0' or '1'"); return; } - if (! TMH_test_exchange_configured_for_currency ( - oc->parse_order.brutto.currency)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "no trusted exchange for this currency"); - return; - } - if ( (! no_fee) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.brutto, - &oc->parse_order.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; - } /* Add order_id if it doesn't exist. */ - if (NULL != oid) + if (NULL != order_id) { - oc->parse_order.order_id = GNUNET_strdup (oid); + oc->parse_order.order_id = GNUNET_strdup (order_id); } else { @@ -3001,6 +3415,217 @@ parse_order (struct OrderContext *oc) /** + * Parse the inputs for a particular choice. + * + * @param[in,out] oc order context + * @param[out] choice to parse inputs for + * @param jinputs array of inputs to parse + * @return #GNUNET_OK on success, #GNUNET_SYSERR + * if an error was encountered (and already handled) + */ +static enum GNUNET_GenericReturnValue +parse_order_inputs (struct OrderContext *oc, + struct TALER_MerchantContractChoice *choice, + const json_t *jinputs) +{ + const json_t *jinput; + size_t idx; + + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MerchantContractInput input = { + .details.token.count = 1 + }; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + struct GNUNET_JSON_Specification ispec[] = { + // FIXME: define spec parser for 'kind'... + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &input.details.token.token_family_slug), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &input.details.token.count), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (jinput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid input #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return GNUNET_SYSERR; + } + + input.type = TMH_contract_input_type_from_string (kind); + if (TALER_MCIT_INVALID == input.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in input #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return GNUNET_SYSERR; + } + + if (0 == input.details.token.count) + { + /* Ignore inputs with 'number' field set to 0 */ + continue; + } + + if (GNUNET_OK != + add_input_token_family (oc, + input.details.token.token_family_slug)) + { + /* error is already scheduled, return. */ + return GNUNET_SYSERR; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + } + return GNUNET_OK; +} + + +/** + * Parse the outputs for a particular choice. + * + * @param[in,out] oc order context + * @param[out] choice to parse inputs for + * @param joutputs array of outputs to parse + * @return #GNUNET_OK on success, #GNUNET_SYSERR + * if an error was encountered (and already handled) + */ +static enum GNUNET_GenericReturnValue +parse_order_outputs (struct OrderContext *oc, + struct TALER_MerchantContractChoice *choice, + const json_t *joutputs) +{ + const json_t *joutput; + size_t idx; + + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MerchantContractOutput output = { + .details.token.count = 1 + }; + const char *kind; + const char *ierror_name; + unsigned int ierror_line; + bool nots; + struct GNUNET_TIME_Timestamp valid_at; + struct GNUNET_JSON_Specification ispec[] = { + // FIXME: define spec parser for 'kind'... + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &output.details.token.token_family_slug), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &output.details.token.count), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("valid_at", + &valid_at), + &nots), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (joutput, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid output #%u for field %s\n", + (unsigned int) idx, + ierror_name); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + ierror_name); + return GNUNET_SYSERR; + } + if (nots) + { + valid_at = oc->parse_order.pay_deadline; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Looking for output token valid at pay deadline %s\n", + GNUNET_TIME_timestamp2s (valid_at)); + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Looking for output token valid at %s\n", + GNUNET_TIME_timestamp2s (valid_at)); + } + if (GNUNET_TIME_timestamp_cmp (valid_at, + <, + oc->parse_order.pay_deadline)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "valid_at before pay_deadline"); + return GNUNET_SYSERR; + } + + output.type = TMH_contract_output_type_from_string (kind); + if (TALER_MCOT_INVALID == output.type) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'kind' invalid in output #%u\n", + (unsigned int) idx); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "kind"); + return GNUNET_SYSERR; + } + + if (0 == output.details.token.count) + { + /* Ignore outputs with 'number' field set to 0. */ + continue; + } + + if (GNUNET_OK != + add_output_token_family (oc, + output.details.token.token_family_slug, + valid_at, + &output.details.token.key_index)) + { + /* Error is already scheduled, return. */ + return GNUNET_SYSERR; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + } + return GNUNET_OK; +} + + +/** * Parse contract choices. Upon success, continue * processing with merge_inventory(). * @@ -3009,15 +3634,24 @@ parse_order (struct OrderContext *oc) static void parse_choices (struct OrderContext *oc) { - if (NULL == oc->parse_order.choices) + const json_t *choices; + + switch (oc->parse_order.version) { + case TALER_MCV_V0: oc->phase++; return; + case TALER_MCV_V1: + /* handle below */ + break; + default: + GNUNET_assert (0); } + choices = oc->parse_order.details.v1.choices; GNUNET_array_grow (oc->parse_choices.choices, oc->parse_choices.choices_len, - json_array_size (oc->parse_order.choices)); + json_array_size (choices)); for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { struct TALER_MerchantContractChoice *choice @@ -3026,13 +3660,14 @@ parse_choices (struct OrderContext *oc) unsigned int error_line; const json_t *jinputs; const json_t *joutputs; + bool no_fee; struct GNUNET_JSON_Specification spec[] = { TALER_JSON_spec_amount_any ("amount", &choice->amount), GNUNET_JSON_spec_mark_optional ( TALER_JSON_spec_amount_any ("max_fee", &choice->max_fee), - NULL), + &no_fee), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_array_const ("inputs", &jinputs), @@ -3045,7 +3680,7 @@ parse_choices (struct OrderContext *oc) }; enum GNUNET_GenericReturnValue ret; - ret = GNUNET_JSON_parse (json_array_get (oc->parse_order.choices, + ret = GNUNET_JSON_parse (json_array_get (choices, i), spec, &error_name, @@ -3062,6 +3697,33 @@ parse_choices (struct OrderContext *oc) "choice"); return; } + if ( (! no_fee) && + (GNUNET_OK != + 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; + } + + if (! TMH_test_exchange_configured_for_currency ( + choice->amount.currency)) + { + GNUNET_break_op (0); + GNUNET_JSON_parse_free (spec); + // FIXME: use CONFLICT and a different EC! + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "no trusted exchange for this currency"); + return; + } + if ( (0 == json_array_size (jinputs)) && (0 == json_array_size (joutputs)) ) @@ -3075,195 +3737,17 @@ parse_choices (struct OrderContext *oc) "choice"); return; } - - { - // TODO: Maybe move to a separate function - const json_t *jinput; - size_t idx; - json_array_foreach ((json_t *) jinputs, idx, jinput) - { - struct TALER_MerchantContractInput input = { - .details.token.count = 1 - }; - const char *kind; - const char *ierror_name; - unsigned int ierror_line; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("kind", - &kind), - GNUNET_JSON_spec_string ("token_family_slug", - &input.details.token.token_family_slug), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("count", - &input.details.token.count), - NULL), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (jinput, - ispec, - &ierror_name, - &ierror_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Invalid input #%u for field %s\n", - (unsigned int) idx, - ierror_name); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ierror_name); - return; - } - - input.type = TMH_contract_input_type_from_string (kind); - - if (TALER_MCIT_INVALID == input.type) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Field 'kind' invalid in input #%u\n", - (unsigned int) idx); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "kind"); - return; - } - - if (0 == input.details.token.count) - { - /* Ignore inputs with 'number' field set to 0 */ - continue; - } - - if (GNUNET_OK != - add_input_token_family (oc, - input.details.token.token_family_slug)) - { - /* error is already scheduled, return. */ - return; - } - - GNUNET_array_append (choice->inputs, - choice->inputs_len, - input); - } - } - - { - const json_t *joutput; - size_t idx; - json_array_foreach ((json_t *) joutputs, idx, joutput) - { - struct TALER_MerchantContractOutput output = { - .details.token.count = 1 - }; - const char *kind; - const char *ierror_name; - unsigned int ierror_line; - bool nots; - struct GNUNET_TIME_Timestamp valid_at; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("kind", - &kind), - GNUNET_JSON_spec_string ("token_family_slug", - &output.details.token.token_family_slug), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("count", - &output.details.token.count), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("valid_at", - &valid_at), - &nots), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (joutput, - ispec, - &ierror_name, - &ierror_line)) - { - GNUNET_JSON_parse_free (spec); - GNUNET_JSON_parse_free (ispec); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Invalid output #%u for field %s\n", - (unsigned int) idx, - ierror_name); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - ierror_name); - return; - } - if (nots) - { - valid_at = oc->parse_order.pay_deadline; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking for output token valid at pay deadline %s\n", - GNUNET_TIME_timestamp2s (valid_at)); - } - else - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Looking for output token valid at %s\n", - GNUNET_TIME_timestamp2s (valid_at)); - } - - - if (GNUNET_TIME_timestamp_cmp (valid_at, - <, - oc->parse_order.pay_deadline)) - { - GNUNET_JSON_parse_free (spec); - GNUNET_JSON_parse_free (ispec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "valid_at before pay_deadline"); - return; - } - - output.type = TMH_contract_output_type_from_string (kind); - if (TALER_MCOT_INVALID == output.type) - { - GNUNET_JSON_parse_free (spec); - GNUNET_JSON_parse_free (ispec); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Field 'kind' invalid in output #%u\n", - (unsigned int) idx); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "kind"); - return; - } - - if (0 == output.details.token.count) - { - /* Ignore outputs with 'number' field set to 0. */ - continue; - } - - if (GNUNET_OK != - add_output_token_family (oc, - output.details.token.token_family_slug, - valid_at, - &output.details.token.key_index)) - { - /* Error is already scheduled, return. */ - return; - } - - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - } - } + if (GNUNET_OK != + parse_order_inputs (oc, + choice, + jinputs)) + return; + if (GNUNET_OK != + parse_order_outputs (oc, + choice, + joutputs)) + return; } - oc->phase++; } diff --git a/src/include/taler_merchant_testing_lib.h b/src/include/taler_merchant_testing_lib.h @@ -613,7 +613,7 @@ TALER_TESTING_cmd_merchant_post_orders3 ( * @param merchant_url base URL of the merchant serving * the proposal request. * @param http_status expected HTTP status. - * @param token_family_reference label of the POST /tokenfamilies cmd. + * @param token_family_slug slug of the token family to use * @param num_inputs number of input tokens. * @param num_outputs number of output tokens. * @param order_id the name of the order to add. @@ -629,7 +629,7 @@ TALER_TESTING_cmd_merchant_post_orders_choices ( const struct GNUNET_CONFIGURATION_Handle *cfg, const char *merchant_url, unsigned int http_status, - const char *token_family_reference, + const char *token_family_slug, unsigned int num_inputs, unsigned int num_outputs, const char *order_id, diff --git a/src/testing/test_merchant_api.c b/src/testing/test_merchant_api.c @@ -1714,7 +1714,7 @@ run (void *cls, cred.cfg, merchant_url, MHD_HTTP_OK, - "create-upcoming-tokenfamily", + "subscription-upcoming", 0, 1, "5-upcoming-output", @@ -1740,7 +1740,7 @@ run (void *cls, cred.cfg, merchant_url, MHD_HTTP_OK, - "create-tokenfamily", + "subscription-1", 0, 1, "5-output", @@ -1763,7 +1763,7 @@ run (void *cls, cred.cfg, merchant_url, MHD_HTTP_OK, - "create-tokenfamily", + "subscription-1", 1, 1, "5-input-output", @@ -1796,7 +1796,7 @@ run (void *cls, cred.cfg, merchant_url, MHD_HTTP_OK, - "create-tokenfamily", + "subscription-1", 1, 1, "5-input-output-2", diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -266,11 +266,11 @@ fi echo " OK" echo "curl 'http://localhost:9966/private/orders' \ - -d '{\"order\":{\"version\":1,\"amount\":\"TESTKUDOS:7\",\"summary\":\"with_subscription\",\"fulfillment_message\":\"Paid successfully\",\"choices\":[{\"inputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}],\"outputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}]}]}}'" + -d '{\"order\":{\"version\":1,\"summary\":\"with_subscription\",\"fulfillment_message\":\"Paid successfully\",\"choices\":[\{\"amount\":\"TESTKUDOS:7","inputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}],\"outputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}]}]}}'" echo -n "Creating v1 order with token family ..." STATUS=$(curl 'http://localhost:9966/private/orders' \ - -d '{"order":{"version":1,"amount":"TESTKUDOS:7","summary":"with_subscription","fulfillment_message":"Paid successfully","choices":[{"inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ + -d '{"order":{"version":1,"summary":"with_subscription","fulfillment_message":"Paid successfully","choices":[{"amount":"TESTKUDOS:7","inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] diff --git a/src/testing/testing_api_cmd_post_orders.c b/src/testing/testing_api_cmd_post_orders.c @@ -60,23 +60,6 @@ struct OrdersState const char *expected_order_id; /** - * Reference to a POST /tokenfamilies command. Can be NULL. - */ - const char *token_family_reference; - - /** - * How many tokens of the token family created in - * @a token_family_reference are required as inputs. - */ - unsigned int num_inputs; - - /** - * How many tokens of the token family created in - * @a token_family_reference should be issued as outputs. - */ - unsigned int num_outputs; - - /** * Contract terms obtained from the backend. */ json_t *contract_terms; @@ -87,11 +70,6 @@ struct OrdersState json_t *order_terms; /** - * Choices array with inputs and outputs for v1 order. - */ - json_t *choices; - - /** * Contract terms hash code. */ struct TALER_PrivateContractHashP h_contract_terms; @@ -589,48 +567,6 @@ orders_run2 (void *cls, /** - * Constructs the json for a the choices of an order request. - * - * @param input_slug the name of the token family to use for input, can be NULL - * @param output_slug the name of the token family to use for the output, can be NULL. - * @param input_count number of token inputs to require - * @param output_count number of tokens to output - * @param input_valid_after validity date for the input token. - * @param output_valid_after validity date for the output token. - * @param[out] choices where to write the json string. - */ -static void -make_choices_json ( - const char *input_slug, - const char *output_slug, - uint16_t input_count, - uint16_t output_count, - struct GNUNET_TIME_Timestamp input_valid_after, - struct GNUNET_TIME_Timestamp output_valid_after, - json_t **choices) -{ - /* FIXME: ugly code should return c, use GNUNET_JSON_PACK() for more type-safety */ - json_t *c; - - c = json_pack ("[{s:o, s:o}]", - "inputs", json_pack ("[{s:s, s:i, s:s, s:o}]", - "kind", "token", - "count", input_count, - "token_family_slug", input_slug, - "valid_after", GNUNET_JSON_from_timestamp - (input_valid_after)), - "outputs", json_pack ("[{s:s, s:i, s:s, s:o}]", - "kind", "token", - "count", output_count, - "token_family_slug", output_slug, - "valid_after", GNUNET_JSON_from_timestamp - (output_valid_after))); - - *choices = c; -} - - -/** * Run a "orders" CMD. * * @param cls closure. @@ -644,7 +580,6 @@ orders_run3 (void *cls, { struct OrdersState *ps = cls; struct GNUNET_TIME_Absolute now; - const char *slug; ps->is = is; now = GNUNET_TIME_absolute_get_monotonic (ps->cfg); @@ -663,37 +598,6 @@ orders_run3 (void *cls, GNUNET_free (order_id); } - { - const struct TALER_TESTING_Command *token_family_cmd; - token_family_cmd = - TALER_TESTING_interpreter_lookup_command (is, - ps->token_family_reference); - if (NULL == token_family_cmd) - TALER_TESTING_FAIL (is); - if (GNUNET_OK != - TALER_TESTING_get_trait_token_family_slug (token_family_cmd, - &slug)) - TALER_TESTING_FAIL (is); - } - make_choices_json (slug, slug, - ps->num_inputs, - ps->num_outputs, - GNUNET_TIME_absolute_to_timestamp (now), - GNUNET_TIME_absolute_to_timestamp (now), - &ps->choices); - - GNUNET_assert (0 == - json_object_set_new (ps->order_terms, - "choices", - ps->choices) - ); - GNUNET_assert (0 == - json_object_set_new (ps->order_terms, - "version", - json_integer (1)) - ); - - GNUNET_CRYPTO_random_block (GNUNET_CRYPTO_QUALITY_WEAK, &ps->nonce, sizeof (struct GNUNET_CRYPTO_EddsaPublicKey)); @@ -770,7 +674,7 @@ mark_forgettable (void *cls, * @param order_id the name of the order to add, can be NULL. * @param refund_deadline the deadline for refunds on this order. * @param pay_deadline the deadline for payment on this order. - * @param amount the amount this order is for. + * @param amount the amount this order is for, NULL for v1 orders * @param[out] order where to write the json string. */ static void @@ -786,7 +690,7 @@ make_order_json (const char *order_id, /* Include required fields and some dummy objects to test forgetting. */ contract_terms = json_pack ( - "{s:s, s:s?, s:s, s:s, s:o, s:o, s:s, s:[{s:s}, {s:s}, {s:s}]}", + "{s:s, s:s?, s:s?, s:s, s:o, s:o, s:s, s:[{s:s}, {s:s}, {s:s}]}", "summary", "merchant-lib testcase", "order_id", order_id, "amount", amount, @@ -981,7 +885,7 @@ TALER_TESTING_cmd_merchant_post_orders_choices ( const struct GNUNET_CONFIGURATION_Handle *cfg, const char *merchant_url, unsigned int http_status, - const char *token_family_reference, + const char *token_family_slug, unsigned int num_inputs, unsigned int num_outputs, const char *order_id, @@ -990,18 +894,75 @@ TALER_TESTING_cmd_merchant_post_orders_choices ( const char *amount) { struct OrdersState *ps; + struct TALER_Amount brutto; + json_t *choice; + json_t *choices; + json_t *inputs; + json_t *outputs; ps = GNUNET_new (struct OrdersState); ps->cfg = cfg; make_order_json (order_id, refund_deadline, pay_deadline, - amount, + NULL, &ps->order_terms); + GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (amount, + &brutto)); + inputs = json_array (); + GNUNET_assert (NULL != inputs); + GNUNET_assert (0 == + json_array_append_new ( + inputs, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("kind", + "token"), + GNUNET_JSON_pack_uint64 ("count", + num_inputs), + GNUNET_JSON_pack_string ("token_family_slug", + token_family_slug) + ))); + outputs = json_array (); + GNUNET_assert (NULL != outputs); + GNUNET_assert (0 == + json_array_append_new ( + outputs, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("kind", + "token"), + GNUNET_JSON_pack_uint64 ("count", + num_outputs), + GNUNET_JSON_pack_string ("token_family_slug", + token_family_slug) + ))); + choice + = GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &brutto), + GNUNET_JSON_pack_array_steal ("inputs", + inputs), + GNUNET_JSON_pack_array_steal ("outputs", + outputs)); + choices = json_array (); + GNUNET_assert (NULL != choices); + GNUNET_assert (0 == + json_array_append_new ( + choices, + choice)); + GNUNET_assert (0 == + json_object_set_new (ps->order_terms, + "choices", + choices) + ); + GNUNET_assert (0 == + json_object_set_new (ps->order_terms, + "version", + json_integer (1)) + ); + + ps->http_status = http_status; - ps->token_family_reference = token_family_reference; - ps->num_inputs = num_inputs; - ps->num_outputs = num_outputs; ps->expected_order_id = order_id; ps->merchant_url = merchant_url; ps->with_claim = true;