merchant

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

commit 1d7f15a524fdeca2304d1fa5bf13dcf0decd8ecc
parent 801f18909fc696d37a160551233f0749db6f7d68
Author: Christian Grothoff <christian@grothoff.org>
Date:   Tue, 24 Dec 2024 17:51:41 +0100

fix pay handling for v1 contracts, including in test

Diffstat:
Msrc/backend/taler-merchant-httpd_contract.c | 8+++++++-
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 666+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 5++++-
Msrc/testing/testing_api_cmd_pay_order.c | 371+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
4 files changed, 630 insertions(+), 420 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c @@ -111,9 +111,16 @@ parse_choices (void *cls, for (unsigned int i = 0; i<*choices_len; i++) { + struct TALER_MerchantContractChoice *choice = &(*choices)[i]; const json_t *jinputs; const json_t *joutputs; 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), GNUNET_JSON_spec_array_const ("inputs", &jinputs), GNUNET_JSON_spec_array_const ("outputs", @@ -122,7 +129,6 @@ parse_choices (void *cls, }; const char *error_name; unsigned int error_line; - struct TALER_MerchantContractChoice *choice = &(*choices)[i]; if (GNUNET_OK != GNUNET_JSON_parse (json_array_get (root, i), diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -107,7 +107,7 @@ enum PayPhase PP_VALIDATE_TOKENS, /** - * Contract has been paid. + * Check if contract has been paid. */ PP_CONTRACT_PAID, @@ -365,6 +365,8 @@ struct ExchangeGroup */ struct PayContext { + // FIXME: group more entries by phase that initializes them, + // like we do for 'validate_tokens'. /** * Stored in a DLL. @@ -404,16 +406,6 @@ struct PayContext struct TokenEnvelope *token_envelopes; /** - * Array with @e choices_len choices from the contract terms. - */ - struct TALER_MerchantContractChoice *choices; - - /** - * Array with @e token_families_len token families from the contract terms. - */ - struct TALER_MerchantContractTokenFamily *token_families; - - /** * MHD connection to return to */ struct MHD_Connection *connection; @@ -498,6 +490,7 @@ struct PayContext */ struct TALER_PrivateContractHashP h_contract_terms; + /** * "h_wire" from @e contract_terms. Used to identify * the instance's wire transfer method. @@ -505,19 +498,83 @@ struct PayContext struct TALER_MerchantWireHashP h_wire; /** - * Maximum fee the merchant is willing to pay, from @e root. - * Note that IF the total fee of the exchange is higher, that is - * acceptable to the merchant if the customer is willing to - * pay the difference - * (i.e. amount - max_fee <= actual_amount - actual_fee). + * Version of the contract terms. + */ + enum TALER_MerchantContractVersion version; + + /** + * @e version dependent details from our contract. */ - struct TALER_Amount max_fee; + union + { + + struct + { + /** + * Maximum fee the merchant is willing to pay, from @e root. + * Note that IF the total fee of the exchange is higher, that is + * acceptable to the merchant if the customer is willing to + * pay the difference + * (i.e. amount - max_fee <= actual_amount - actual_fee). + */ + struct TALER_Amount max_fee; + + /** + * Amount from @e root. This is the amount the merchant expects + * to make, minus @e max_fee. + */ + struct TALER_Amount amount; + } v0; + + struct + { + + /** + * Array with @e choices_len choices from the contract terms. + */ + struct TALER_MerchantContractChoice *choices; + + /** + * Array with @e token_families_len token families from the contract terms. + */ + struct TALER_MerchantContractTokenFamily *token_families; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + /** + * Length of the @e token_families array. + */ + unsigned int token_families_len; + + } v1; + + } details; /** - * Amount from @e root. This is the amount the merchant expects - * to make, minus @e max_fee. + * Results from the phase_validate_tokens() */ - struct TALER_Amount amount; + struct + { + + /** + * Maximum fee the merchant is willing to pay, from @e root. + * Note that IF the total fee of the exchange is higher, that is + * acceptable to the merchant if the customer is willing to + * pay the difference + * (i.e. amount - max_fee <= actual_amount - actual_fee). + */ + struct TALER_Amount max_fee; + + /** + * Amount from @e root. This is the amount the merchant expects + * to make, minus @e max_fee. + */ + struct TALER_Amount brutto; + + } validate_tokens; /** * Considering all the coins with the "found_in_db" flag @@ -600,16 +657,6 @@ struct PayContext unsigned int output_tokens_len; /** - * Length of the @e choices array. - */ - unsigned int choices_len; - - /** - * Length of the @e token_families array. - */ - unsigned int token_families_len; - - /** * Number of exchanges involved in the payment. Length * of the @e eg array. */ @@ -879,7 +926,7 @@ batch_deposit_transaction (const struct ExchangeGroup *eg, uint32_t off = 0; GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &total_without_fees)); for (size_t i = 0; i<pc->coins_cnt; i++) { @@ -1691,7 +1738,7 @@ phase_success_response (struct PayContext *pc) ? NULL : TALER_build_pos_confirmation (pc->pos_key, pc->pos_alg, - &pc->amount, + &pc->validate_tokens.brutto, pc->timestamp); token_sigs = (0 >= pc->output_tokens_len) ? NULL @@ -1903,9 +1950,9 @@ check_payment_sufficient (struct PayContext *pc) struct TALER_Amount total_needed; if (0 == pc->coins_cnt) - return TALER_amount_is_zero (&pc->amount); + return TALER_amount_is_zero (&pc->validate_tokens.brutto); GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &total_wire_fee)); for (unsigned int i = 0; i < pc->num_exchanges; i++) { @@ -1942,10 +1989,10 @@ check_payment_sufficient (struct PayContext *pc) * amount with fee / and wire fee, for all the coins. */ GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &acc_fee)); GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &acc_amount)); for (size_t i = 0; i<pc->coins_cnt; i++) { @@ -2013,7 +2060,7 @@ check_payment_sufficient (struct PayContext *pc) TALER_amount2s (&total_wire_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Deposit fee limit for merchant: %s\n", - TALER_amount2s (&pc->max_fee)); + TALER_amount2s (&pc->validate_tokens.max_fee)); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Total refunded amount: %s\n", TALER_amount2s (&pc->total_refunded)); @@ -2049,7 +2096,7 @@ check_payment_sufficient (struct PayContext *pc) "Overflow adding up amounts")); return false; } - if (-1 == TALER_amount_cmp (&pc->max_fee, + if (-1 == TALER_amount_cmp (&pc->validate_tokens.max_fee, &acc_fee)) { /** @@ -2063,12 +2110,12 @@ check_payment_sufficient (struct PayContext *pc) GNUNET_assert (TALER_AAR_RESULT_POSITIVE == TALER_amount_subtract (&excess_fee, &acc_fee, - &pc->max_fee)); + &pc->validate_tokens.max_fee)); /* add that to the total */ if (0 > TALER_amount_add (&total_needed, &excess_fee, - &pc->amount)) + &pc->validate_tokens.brutto)) { GNUNET_break (0); pay_end (pc, @@ -2084,7 +2131,7 @@ check_payment_sufficient (struct PayContext *pc) { /* Fees are fully covered by the merchant, all we require is that the total payment is not below the contract's amount */ - total_needed = pc->amount; + total_needed = pc->validate_tokens.brutto; } /* Do not count refunds towards the payment */ @@ -2123,7 +2170,7 @@ check_payment_sufficient (struct PayContext *pc) return false; } if (-1 < TALER_amount_cmp (&acc_amount, - &pc->amount)) + &pc->validate_tokens.brutto)) { GNUNET_break_op (0); pay_end (pc, @@ -2181,13 +2228,13 @@ phase_execute_pay_transaction (struct PayContext *pc) (used in check_coin_paid(), check_coin_refunded() and check_payment_sufficient()). */ GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &pc->total_paid)); GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &pc->total_fees_paid)); GNUNET_break (GNUNET_OK == - TALER_amount_set_zero (pc->amount.currency, + TALER_amount_set_zero (pc->validate_tokens.brutto.currency, &pc->total_refunded)); for (size_t i = 0; i<pc->coins_cnt; i++) pc->dc[i].found_in_db = false; @@ -2240,7 +2287,7 @@ phase_execute_pay_transaction (struct PayContext *pc) else if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { /* UNIQUE constraint violation, meaning this token was already used. */ - TMH_db->rollback(TMH_db->cls); + TMH_db->rollback (TMH_db->cls); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_CONFLICT, @@ -2275,13 +2322,14 @@ phase_execute_pay_transaction (struct PayContext *pc) } if (pc->deposit_currency_mismatch) { - TMH_db->rollback(TMH_db->cls); + TMH_db->rollback (TMH_db->cls); GNUNET_break_op (0); pay_end (pc, TALER_MHD_reply_with_error (pc->connection, MHD_HTTP_BAD_REQUEST, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->amount.currency)); + pc->validate_tokens.brutto.currency)) + ; return; } } @@ -2647,20 +2695,23 @@ sign_token_envelopes (struct PayContext *pc, * @param slug slug to search for * @return NULL if @a slug was not found */ -static struct TALER_MerchantContractTokenFamily * +static const struct TALER_MerchantContractTokenFamily * find_family (const struct PayContext *pc, const char *slug) { - for (unsigned int i = 0; i<pc->token_families_len; i++) + for (unsigned int i = 0; i<pc->details.v1.token_families_len; i++) { - if (0 == strcmp (pc->token_families[i].slug, + const struct TALER_MerchantContractTokenFamily *tfi + = &pc->details.v1.token_families[i]; + + if (0 == strcmp (tfi->slug, slug)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Token family %s found with %u keys\n", slug, - pc->token_families[i].keys_len); - return &pc->token_families[i]; + tfi->keys_len); + return tfi; } } return NULL; @@ -2678,178 +2729,173 @@ find_family (const struct PayContext *pc, static void phase_validate_tokens (struct PayContext *pc) { - if (NULL == pc->choices || 0 >= pc->choices_len) + switch (pc->version) { + case TALER_MCV_V0: /* No tokens to validate */ pc->phase = PP_PAY_TRANSACTION; - return; - } - - if (pc->choice_index < 0) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' has non-empty choices array but" - "request is missing 'choice_index' field\n", - pc->order_id); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING, - NULL)); - return; - } - - if (pc->choice_index >= pc->choices_len) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Order `%s' has choices array with %u elements but " - "request has 'choice_index' field with value %ld\n", - pc->order_id, - pc->choices_len, - pc->choice_index); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, - NULL)); - return; - } - - { - const struct TALER_MerchantContractChoice *selected - = &pc->choices[pc->choice_index]; - - for (unsigned int i = 0; i<selected->inputs_len; i++) + pc->validate_tokens.max_fee = pc->details.v0.max_fee; + pc->validate_tokens.brutto = pc->details.v0.amount; + break; + case TALER_MCV_V1: { - const struct TALER_MerchantContractInput *input - = &selected->inputs[i]; - const struct TALER_MerchantContractTokenFamily *family; + const struct TALER_MerchantContractChoice *selected + = &pc->details.v1.choices[pc->choice_index]; - if (input->type != TALER_MCIT_TOKEN) - { - /* only validate inputs of type token (for now) */ - continue; - } + pc->validate_tokens.max_fee = selected->max_fee; + pc->validate_tokens.brutto = selected->amount; - family = find_family (pc, - input->details.token.token_family_slug); - if (NULL == family) + for (unsigned int i = 0; i<selected->inputs_len; i++) { - /* this should never happen, since the choices and - token families are validated on insert. */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "token family not found in order")); - return; - } - if (GNUNET_NO == - find_valid_input_tokens (pc, - family, - i, - input->details.token.count)) - { - /* Error is already scheduled from find_valid_input_token. */ - return; - } - } - - GNUNET_array_grow (pc->output_tokens, - pc->output_tokens_len, - selected->outputs_len); + const struct TALER_MerchantContractInput *input + = &selected->inputs[i]; + const struct TALER_MerchantContractTokenFamily *family; - for (unsigned int i = 0; i<selected->outputs_len; i++) - { - enum GNUNET_DB_QueryStatus qs; - struct TALER_MERCHANTDB_TokenFamilyKeyDetails details; - const struct TALER_MerchantContractOutput *output - = &selected->outputs[i]; - struct TALER_MerchantContractTokenFamily *family; - struct TALER_MerchantContractTokenFamilyKey *key; + if (input->type != TALER_MCIT_TOKEN) + { + /* only validate inputs of type token (for now) */ + continue; + } - if (output->type != TALER_MCOT_TOKEN) - { - /* only validate outputs of type tokens (for now) */ - continue; + family = find_family (pc, + input->details.token.token_family_slug); + if (NULL == family) + { + /* this should never happen, since the choices and + token families are validated on insert. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "token family not found in order")); + return; + } + if (GNUNET_NO == + find_valid_input_tokens (pc, + family, + i, + input->details.token.count)) + { + /* Error is already scheduled from find_valid_input_token. */ + return; + } } - family = find_family (pc, - output->details.token.token_family_slug); - if (NULL == family) - { - /* this should never happen, since the choices and - token families are validated on insert. */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "token family not found in order")); - return; - } - if (output->details.token.key_index >= family->keys_len) - { - /* this should never happen, since the choices and - token families are validated on insert. */ - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "key index invalid for token family")); - return; - } - key = &family->keys[output->details.token.key_index]; - qs = TMH_db->lookup_token_family_key ( - TMH_db->cls, - pc->hc->instance->settings.id, - family->slug, - pc->timestamp, - pc->pay_deadline, - &details); - if (qs <= 0) + GNUNET_array_grow (pc->output_tokens, + pc->output_tokens_len, + selected->outputs_len); + + for (unsigned int i = 0; i<selected->outputs_len; i++) { - GNUNET_log ( - GNUNET_ERROR_TYPE_ERROR, - "Did not find key for %s at [%llu,%llu]\n", - family->slug, - (unsigned long long) pc->timestamp.abs_time.abs_value_us, - (unsigned long long) pc->pay_deadline.abs_time.abs_value_us); - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - NULL)); - return; - } + enum GNUNET_DB_QueryStatus qs; + struct TALER_MERCHANTDB_TokenFamilyKeyDetails details; + const struct TALER_MerchantContractOutput *output + = &selected->outputs[i]; + const struct TALER_MerchantContractTokenFamily *family; + struct TALER_MerchantContractTokenFamilyKey *key; + + if (output->type != TALER_MCOT_TOKEN) + { + /* only validate outputs of type tokens (for now) */ + continue; + } - GNUNET_assert (NULL != details.priv.private_key); + family = find_family (pc, + output->details.token.token_family_slug); + if (NULL == family) + { + /* this should never happen, since the choices and + token families are validated on insert. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "token family not found in order")); + return; + } + if (output->details.token.key_index >= family->keys_len) + { + /* this should never happen, since the choices and + token families are validated on insert. */ + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, + "key index invalid for token family")); + return; + } + key = &family->keys[output->details.token.key_index]; + qs = TMH_db->lookup_token_family_key ( + TMH_db->cls, + pc->hc->instance->settings.id, + family->slug, + pc->timestamp, + pc->pay_deadline, + &details); + if (qs <= 0) + { + GNUNET_log ( + GNUNET_ERROR_TYPE_ERROR, + "Did not find key for %s at [%llu,%llu]\n", + family->slug, + (unsigned long long) pc->timestamp.abs_time.abs_value_us, + (unsigned long long) pc->pay_deadline.abs_time.abs_value_us); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + NULL)); + return; + } - if (GNUNET_OK != - sign_token_envelopes (pc, - key, - &details.priv, - /* TODO: Use critical field stored in database here instead. */ - details.token_family.kind == - TALER_MERCHANTDB_TFK_Subscription, - i, - output->details.token.count)) - { - /* Error is already scheduled from sign_token_envelopes. */ - return; + GNUNET_assert (NULL != details.priv.private_key); + + if (GNUNET_OK != + sign_token_envelopes (pc, + key, + &details.priv, + /* TODO: Use critical field stored in database here instead. */ + details.token_family.kind == + TALER_MERCHANTDB_TFK_Subscription, + i, + output->details.token.count)) + { + /* Error is already scheduled from sign_token_envelopes. */ + return; + } } + } + } + + for (size_t i = 0; i<pc->coins_cnt; i++) + { + const struct DepositConfirmation *dc = &pc->dc[i]; + if (GNUNET_OK != + TALER_amount_cmp_currency (&dc->cdd.amount, + &pc->validate_tokens.brutto)) + { + GNUNET_break_op (0); + fprintf (stderr, + "HERE (%u): %s != %s\n", + (unsigned int) pc->version, + dc->cdd.amount.currency, + TALER_amount2s (&pc->validate_tokens.brutto)); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, + pc->validate_tokens.brutto.currency)); + return; } } @@ -3174,16 +3220,17 @@ phase_check_contract (struct PayContext *pc) /* Get details from contract and check fundamentals */ { const char *fulfillment_url = NULL; + uint64_t version = 0; struct GNUNET_JSON_Specification espec[] = { - TALER_JSON_spec_amount_any ("amount", - &pc->amount), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("version", + &version), + NULL), GNUNET_JSON_spec_mark_optional ( /* This one does not have to be a Web URL */ GNUNET_JSON_spec_string ("fulfillment_url", &fulfillment_url), NULL), - TALER_JSON_spec_amount_any ("max_fee", - &pc->max_fee), GNUNET_JSON_spec_timestamp ("timestamp", &pc->timestamp), GNUNET_JSON_spec_timestamp ("refund_deadline", @@ -3192,16 +3239,6 @@ phase_check_contract (struct PayContext *pc) &pc->pay_deadline), GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", &pc->wire_transfer_deadline), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_choices ("choices", - &pc->choices, - &pc->choices_len), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_token_families ("token_families", - &pc->token_families, - &pc->token_families_len), - NULL), GNUNET_JSON_spec_fixed_auto ("h_wire", &pc->h_wire), GNUNET_JSON_spec_mark_optional ( @@ -3216,8 +3253,6 @@ phase_check_contract (struct PayContext *pc) res = TALER_MHD_parse_internal_json_data (pc->connection, pc->contract_terms, espec); - if (NULL != fulfillment_url) - pc->fulfillment_url = GNUNET_strdup (fulfillment_url); if (GNUNET_YES != res) { GNUNET_break (0); @@ -3227,41 +3262,131 @@ phase_check_contract (struct PayContext *pc) : MHD_NO); return; } - } - - if (GNUNET_OK != - TALER_amount_cmp_currency (&pc->max_fee, - &pc->amount)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "'max_fee' in database does not match currency of contract price")); - return; - } - - for (size_t i = 0; i<pc->coins_cnt; i++) - { - struct DepositConfirmation *dc = &pc->dc[i]; - - if (GNUNET_OK != - TALER_amount_cmp_currency (&dc->cdd.amount, - &pc->amount)) + if (NULL != fulfillment_url) + pc->fulfillment_url = GNUNET_strdup (fulfillment_url); + switch (version) { - GNUNET_break_op (0); + case 0: + { + struct GNUNET_JSON_Specification v0spec[] = { + TALER_JSON_spec_amount_any ("amount", + &pc->details.v0.amount), + TALER_JSON_spec_amount_any ("max_fee", + &pc->details.v0.max_fee), + GNUNET_JSON_spec_end () + }; + res = TALER_MHD_parse_internal_json_data (pc->connection, + pc->contract_terms, + v0spec); + if (GNUNET_YES != res) + { + GNUNET_break (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + if (GNUNET_OK != + TALER_amount_cmp_currency (&pc->details.v0.max_fee, + &pc->details.v0.amount)) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "'max_fee' in database does not match currency of contract price")); + return; + } + + if (pc->choice_index > 0) + { + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, + "contract terms v0 has no choices")); + return; + } + } + pc->version = TALER_MCV_V0; + break; + case 1: + { + struct GNUNET_JSON_Specification v1spec[] = { + TALER_JSON_spec_choices ("choices", + &pc->details.v1.choices, + &pc->details.v1.choices_len), + TALER_JSON_spec_token_families ("token_families", + &pc->details.v1.token_families, + &pc->details.v1.token_families_len), + GNUNET_JSON_spec_end () + }; + res = TALER_MHD_parse_internal_json_data (pc->connection, + pc->contract_terms, + v1spec); + if (GNUNET_YES != res) + { + GNUNET_break (0); + pay_end (pc, + (GNUNET_NO == res) + ? MHD_YES + : MHD_NO); + return; + } + if (pc->choice_index < 0) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has non-empty choices array but" + "request is missing 'choice_index' field\n", + pc->order_id); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING, + NULL)); + return; + } + if (pc->choice_index >= pc->details.v1.choices_len) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Order `%s' has choices array with %u elements but " + "request has 'choice_index' field with value %ld\n", + pc->order_id, + pc->details.v1.choices_len, + pc->choice_index); + GNUNET_break (0); + pay_end (pc, + TALER_MHD_reply_with_error ( + pc->connection, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS, + NULL)); + return; + } + } + pc->version = TALER_MCV_V1; + break; + default: + GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( pc->connection, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - pc->amount.currency)); + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "contract 'version' in database not supported by this backend") + ); return; } } + if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline, <, pc->refund_deadline)) @@ -3288,7 +3413,7 @@ phase_check_contract (struct PayContext *pc) return; } - /* Make sure wire method (still) exists for this instance */ +/* Make sure wire method (still) exists for this instance */ { struct TMH_WireMethod *wm; @@ -3753,25 +3878,32 @@ pay_context_cleanup (void *cls) GNUNET_free (eg); } GNUNET_free (pc->egs); - for (unsigned int i = 0; i< pc->token_families_len; i++) + switch (pc->version) { - struct TALER_MerchantContractTokenFamily *tf - = &pc->token_families[i]; - - GNUNET_free (tf->slug); - GNUNET_free (tf->name); - GNUNET_free (tf->description); - json_decref (tf->description_i18n); - for (unsigned int j = 0; j<tf->keys_len; j++) + case TALER_MCV_V0: + break; + case TALER_MCV_V1: + for (unsigned int i = 0; i< pc->details.v1.token_families_len; i++) { - struct TALER_MerchantContractTokenFamilyKey *key - = &tf->keys[j]; + struct TALER_MerchantContractTokenFamily *tf + = &pc->details.v1.token_families[i]; - TALER_token_issue_pub_free (&key->pub); + GNUNET_free (tf->slug); + GNUNET_free (tf->name); + GNUNET_free (tf->description); + json_decref (tf->description_i18n); + for (unsigned int j = 0; j<tf->keys_len; j++) + { + struct TALER_MerchantContractTokenFamilyKey *key + = &tf->keys[j]; + + TALER_token_issue_pub_free (&key->pub); + } + GNUNET_free (tf->keys); } - GNUNET_free (tf->keys); + GNUNET_free (pc->details.v1.token_families); + break; } - GNUNET_free (pc->token_families); if (NULL != pc->response) { MHD_destroy_response (pc->response); diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -2270,6 +2270,7 @@ compute_fee (struct OrderContext *oc, return; } if ( (settings->use_stefan) && + (NULL != max_stefan_fee) && (GNUNET_OK == TALER_amount_is_valid (max_stefan_fee)) ) { @@ -2309,7 +2310,9 @@ set_max_fee (struct OrderContext *oc) 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], + NULL != oc->set_exchanges.details.v1.max_stefan_fees + ? &oc->set_exchanges.details.v1.max_stefan_fees[i] + : NULL, &oc->set_max_fee.details.v1.max_fees[i]); break; default: diff --git a/src/testing/testing_api_cmd_pay_order.c b/src/testing/testing_api_cmd_pay_order.c @@ -559,8 +559,6 @@ pay_run (void *cls, struct TALER_MerchantWireHashP h_wire; const struct TALER_PrivateContractHashP *h_proposal; struct TALER_Amount max_fee; - const json_t *choices = NULL; - const json_t *token_families = NULL; const char *error_name = NULL; unsigned int error_line = 0; struct TALER_MERCHANT_PayCoin *pay_coins; @@ -598,7 +596,12 @@ pay_run (void *cls, { /* Get information that needs to be put verbatim in the * deposit permission */ + uint64_t version = 0; struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("version", + &version), + NULL), GNUNET_JSON_spec_string ("order_id", &order_id), GNUNET_JSON_spec_timestamp ("refund_deadline", @@ -611,18 +614,6 @@ pay_run (void *cls, &merchant_pub), GNUNET_JSON_spec_fixed_auto ("h_wire", &h_wire), - TALER_JSON_spec_amount_any ("amount", - &ps->total_amount), - TALER_JSON_spec_amount_any ("max_fee", - &max_fee), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("token_families", - &token_families), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("choices", - &choices), - NULL), /* FIXME oec: parse minimum age, use data later? */ GNUNET_JSON_spec_end () }; @@ -645,6 +636,221 @@ pay_run (void *cls, free (js); TALER_TESTING_FAIL (is); } + switch (version) + { + case 0: + { + struct GNUNET_JSON_Specification v0spec[] = { + TALER_JSON_spec_amount_any ("amount", + &ps->total_amount), + TALER_JSON_spec_amount_any ("max_fee", + &max_fee), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (contract_terms, + v0spec, + &error_name, + &error_line)) + { + char *js; + + js = json_dumps (contract_terms, + JSON_INDENT (1)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + error_name, + error_line, + js); + free (js); + TALER_TESTING_FAIL (is); + } + } + if (0 < ps->choice_index) + TALER_TESTING_FAIL (is); + break; + case 1: + { + const json_t *choices; + const json_t *token_families; + struct GNUNET_JSON_Specification v1spec[] = { + GNUNET_JSON_spec_object_const ("token_families", + &token_families), + GNUNET_JSON_spec_array_const ("choices", + &choices), + GNUNET_JSON_spec_end () + }; + const json_t *outputs; + json_t *output; + unsigned int output_index; + const json_t *choice; + + if (GNUNET_OK != + GNUNET_JSON_parse (contract_terms, + v1spec, + &error_name, + &error_line)) + { + char *js; + + js = json_dumps (contract_terms, + JSON_INDENT (1)); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + error_name, + error_line, + js); + free (js); + TALER_TESTING_FAIL (is); + } + + choice = json_array_get (choices, + ps->choice_index); + if (NULL == choice) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No choice found at index %d\n", + ps->choice_index); + TALER_TESTING_FAIL (is); + } + + { + const char *ierror_name = NULL; + unsigned int ierror_line = 0; + + struct GNUNET_JSON_Specification ispec[] = { + TALER_JSON_spec_amount_any ("amount", + &ps->total_amount), + TALER_JSON_spec_amount_any ("max_fee", + &max_fee), + GNUNET_JSON_spec_array_const ("outputs", + &outputs), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (choice, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + ierror_name, + ierror_line, + json_dumps (choice, + JSON_INDENT (2))); + TALER_TESTING_FAIL (is); + } + } + + json_array_foreach (outputs, output_index, output) + { + const char *slug; + const char *kind; + uint32_t key_index; + uint32_t count = 1; + const char *ierror_name = NULL; + unsigned int ierror_line = 0; + + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("kind", + &kind), + GNUNET_JSON_spec_string ("token_family_slug", + &slug), + GNUNET_JSON_spec_uint32 ("key_index", + &key_index), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint32 ("count", + &count), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (output, + ispec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Parser failed on %s:%u for input `%s'\n", + ierror_name, + ierror_line, + json_dumps (output, + JSON_INDENT (2))); + TALER_TESTING_FAIL (is); + } + + if (0 != strcmp ("token", kind)) + { + continue; + } + + GNUNET_array_grow (ps->issued_tokens, + ps->num_issued_tokens, + ps->num_issued_tokens + count); + + for (unsigned int k = 0; k < count; k++) + { + struct TALER_MERCHANT_PrivateTokenDetails *details = + &ps->issued_tokens[ps->num_issued_tokens - count + k]; + + if (GNUNET_OK != + find_token_public_key (token_families, + slug, + key_index, + &details->issue_pub)) + { + TALER_TESTING_FAIL (is); + } + + /* Only RSA is supported for now. */ + GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == + details->issue_pub.public_key->cipher); + + TALER_token_blind_input_copy (&details->blinding_inputs, + TALER_token_blind_input_rsa_singleton () + ); + /* TODO: Where to get details->blinding_inputs from? */ + TALER_token_use_setup_random (&details->master); + TALER_token_use_setup_priv (&details->master, + &details->blinding_inputs, + &details->token_priv); + TALER_token_use_blinding_secret_create (&details->master, + &details->blinding_inputs, + &details->blinding_secret); + GNUNET_CRYPTO_eddsa_key_get_public (&details->token_priv.private_key + , + &details->token_pub.public_key); + GNUNET_CRYPTO_hash (&details->token_pub.public_key, + sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey), + &details->h_token_pub.hash); + details->envelope.blinded_pub = GNUNET_CRYPTO_message_blind_to_sign + ( + details->issue_pub.public_key, + &details->blinding_secret, + NULL, /* TODO: Add session nonce to support CS tokens */ + &details->h_token_pub.hash, + sizeof (details->h_token_pub.hash), + details->blinding_inputs.blinding_inputs); + + if (NULL == details->envelope.blinded_pub) + { + GNUNET_break (0); + TALER_TESTING_FAIL (is); + } + } + } + } + + break; + default: + TALER_TESTING_FAIL (is); + } + + } { @@ -688,143 +894,6 @@ pay_run (void *cls, } GNUNET_free (tr); } - if (0 <= ps->choice_index) - { - const json_t *outputs; - json_t *output; - unsigned int output_index; - const json_t *choice = json_array_get (choices, ps->choice_index); - - if (NULL == choice) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "No choice found at index %d\n", - ps->choice_index); - TALER_TESTING_FAIL (is); - } - - { - const char *ierror_name = NULL; - unsigned int ierror_line = 0; - - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_array_const ("outputs", - &outputs), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (choice, - ispec, - &ierror_name, - &ierror_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Parser failed on %s:%u for input `%s'\n", - ierror_name, - ierror_line, - json_dumps (choice, - JSON_INDENT (2))); - TALER_TESTING_FAIL (is); - } - } - - json_array_foreach (outputs, output_index, output) - { - const char *slug; - const char *kind; - uint32_t key_index; - uint32_t count = 1; - const char *ierror_name = NULL; - unsigned int ierror_line = 0; - - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("kind", - &kind), - GNUNET_JSON_spec_string ("token_family_slug", - &slug), - GNUNET_JSON_spec_uint32 ("key_index", - &key_index), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("count", - &count), - NULL), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (output, - ispec, - &ierror_name, - &ierror_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Parser failed on %s:%u for input `%s'\n", - ierror_name, - ierror_line, - json_dumps (output, - JSON_INDENT (2))); - TALER_TESTING_FAIL (is); - } - - if (0 != strcmp ("token", kind)) - { - continue; - } - - GNUNET_array_grow (ps->issued_tokens, - ps->num_issued_tokens, - ps->num_issued_tokens + count); - - for (unsigned int k = 0; k < count; k++) - { - struct TALER_MERCHANT_PrivateTokenDetails *details = - &ps->issued_tokens[ps->num_issued_tokens - count + k]; - - if (GNUNET_OK != - find_token_public_key (token_families, - slug, - key_index, - &details->issue_pub)) - { - TALER_TESTING_FAIL (is); - } - - /* Only RSA is supported for now. */ - GNUNET_assert (GNUNET_CRYPTO_BSA_RSA == - details->issue_pub.public_key->cipher); - - TALER_token_blind_input_copy (&details->blinding_inputs, - TALER_token_blind_input_rsa_singleton ()); - /* TODO: Where to get details->blinding_inputs from? */ - TALER_token_use_setup_random (&details->master); - TALER_token_use_setup_priv (&details->master, - &details->blinding_inputs, - &details->token_priv); - TALER_token_use_blinding_secret_create (&details->master, - &details->blinding_inputs, - &details->blinding_secret); - GNUNET_CRYPTO_eddsa_key_get_public (&details->token_priv.private_key, - &details->token_pub.public_key); - GNUNET_CRYPTO_hash (&details->token_pub.public_key, - sizeof (struct GNUNET_CRYPTO_EcdsaPublicKey), - &details->h_token_pub.hash); - details->envelope.blinded_pub = GNUNET_CRYPTO_message_blind_to_sign ( - details->issue_pub.public_key, - &details->blinding_secret, - NULL, /* TODO: Add session nonce to support CS tokens */ - &details->h_token_pub.hash, - sizeof (details->h_token_pub.hash), - details->blinding_inputs.blinding_inputs); - - if (NULL == details->envelope.blinded_pub) - { - GNUNET_break (0); - TALER_TESTING_FAIL (is); - } - } - } - } GNUNET_array_grow (output_tokens, len_output_tokens,