merchant

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

commit 96ac05f7782c77663f43bb3a63cd36aa4a090c94
parent d1d992fdf54bd6b1a849272cae224f1ad7daf2a7
Author: Christian Grothoff <christian@grothoff.org>
Date:   Mon, 25 May 2026 21:45:28 +0200

Merge branch 'bNEW'

Diffstat:
Msrc/backend/taler-merchant-httpd_get-orders-ORDER_ID.c | 198++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c | 165++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Msrc/backend/taler-merchant-httpd_get-private-orders.c | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c | 161++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/backend/taler-merchant-httpd_post-private-orders.c | 1135+++++++++++++++++++++++--------------------------------------------------------
Msrc/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c | 5++---
Msrc/include/taler/taler_merchant_util.h | 1851+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msrc/testing/test_merchant_mfa.sh | 83+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/testing/test_merchant_statistics.sh | 4++--
Asrc/util/base_terms_parse.c | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/base_terms_serialize.c | 88+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/contract_choice_parse.c | 468+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/contract_choice_serialize.c | 166+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/contract_parse.c | 1631++++++++-----------------------------------------------------------------------
Msrc/util/contract_serialize.c | 523+++++++++----------------------------------------------------------------------
Asrc/util/contract_version_parse.c | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/merchant_parse.c | 152+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/meson.build | 16+++++++++++++++-
Asrc/util/order_choice_parse.c | 444+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/order_choice_serialize.c | 153+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/order_parse.c | 290+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/product_parse.c | 285+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/product_sold_serialize.c | 116+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/util/template_parse.c | 28++++++++++++++++------------
Msrc/util/test_contract.c | 19+++++++++----------
Asrc/util/token_family_parse.c | 399+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/token_family_serialize.c | 144+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/util.c | 75+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
28 files changed, 5321 insertions(+), 3616 deletions(-)

diff --git a/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.c b/src/backend/taler-merchant-httpd_get-orders-ORDER_ID.c @@ -154,6 +154,28 @@ struct GetOrderData struct TALER_MERCHANT_Contract *contract_terms; /** + * Parsed proto contract terms, NULL when parsing failed. + * Careful, aliased with pc in @e contract_terms if @e contract_terms is + * not NULL! + */ + struct TALER_MERCHANT_ProtoContract *pc; + + /** + * Order of the payment we are checking. NULL when we have a contract. + */ + json_t *order_json; + + /** + * Parsed order, NULL when parsing failed. + */ + struct TALER_MERCHANT_Order *order; + + /** + * Common terms from @e order or @e contract + */ + const struct TALER_MERCHANT_ContractBaseTerms *ct; + + /** * Total refunds granted for this payment. Only initialized * if @e refunded is set to true. */ @@ -232,9 +254,9 @@ struct GetOrderData bool generate_html; /** - * Did we parse the contract terms? + * Did we parse the order? */ - bool contract_parsed; + bool order_parsed; /** * Set to true if the refunds found in the DB have @@ -310,15 +332,31 @@ suspend_god (struct GetOrderData *god) { json_decref (god->contract_terms_json); god->contract_terms_json = NULL; - god->contract_parsed = false; + } + if (NULL != god->order_json) + { + json_decref (god->order_json); + god->order_json = NULL; + god->order_parsed = false; } if (NULL != god->contract_terms) { TALER_MERCHANT_contract_free (god->contract_terms); god->contract_terms = NULL; + god->pc = NULL; } + if (NULL != god->pc) + { + TALER_MERCHANT_proto_contract_free (god->pc); + god->pc = NULL; + } + if (NULL != god->order) + { + TALER_MERCHANT_order_free (god->order); + god->order = NULL; + } + god->ct = NULL; /* ensure not dangling */ GNUNET_assert (! god->suspended); - god->contract_parsed = false; god->contract_match = false; god->token_match = false; god->contract_available = false; @@ -351,6 +389,17 @@ god_cleanup (void *cls) TALER_MERCHANT_contract_free (god->contract_terms); god->contract_terms = NULL; } + if (NULL != god->order_json) + { + json_decref (god->order_json); + god->order_json = NULL; + } + if (NULL != god->order) + { + TALER_MERCHANT_order_free (god->order); + god->order = NULL; + } + god->ct = NULL; /* ensure not dangling */ if (NULL != god->session_eh) { TALER_MERCHANTDB_event_listen_cancel (god->session_eh); @@ -585,6 +634,7 @@ phase_lookup_terms (struct GetOrderData *god) /* Convert order_id to h_contract_terms */ TALER_MERCHANTDB_preflight (TMH_db); GNUNET_assert (NULL == god->contract_terms_json); + GNUNET_assert (NULL == god->order_json); { enum GNUNET_DB_QueryStatus qs; @@ -689,9 +739,7 @@ phase_lookup_terms (struct GetOrderData *god) god->order_id, &db_claim_token, &unused, - (NULL == god->contract_terms_json) - ? &god->contract_terms_json - : NULL); + &god->order_json); if (0 > qs) { /* single, read-only SQL statements should never cause @@ -736,21 +784,61 @@ static void phase_parse_contract (struct GetOrderData *god) { GNUNET_break (NULL == god->contract_terms); - god->contract_terms = TALER_MERCHANT_contract_parse ( - god->contract_terms_json, - true); - - if (NULL == god->contract_terms) + GNUNET_break (NULL == god->order); + if (NULL != god->contract_terms_json) { - phase_fail (god, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, - god->order_id); - return; + if (NULL == + json_object_get (god->contract_terms_json, + "nonce")) + { + god->pc = TALER_MERCHANT_proto_contract_parse ( + god->contract_terms_json); + if (NULL == god->pc) + { + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; + } + god->ct = god->pc->base; + } + else + { + god->contract_terms = TALER_MERCHANT_contract_parse ( + god->contract_terms_json); + if (NULL == god->contract_terms) + { + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; + } + god->pc = god->contract_terms->pc; + god->ct = god->contract_terms->pc->base; + } } - god->contract_parsed = true; + if (NULL != god->order_json) + { + god->order = TALER_MERCHANT_order_parse ( + god->order_json); + if (NULL == god->order) + { + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + god->order_id); + return; + } + god->order_parsed = true; + god->ct = god->order->base; + } + GNUNET_assert ( (NULL != god->order) || + (NULL != god->pc) ); + if ( (NULL != god->session_id) && - (NULL != god->contract_terms->fulfillment_url) && + (NULL != god->ct->fulfillment_url) && (NULL == god->session_eh) ) { struct TMH_SessionEventP session_eh = { @@ -765,8 +853,8 @@ phase_parse_contract (struct GetOrderData *god) GNUNET_CRYPTO_hash (god->session_id, strlen (god->session_id), &session_eh.h_session_id); - GNUNET_CRYPTO_hash (god->contract_terms->fulfillment_url, - strlen (god->contract_terms->fulfillment_url), + GNUNET_CRYPTO_hash (god->ct->fulfillment_url, + strlen (god->ct->fulfillment_url), &session_eh.h_fulfillment_url); god->session_eh = TALER_MERCHANTDB_event_listen ( @@ -821,11 +909,10 @@ phase_check_client_access (struct GetOrderData *god) if (! (god->token_match || god->contract_match) ) { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Neither claim token nor contract matched\n"); /* Client has no rights to this order */ - if (NULL == god->contract_terms->public_reorder_url) + if (NULL == god->ct->public_reorder_url) { /* We cannot give the client a new order, just fail */ if (! GNUNET_is_zero (&god->h_contract_terms)) @@ -872,7 +959,7 @@ phase_check_client_access (struct GetOrderData *god) MHD_add_response_header ( reply, MHD_HTTP_HEADER_LOCATION, - god->contract_terms->public_reorder_url)); + god->ct->public_reorder_url)); ret = MHD_queue_response (god->sc.con, MHD_HTTP_FOUND, reply); @@ -888,7 +975,7 @@ phase_check_client_access (struct GetOrderData *god) MHD_HTTP_ACCEPTED, GNUNET_JSON_pack_string ( "public_reorder_url", - god->contract_terms->public_reorder_url))); + god->ct->public_reorder_url))); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, @@ -909,15 +996,22 @@ get_order_summary (const struct GetOrderData *god) { const char *language_pattern; const char *ret; + json_t *terms; - language_pattern = MHD_lookup_connection_value (god->sc.con, - MHD_HEADER_KIND, - MHD_HTTP_HEADER_ACCEPT_LANGUAGE); + if (NULL != god->contract_terms_json) + terms = god->contract_terms_json; + else + terms = god->order_json; + language_pattern = MHD_lookup_connection_value ( + god->sc.con, + MHD_HEADER_KIND, + MHD_HTTP_HEADER_ACCEPT_LANGUAGE); if (NULL == language_pattern) language_pattern = "en"; - ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms_json, - language_pattern, - "summary")); + ret = json_string_value (TALER_JSON_extract_i18n ( + terms, + language_pattern, + "summary")); if (NULL == ret) { /* Upon order creation (and insertion into the database), the presence @@ -958,7 +1052,6 @@ send_pay_request (struct GetOrderData *god, suspend_god (god); return true; } - /* Check if resource_id has been paid for in the same session * with another order_id. */ @@ -995,11 +1088,10 @@ send_pay_request (struct GetOrderData *god, { struct MHD_Response *reply; - GNUNET_assert (NULL != god->contract_terms->fulfillment_url); GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Redirecting to already paid order %s via fulfillment URL %s\n", already_paid_order_id, - god->contract_terms->fulfillment_url); + god->ct->fulfillment_url); reply = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); @@ -1014,7 +1106,7 @@ send_pay_request (struct GetOrderData *god, MHD_add_response_header ( reply, MHD_HTTP_HEADER_LOCATION, - god->contract_terms->fulfillment_url)); + god->ct->fulfillment_url)); { ret = MHD_queue_response (god->sc.con, MHD_HTTP_FOUND, @@ -1080,7 +1172,7 @@ send_pay_request (struct GetOrderData *god, taler_pay_uri), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", - god->contract_terms->fulfillment_url)), + god->ct->fulfillment_url)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("already_paid_order_id", already_paid_order_id))); @@ -1137,7 +1229,7 @@ static bool phase_redirect_to_paid_order (struct GetOrderData *god) { if ( (NULL != god->session_id) && - (NULL != god->contract_terms->fulfillment_url) ) + (NULL != god->ct->fulfillment_url) ) { /* Check if client paid for this fulfillment article already within this session, but using a different @@ -1153,11 +1245,11 @@ phase_redirect_to_paid_order (struct GetOrderData *god) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Running re-purchase detection for %s/%s\n", god->session_id, - god->contract_terms->fulfillment_url); + god->ct->fulfillment_url); qs = TALER_MERCHANTDB_lookup_order_by_fulfillment ( TMH_db, god->hc->instance->settings.id, - god->contract_terms->fulfillment_url, + god->ct->fulfillment_url, god->session_id, TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase, &already_paid_order_id); @@ -1304,11 +1396,12 @@ phase_check_refunded (struct GetOrderData *god) struct TALER_Amount refund_amount; const char *refund_currency; - switch (god->contract_terms->version) + GNUNET_assert (NULL != god->contract_terms); + switch (god->ct->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: - refund_amount = god->contract_terms->details.v0.brutto; - refund_currency = god->contract_terms->details.v0.brutto.currency; + refund_amount = god->pc->details.v0.brutto; + refund_currency = god->pc->details.v0.brutto.currency; break; case TALER_MERCHANT_CONTRACT_VERSION_1: if (god->choice_index < 0) @@ -1318,11 +1411,12 @@ phase_check_refunded (struct GetOrderData *god) return false; } GNUNET_assert (god->choice_index < - god->contract_terms->details.v1.choices_len); - refund_currency = god->contract_terms->details.v1.choices[god->choice_index] - .amount.currency; - GNUNET_assert (GNUNET_OK == TALER_amount_set_zero (refund_currency, - &refund_amount)); + god->pc->details.v1.choices_len); + refund_currency = god->pc->details.v1.choices[ + god->choice_index].amount.currency; + GNUNET_assert (GNUNET_OK == + TALER_amount_set_zero (refund_currency, + &refund_amount)); break; default: { @@ -1483,8 +1577,7 @@ phase_return_status (struct GetOrderData *god) MHD_HTTP_OK, GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", - god->contract_terms->fulfillment_url - )), + god->ct->fulfillment_url)), GNUNET_JSON_pack_bool ("refunded", god->refunded), GNUNET_JSON_pack_bool ("refund_pending", @@ -1502,7 +1595,8 @@ phase_return_status (struct GetOrderData *god) char *uri; GNUNET_assert (NULL != god->contract_terms_json); - uri = make_taler_refund_uri (god->contract_terms->merchant_base_url, + GNUNET_assert (NULL != god->contract_terms); + uri = make_taler_refund_uri (god->pc->merchant_base_url, god->order_id); if (NULL == uri) { @@ -1565,7 +1659,9 @@ phase_return_status (struct GetOrderData *god) context = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_incref ("contract_terms", - god->contract_terms_json), + NULL != god->contract_terms_json + ? god->contract_terms_json + : god->order_json), GNUNET_JSON_pack_string ("order_summary", get_order_summary (god)), TALER_JSON_pack_amount ("refund_amount", diff --git a/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c b/src/backend/taler-merchant-httpd_get-private-orders-ORDER_ID.c @@ -263,6 +263,33 @@ struct GetOrderRequestContext struct TALER_MERCHANT_Contract *contract_terms; /** + * Proto-contract. Be careful: do NOT free this + * if @e contract_terms is not NULL! + */ + struct TALER_MERCHANT_ProtoContract *pc; + + /** + * Order terms of the payment we are checking. NULL when we have + * a contract. + */ + json_t *order_json; + + /** + * Parsed order details, NULL when parsing failed or we have a contract. + */ + struct TALER_MERCHANT_Order *order; + + /** + * Common terms of @e order and @e contract. + */ + const struct TALER_MERCHANT_ContractBaseTerms *ct; + + /** + * Timestamp of the contract or order. + */ + struct GNUNET_TIME_Timestamp timestamp; + + /** * Claim token of the order. */ struct TALER_ClaimTokenP claim_token; @@ -515,12 +542,32 @@ gorc_cleanup (void *cls) } if (NULL != gorc->contract_terms_json) + { json_decref (gorc->contract_terms_json); + gorc->contract_terms_json = NULL; + } + if (NULL != gorc->order_json) + { + json_decref (gorc->order_json); + gorc->order_json = NULL; + } if (NULL != gorc->contract_terms) { TALER_MERCHANT_contract_free (gorc->contract_terms); gorc->contract_terms = NULL; + gorc->pc = NULL; /* was an alias! */ + } + if (NULL != gorc->pc) + { + TALER_MERCHANT_proto_contract_free (gorc->pc); + gorc->pc = NULL; } + if (NULL != gorc->order) + { + TALER_MERCHANT_order_free (gorc->order); + gorc->order = NULL; + } + gorc->ct = NULL; /* avoid dangling pointer */ if (NULL != gorc->wire_details) json_decref (gorc->wire_details); if (NULL != gorc->refund_details) @@ -595,7 +642,7 @@ phase_init (struct GetOrderRequestContext *gorc) &resume_by_event, gorc); if ( (NULL != gorc->session_id) && - (NULL != gorc->contract_terms->fulfillment_url) ) + (NULL != gorc->ct->fulfillment_url) ) { struct TMH_SessionEventP session_eh = { .header.size = htons (sizeof (session_eh)), @@ -609,8 +656,8 @@ phase_init (struct GetOrderRequestContext *gorc) GNUNET_CRYPTO_hash (gorc->session_id, strlen (gorc->session_id), &session_eh.h_session_id); - GNUNET_CRYPTO_hash (gorc->contract_terms->fulfillment_url, - strlen (gorc->contract_terms->fulfillment_url), + GNUNET_CRYPTO_hash (gorc->ct->fulfillment_url, + strlen (gorc->ct->fulfillment_url), &session_eh.h_fulfillment_url); gorc->session_eh = TALER_MERCHANTDB_event_listen ( @@ -743,13 +790,13 @@ phase_parse_contract (struct GetOrderRequestContext *gorc) { struct TMH_HandlerContext *hc = gorc->hc; - if (NULL == gorc->contract_terms) + if ( (NULL == gorc->order) && + (NULL != gorc->order_json) ) { - gorc->contract_terms = TALER_MERCHANT_contract_parse ( - gorc->contract_terms_json, - true); + gorc->order = TALER_MERCHANT_order_parse ( + gorc->order_json); - if (NULL == gorc->contract_terms) + if (NULL == gorc->order) { GNUNET_break (0); phase_end (gorc, @@ -760,18 +807,70 @@ phase_parse_contract (struct GetOrderRequestContext *gorc) hc->infix)); return; } + gorc->ct = gorc->order->base; + gorc->timestamp = gorc->order->timestamp; + } + if ( (NULL == gorc->contract_terms) && + (NULL != gorc->contract_terms_json) ) + { + if (NULL == + json_object_get (gorc->contract_terms_json, + "nonce")) + { + /* only have a proto contract */ + gorc->pc = TALER_MERCHANT_proto_contract_parse ( + gorc->contract_terms_json); + + if (NULL == gorc->pc) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + hc->infix)); + return; + } + gorc->ct = gorc->pc->base; + gorc->timestamp = gorc->pc->timestamp; + } + else + { + gorc->contract_terms = TALER_MERCHANT_contract_parse ( + gorc->contract_terms_json); + gorc->pc = gorc->contract_terms->pc; + if (NULL == gorc->contract_terms) + { + GNUNET_break (0); + phase_end (gorc, + TALER_MHD_reply_with_error ( + gorc->sc.con, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + hc->infix)); + return; + } + gorc->ct = gorc->contract_terms->pc->base; + gorc->timestamp = gorc->contract_terms->pc->timestamp; + } } - switch (gorc->contract_terms->version) + switch (gorc->ct->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: - gorc->contract_amount = gorc->contract_terms->details.v0.brutto; + gorc->contract_amount + = (NULL != gorc->pc) + ? gorc->pc->details.v0.brutto + : gorc->order->details.v0.brutto; break; case TALER_MERCHANT_CONTRACT_VERSION_1: if (gorc->choice_index >= 0) { if (gorc->choice_index >= - gorc->contract_terms->details.v1.choices_len) + ( (NULL != gorc->pc) + ? gorc->pc->details.v1.choices_len + : gorc->order->details.v1.choices_len) ) { GNUNET_break (0); phase_end (gorc, @@ -782,17 +881,17 @@ phase_parse_contract (struct GetOrderRequestContext *gorc) NULL)); return; } - gorc->contract_amount = - gorc->contract_terms->details.v1.choices[gorc->choice_index].amount; + (NULL != gorc->pc) + ? gorc->pc->details.v1.choices[gorc->choice_index].amount + : gorc->order->details.v1.choices[gorc->choice_index].amount; } else { GNUNET_break (gorc->order_only); GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Choice index %i for order %s is invalid or not yet available", - gorc->choice_index, - gorc->contract_terms->order_id); + "Choice index %i is invalid", + gorc->choice_index); } break; default: @@ -808,7 +907,7 @@ phase_parse_contract (struct GetOrderRequestContext *gorc) } } - if ( (! gorc->order_only) && + if ( (NULL != gorc->contract_terms) && (GNUNET_OK != TALER_JSON_contract_hash (gorc->contract_terms_json, &gorc->h_contract_terms)) ) @@ -821,8 +920,10 @@ phase_parse_contract (struct GetOrderRequestContext *gorc) NULL)); return; } - GNUNET_assert (NULL != gorc->contract_terms_json); - GNUNET_assert (NULL != gorc->contract_terms); + GNUNET_assert ( (NULL != gorc->contract_terms_json) || + (NULL != gorc->order_json) ); + GNUNET_assert ( (NULL != gorc->pc) || + (NULL != gorc->order) ); gorc->phase++; } @@ -982,7 +1083,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) json_t *reply; if ( (gorc->paid) || - (NULL == gorc->contract_terms->fulfillment_url) || + (NULL == gorc->ct->fulfillment_url) || (NULL == gorc->session_id) ) { /* Repurchase cannot apply */ @@ -992,11 +1093,11 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Running re-purchase detection for %s/%s\n", gorc->session_id, - gorc->contract_terms->fulfillment_url); + gorc->ct->fulfillment_url); qs = TALER_MERCHANTDB_lookup_order_by_fulfillment ( TMH_db, hc->instance->settings.id, - gorc->contract_terms->fulfillment_url, + gorc->ct->fulfillment_url, gorc->session_id, TALER_EXCHANGE_YNA_NO != gorc->allow_refunded_for_repurchase, @@ -1018,7 +1119,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "No already paid order for %s/%s\n", gorc->session_id, - gorc->contract_terms->fulfillment_url); + gorc->ct->fulfillment_url); gorc->phase++; return; } @@ -1062,7 +1163,7 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) GNUNET_JSON_pack_string ("already_paid_order_id", already_paid_order_id), GNUNET_JSON_pack_string ("already_paid_fulfillment_url", - gorc->contract_terms->fulfillment_url), + gorc->ct->fulfillment_url), /* undefined for unpaid v1 contracts */ GNUNET_JSON_pack_allow_null ( TALER_JSON_pack_amount ("total_amount", @@ -1072,11 +1173,13 @@ phase_check_repurchase (struct GetOrderRequestContext *gorc) GNUNET_JSON_pack_object_incref ("proto_contract_terms", gorc->contract_terms_json), GNUNET_JSON_pack_string ("summary", - gorc->contract_terms->summary), + gorc->ct->summary), GNUNET_JSON_pack_timestamp ("pay_deadline", - gorc->contract_terms->pay_deadline), + NULL != gorc->pc + ? gorc->pc->pay_deadline + : gorc->order->pay_deadline), GNUNET_JSON_pack_timestamp ("creation_time", - gorc->contract_terms->timestamp)); + gorc->timestamp)); GNUNET_free (order_status_url); GNUNET_free (taler_pay_uri); @@ -1168,9 +1271,9 @@ phase_unpaid_finish (struct GetOrderRequestContext *gorc) TALER_JSON_pack_amount ("total_amount", &gorc->contract_amount)), GNUNET_JSON_pack_string ("summary", - gorc->contract_terms->summary), + gorc->ct->summary), GNUNET_JSON_pack_timestamp ("creation_time", - gorc->contract_terms->timestamp)); + gorc->timestamp)); check_reply (gorc, reply); json_decref (reply); @@ -1584,7 +1687,7 @@ phase_check_local_transfers (struct GetOrderRequestContext *gorc) TMH_notify_order_change (hc->instance, TMH_OSF_PAID | TMH_OSF_WIRED, - gorc->contract_terms->timestamp, + gorc->timestamp, gorc->order_serial); } } @@ -1630,7 +1733,7 @@ phase_reply_result (struct GetOrderRequestContext *gorc) { GNUNET_break (GNUNET_YES == TALER_amount_is_zero (&gorc->contract_amount)); - gorc->last_payment = gorc->contract_terms->timestamp; + gorc->last_payment = gorc->timestamp; } { json_t *reply; diff --git a/src/backend/taler-merchant-httpd_get-private-orders.c b/src/backend/taler-merchant-httpd_get-private-orders.c @@ -462,7 +462,7 @@ add_order (void *cls, struct GNUNET_TIME_Timestamp creation_time) { struct TMH_PendingOrder *po = cls; - json_t *contract_terms = NULL; + json_t *terms = NULL; struct TALER_PrivateContractHashP h_contract_terms; enum GNUNET_DB_QueryStatus qs; char *order_id = NULL; @@ -470,6 +470,7 @@ add_order (void *cls, bool paid; bool wired; struct TALER_MERCHANT_Contract *contract = NULL; + struct TALER_MERCHANT_Order *order = NULL; int16_t choice_index = -1; struct ProcessRefundsClosure prc = { .ec = TALER_EC_NONE @@ -478,6 +479,7 @@ add_order (void *cls, char amount_buf[128]; char refund_buf[128]; char pending_buf[128]; + const struct TALER_MERCHANT_ContractBaseTerms *ct = NULL; /* Bail early if we already have an error */ if (TALER_EC_NONE != po->result) @@ -522,7 +524,7 @@ add_order (void *cls, po->instance_id, order_id, NULL, - &contract_terms, + &terms, &os, &paid, &wired, @@ -530,7 +532,17 @@ add_order (void *cls, NULL, &choice_index); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { GNUNET_break (os == order_serial); + contract = TALER_MERCHANT_contract_parse (terms); + if (NULL == contract) + { + GNUNET_break (0); + po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; + goto cleanup; + } + ct = contract->pc->base; + } } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { @@ -544,7 +556,18 @@ add_order (void *cls, order_id, NULL, &unused, - &contract_terms); + &terms); + if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) + { + order = TALER_MERCHANT_order_parse (terms); + if (NULL == order) + { + GNUNET_break (0); + po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; + goto cleanup; + } + ct = order->base; + } } if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) { @@ -560,30 +583,23 @@ add_order (void *cls, goto cleanup; } - contract = TALER_MERCHANT_contract_parse (contract_terms, - true); - if (NULL == contract) - { - GNUNET_break (0); - po->result = TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID; - goto cleanup; - } if (paid) { const struct TALER_Amount *brutto; - switch (contract->version) + GNUNET_assert (NULL != contract); + switch (ct->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: - brutto = &contract->details.v0.brutto; + brutto = &contract->pc->details.v0.brutto; break; case TALER_MERCHANT_CONTRACT_VERSION_1: { struct TALER_MERCHANT_ContractChoice *choice - = &contract->details.v1.choices[choice_index]; + = &contract->pc->details.v1.choices[choice_index]; - GNUNET_assert (choice_index < contract->details.v1.choices_len); + GNUNET_assert (choice_index < contract->pc->details.v1.choices_len); brutto = &choice->amount; } break; @@ -617,17 +633,20 @@ add_order (void *cls, } if (0 > TALER_amount_cmp (&prc.total_refund_amount, brutto) && - GNUNET_TIME_absolute_is_future (contract->refund_deadline.abs_time)) + GNUNET_TIME_absolute_is_future ( + contract->pc->refund_deadline.abs_time)) refundable = true; } /* compute amount totals */ amount = NULL; - switch (contract->version) + switch (ct->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: { - amount = &contract->details.v0.brutto; + amount = (NULL != contract) + ? &contract->pc->details.v0.brutto + : &order->details.v0.brutto; if (TALER_amount_is_zero (amount) && (po->of.wired != TALER_EXCHANGE_YNA_ALL) ) @@ -659,11 +678,12 @@ add_order (void *cls, case TALER_MERCHANT_CONTRACT_VERSION_1: if (-1 == choice_index) choice_index = 0; /* default choice */ - GNUNET_assert (choice_index < contract->details.v1.choices_len); + if (NULL != contract) { struct TALER_MERCHANT_ContractChoice *choice - = &contract->details.v1.choices[choice_index]; + = &contract->pc->details.v1.choices[choice_index]; + GNUNET_assert (choice_index < contract->pc->details.v1.choices_len); amount = &choice->amount; /* Accumulate order total */ accumulate_total (po, @@ -680,6 +700,19 @@ add_order (void *cls, goto cleanup; } } + else + { + struct TALER_MERCHANT_OrderChoice *choice + = &order->details.v1.choices[choice_index]; + + GNUNET_assert (choice_index < order->details.v1.choices_len); + amount = &choice->amount; + /* Accumulate order total */ + accumulate_total (po, + amount); + if (TALER_EC_NONE != po->result) + goto cleanup; + } break; default: GNUNET_break (0); @@ -709,7 +742,7 @@ add_order (void *cls, po->pa, GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("order_id", - contract->order_id), + order_id), GNUNET_JSON_pack_uint64 ("row_id", order_serial), GNUNET_JSON_pack_timestamp ("timestamp", @@ -729,7 +762,7 @@ add_order (void *cls, ? &prc.pending_refund_amount : NULL)), GNUNET_JSON_pack_string ("summary", - contract->summary), + ct->summary), GNUNET_JSON_pack_bool ("refundable", refundable), GNUNET_JSON_pack_bool ("paid", @@ -737,7 +770,7 @@ add_order (void *cls, break; case POF_CSV: { - size_t len = strlen (contract->summary); + size_t len = strlen (ct->summary); size_t wpos = 0; char *esummary; struct tm *tm; @@ -747,16 +780,16 @@ add_order (void *cls, esummary = GNUNET_malloc (2 * len + 1); for (size_t off = 0; off<len; off++) { - if ('"' == contract->summary[off]) + if ('"' == ct->summary[off]) esummary[wpos++] = '"'; - esummary[wpos++] = contract->summary[off]; + esummary[wpos++] = ct->summary[off]; } t = GNUNET_TIME_timestamp_to_s (creation_time); tm = localtime (&t); GNUNET_buffer_write_fstr ( &po->csv, "%s,%llu,%04u-%02u-%02u,%02u:%02u (%s),%llu,%s,%s,%s,\"%s\",%s,%s\r\n", - contract->order_id, + order_id, (unsigned long long) order_serial, tm->tm_year + 1900, tm->tm_mon + 1, @@ -776,7 +809,7 @@ add_order (void *cls, } case POF_XML: { - char *esummary = TALER_escape_xml (contract->summary); + char *esummary = TALER_escape_xml (ct->summary); char creation_time_s[128]; const struct tm *tm; time_t tt; @@ -797,7 +830,7 @@ add_order (void *cls, "<Cell><Data ss:Type=\"String\">%s</Data></Cell>" "<Cell ss:Formula=\"=%s()\"><Data ss:Type=\"Boolean\">%s</Data></Cell>" "</Row>\n", - contract->order_id, + order_id, creation_time_s, amount_buf, paid ? refund_buf : "", @@ -810,8 +843,13 @@ add_order (void *cls, } /* end switch po->format */ cleanup: - json_decref (contract_terms); + json_decref (terms); GNUNET_free (order_id); + if (NULL != order) + { + TALER_MERCHANT_order_free (order); + order = NULL; + } if (NULL != contract) { TALER_MERCHANT_contract_free (contract); diff --git a/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ORDER_ID-pay.c @@ -1059,7 +1059,7 @@ batch_deposit_transaction ( dr->details.ok.deposit_timestamp, &pc->check_contract.h_contract_terms, eg->exchange_url, - pc->check_contract.contract_terms->wire_deadline, + pc->check_contract.contract_terms->pc->wire_deadline, &dr->details.ok.accumulated_total_without_fee, &eg->wire_fee, &pc->check_contract.wm->h_wire, @@ -1096,7 +1096,7 @@ batch_deposit_transaction ( &dc->deposit_fee, &dc->refund_fee, GNUNET_TIME_absolute_add ( - pc->check_contract.contract_terms->wire_deadline.abs_time, + pc->check_contract.contract_terms->pc->wire_deadline.abs_time, GNUNET_TIME_randomize (GNUNET_TIME_UNIT_MINUTES))); if (qs < 0) return qs; @@ -1220,8 +1220,8 @@ notify_kyc_required (const struct ExchangeGroup *eg) char *extra; hws = GNUNET_STRINGS_data_to_string_alloc ( - &eg->pc->check_contract.contract_terms->h_wire, - sizeof (eg->pc->check_contract.contract_terms->h_wire)); + &eg->pc->check_contract.contract_terms->pc->h_wire, + sizeof (eg->pc->check_contract.contract_terms->pc->h_wire)); GNUNET_asprintf (&extra, "%s %s", hws, @@ -1413,16 +1413,24 @@ do_batch_deposits (struct ExchangeGroup *eg) all coins are done. */ { struct TALER_EXCHANGE_DepositContractDetail dcd = { - .wire_deadline = pc->check_contract.contract_terms->wire_deadline, - .merchant_payto_uri = pc->check_contract.wm->payto_uri, + .wire_deadline + = pc->check_contract.contract_terms->pc->wire_deadline, + .merchant_payto_uri + = pc->check_contract.wm->payto_uri, .extra_wire_subject_metadata = pc->check_contract.wm->extra_wire_subject_metadata, - .wire_salt = pc->check_contract.wm->wire_salt, - .h_contract_terms = pc->check_contract.h_contract_terms, - .wallet_data_hash = pc->parse_wallet_data.h_wallet_data, - .wallet_timestamp = pc->check_contract.contract_terms->timestamp, - .merchant_pub = hc->instance->merchant_pub, - .refund_deadline = pc->check_contract.contract_terms->refund_deadline + .wire_salt + = pc->check_contract.wm->wire_salt, + .h_contract_terms + = pc->check_contract.h_contract_terms, + .wallet_data_hash + = pc->parse_wallet_data.h_wallet_data, + .wallet_timestamp + = pc->check_contract.contract_terms->pc->timestamp, + .merchant_pub + = hc->instance->merchant_pub, + .refund_deadline + = pc->check_contract.contract_terms->pc->refund_deadline }; /* Collect up to TALER_MAX_COINS eligible coins for this batch */ struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; @@ -1674,7 +1682,7 @@ process_pay_with_keys ( is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); if (is_age_restricted_denom && - (0 < pc->check_contract.contract_terms->minimum_age)) + (0 < pc->check_contract.contract_terms->pc->base->minimum_age)) { /* Minimum age given and restricted coin provided: We need to verify the * minimum age */ @@ -1698,7 +1706,7 @@ process_pay_with_keys ( if (GNUNET_OK != TALER_age_commitment_verify ( &dc->age_commitment, - pc->check_contract.contract_terms->minimum_age, + pc->check_contract.contract_terms->pc->base->minimum_age, &dc->minimum_age_sig)) code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; AGE_FAIL: @@ -1929,11 +1937,11 @@ phase_success_response (struct PayContext *pc) /* Build the response */ pos_confirmation = (NULL == pc->check_contract.pos_key) ? NULL - : TALER_build_pos_confirmation (pc->check_contract.pos_key, - pc->check_contract.pos_alg, - &pc->validate_tokens.brutto, - pc->check_contract.contract_terms->timestamp - ); + : TALER_build_pos_confirmation ( + pc->check_contract.pos_key, + pc->check_contract.pos_alg, + &pc->validate_tokens.brutto, + pc->check_contract.contract_terms->pc->timestamp); pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, @@ -1996,7 +2004,7 @@ phase_payment_notification (struct PayContext *pc) 0); } if ( (NULL != pc->parse_pay.session_id) && - (NULL != pc->check_contract.contract_terms->fulfillment_url) ) + (NULL != pc->check_contract.contract_terms->pc->base->fulfillment_url) ) { struct TMH_SessionEventP session_eh = { .header.size = htons (sizeof (session_eh)), @@ -2007,14 +2015,14 @@ phase_payment_notification (struct PayContext *pc) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about session change to %s for %s\n", pc->parse_pay.session_id, - pc->check_contract.contract_terms->fulfillment_url); + pc->check_contract.contract_terms->pc->base->fulfillment_url); GNUNET_CRYPTO_hash (pc->parse_pay.session_id, strlen (pc->parse_pay.session_id), &session_eh.h_session_id); - GNUNET_CRYPTO_hash (pc->check_contract.contract_terms->fulfillment_url, - strlen (pc->check_contract.contract_terms-> - fulfillment_url), - &session_eh.h_fulfillment_url); + GNUNET_CRYPTO_hash ( + pc->check_contract.contract_terms->pc->base->fulfillment_url, + strlen (pc->check_contract.contract_terms->pc->base->fulfillment_url), + &session_eh.h_fulfillment_url); TALER_MERCHANTDB_event_notify (TMH_db, &session_eh.header, NULL, @@ -2396,10 +2404,10 @@ phase_compute_money_pots (struct PayContext *pc) TALER_amount_set_zero (pc->parse_pay.dc[0].cdd.amount.currency, &assigned)); GNUNET_assert (NULL != contract); - for (size_t i = 0; i<contract->products_len; i++) + for (size_t i = 0; i<contract->pc->products_len; i++) { const struct TALER_MERCHANT_ProductSold *product - = &contract->products[i]; + = &contract->pc->products[i]; const struct TALER_Amount *price = NULL; /* find price in the right currency */ @@ -2460,14 +2468,14 @@ phase_compute_money_pots (struct PayContext *pc) } if ( (! TALER_amount_is_zero (&left)) && - (0 != contract->default_money_pot) ) + (0 != contract->pc->base->default_money_pot) ) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Computing money pot %llu increment as %s\n", - (unsigned long long) contract->default_money_pot, + (unsigned long long) contract->pc->base->default_money_pot, TALER_amount2s (&left)); increment_pot (pc, - contract->default_money_pot, + contract->pc->base->default_money_pot, &left); } } @@ -3124,7 +3132,7 @@ phase_execute_pay_transaction (struct PayContext *pc) { const struct TALER_MERCHANT_ContractChoice *choice = - &pc->check_contract.contract_terms->details.v1 + &pc->check_contract.contract_terms->pc->details.v1 .choices[pc->parse_wallet_data.choice_index]; for (size_t i = 0; i<pc->output_tokens_len; i++) @@ -3192,10 +3200,11 @@ phase_execute_pay_transaction (struct PayContext *pc) } } - TMH_notify_order_change (hc->instance, - TMH_OSF_CLAIMED | TMH_OSF_PAID, - pc->check_contract.contract_terms->timestamp, - pc->check_contract.order_serial); + TMH_notify_order_change ( + hc->instance, + TMH_OSF_CLAIMED | TMH_OSF_PAID, + pc->check_contract.contract_terms->pc->timestamp, + pc->check_contract.order_serial); { enum GNUNET_DB_QueryStatus qs; json_t *jhook; @@ -3531,11 +3540,11 @@ find_family (const struct PayContext *pc, const char *slug) { for (unsigned int i = 0; - i < pc->check_contract.contract_terms->details.v1.token_authorities_len; + i < pc->check_contract.contract_terms->pc->details.v1.token_authorities_len; i++) { const struct TALER_MERCHANT_ContractTokenFamily *tfi - = &pc->check_contract.contract_terms->details.v1.token_authorities[i]; + = &pc->check_contract.contract_terms->pc->details.v1.token_authorities[i]; if (0 == strcmp (tfi->slug, slug)) @@ -3613,8 +3622,8 @@ handle_output_token (struct PayContext *pc, TMH_db, pc->hc->instance->settings.id, family->slug, - pc->check_contract.contract_terms->timestamp, - pc->check_contract.contract_terms->pay_deadline, + pc->check_contract.contract_terms->pc->timestamp, + pc->check_contract.contract_terms->pc->pay_deadline, &details); switch (qs) { @@ -3637,9 +3646,9 @@ handle_output_token (struct PayContext *pc, "Token-family key for %s not found at [%llu,%llu]\n", family->slug, (unsigned long long) - pc->check_contract.contract_terms->timestamp.abs_time.abs_value_us, + pc->check_contract.contract_terms->pc->timestamp.abs_time.abs_value_us, (unsigned long long) - pc->check_contract.contract_terms->pay_deadline.abs_time.abs_value_us + pc->check_contract.contract_terms->pc->pay_deadline.abs_time.abs_value_us ); GNUNET_break (0); pay_end (pc, @@ -3837,20 +3846,20 @@ phase_validate_tokens (struct PayContext *pc) /* We haven't seen a donau output yet. */ pc->validate_tokens.donau_output_index = -1; - switch (pc->check_contract.contract_terms->version) + switch (pc->check_contract.contract_terms->pc->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: /* No tokens to validate */ pc->phase = PP_COMPUTE_MONEY_POTS; pc->validate_tokens.max_fee - = pc->check_contract.contract_terms->details.v0.max_fee; + = pc->check_contract.contract_terms->pc->details.v0.max_fee; pc->validate_tokens.brutto - = pc->check_contract.contract_terms->details.v0.brutto; + = pc->check_contract.contract_terms->pc->details.v0.brutto; break; case TALER_MERCHANT_CONTRACT_VERSION_1: { const struct TALER_MERCHANT_ContractChoice *selected - = &pc->check_contract.contract_terms->details.v1.choices[ + = &pc->check_contract.contract_terms->pc->details.v1.choices[ pc->parse_wallet_data.choice_index]; unsigned int output_off; unsigned int cnt; @@ -4175,8 +4184,8 @@ append_output_token_sig (void *cls, 0, sizeof (out)); GNUNET_assert (TALER_MERCHANT_CONTRACT_VERSION_1 == - pc->check_contract.contract_terms->version); - choice = &pc->check_contract.contract_terms->details.v1 + pc->check_contract.contract_terms->pc->base->version); + choice = &pc->check_contract.contract_terms->pc->details.v1 .choices[pc->parse_wallet_data.choice_index]; output = &choice->outputs[pc->output_index_gen]; cnt = count_output_tokens (pc, @@ -4374,15 +4383,16 @@ phase_check_contract (struct PayContext *pc) TALER_MERCHANT_contract_free (pc->check_contract.contract_terms); pc->check_contract.contract_terms = NULL; } - qs = TALER_MERCHANTDB_lookup_contract_terms2 (TMH_db, - pc->hc->instance->settings.id, - pc->order_id, - &pc->check_contract.contract_terms_json, - &pc->check_contract.order_serial, - &paid, - NULL, - &pc->check_contract.pos_key, - &pc->check_contract.pos_alg); + qs = TALER_MERCHANTDB_lookup_contract_terms2 ( + TMH_db, + pc->hc->instance->settings.id, + pc->order_id, + &pc->check_contract.contract_terms_json, + &pc->check_contract.order_serial, + &paid, + NULL, + &pc->check_contract.pos_key, + &pc->check_contract.pos_alg); if (0 > qs) { /* single, read-only SQL statements should never cause @@ -4432,8 +4442,7 @@ phase_check_contract (struct PayContext *pc) as later phases need it. */ pc->check_contract.contract_terms = TALER_MERCHANT_contract_parse ( - pc->check_contract.contract_terms_json, - true); + pc->check_contract.contract_terms_json); if (NULL == pc->check_contract.contract_terms) { @@ -4463,7 +4472,7 @@ phase_check_contract (struct PayContext *pc) /* Check fundamentals */ { - switch (pc->check_contract.contract_terms->version) + switch (pc->check_contract.contract_terms->pc->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: { @@ -4498,14 +4507,15 @@ phase_check_contract (struct PayContext *pc) return; } if (pc->parse_wallet_data.choice_index >= - pc->check_contract.contract_terms->details.v1.choices_len) + pc->check_contract.contract_terms->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 %d\n", - pc->order_id, - pc->check_contract.contract_terms->details.v1.choices_len, - pc->parse_wallet_data.choice_index); + GNUNET_log ( + GNUNET_ERROR_TYPE_INFO, + "Order `%s' has choices array with %u elements but " + "request has 'choice_index' field with value %d\n", + pc->order_id, + pc->check_contract.contract_terms->pc->details.v1.choices_len, + pc->parse_wallet_data.choice_index); GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( @@ -4530,11 +4540,10 @@ phase_check_contract (struct PayContext *pc) } } - if (GNUNET_TIME_timestamp_cmp (pc->check_contract.contract_terms-> - wire_deadline, - <, - pc->check_contract.contract_terms-> - refund_deadline)) + if (GNUNET_TIME_timestamp_cmp ( + pc->check_contract.contract_terms->pc->wire_deadline, + <, + pc->check_contract.contract_terms->pc->refund_deadline)) { /* This should already have been checked when creating the order! */ GNUNET_break (0); @@ -4546,8 +4555,8 @@ phase_check_contract (struct PayContext *pc) NULL)); return; } - if (GNUNET_TIME_absolute_is_past (pc->check_contract.contract_terms-> - pay_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_past ( + pc->check_contract.contract_terms->pc->pay_deadline.abs_time)) { /* too late */ pay_end (pc, @@ -4564,8 +4573,10 @@ phase_check_contract (struct PayContext *pc) struct TMH_WireMethod *wm; wm = pc->hc->instance->wm_head; - while (0 != GNUNET_memcmp (&pc->check_contract.contract_terms->h_wire, - &wm->h_wire)) + while (0 != + GNUNET_memcmp ( + &pc->check_contract.contract_terms->pc->h_wire, + &wm->h_wire)) wm = wm->next; if (NULL == wm) { diff --git a/src/backend/taler-merchant-httpd_post-private-orders.c b/src/backend/taler-merchant-httpd_post-private-orders.c @@ -318,169 +318,20 @@ struct OrderContext { /** - * Our order ID. + * The main order data as provided by the client. */ - char *order_id; + struct TALER_MERCHANT_Order *order; /** - * Summary of the contract. - */ - const char *summary; - - /** - * Internationalized summary. - */ - const json_t *summary_i18n; - - /** - * URL that will show that the contract was successful - * after it has been paid for. - */ - const char *fulfillment_url; - - /** - * Message shown to the customer after paying for the contract. - * Either fulfillment_url or fulfillment_message must be specified. - */ - const char *fulfillment_message; - - /** - * Map from IETF BCP 47 language tags to localized fulfillment messages. - */ - const json_t *fulfillment_message_i18n; - - /** - * Length of the @e products array. - */ - size_t products_len; - - /** - * Array of products that are being sold. - */ - struct TALER_MERCHANT_ProductSold *products; - - /** - * URL where the same contract could be ordered again (if available). - */ - const char *public_reorder_url; - - /** - * Merchant base URL. + * Base URL of this merchant. */ char *merchant_base_url; /** - * Timestamp of the order. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Deadline for refunds. - */ - struct GNUNET_TIME_Timestamp refund_deadline; - - /** - * Payment deadline. - */ - struct GNUNET_TIME_Timestamp pay_deadline; - - /** - * Wire transfer deadline. - */ - struct GNUNET_TIME_Timestamp wire_deadline; - - /** * Wire transfer round-up interval to apply. */ enum GNUNET_TIME_RounderInterval wire_deadline_rounder; - /** - * Delivery date. - */ - struct GNUNET_TIME_Timestamp delivery_date; - - /** - * Delivery location. - */ - const json_t *delivery_location; - - /** - * Specifies for how long the wallet should try to get an - * automatic refund for the purchase. - */ - struct GNUNET_TIME_Relative auto_refund; - - /** - * Nonce generated by the wallet and echoed by the merchant - * in this field when the proposal is generated. - */ - const char *nonce; - - /** - * Extra data that is only interpreted by the merchant frontend. - */ - const json_t *extra; - - /** - * Minimum age required by the order. - */ - uint32_t minimum_age; - - /** - * Money pot to increment for whatever order payment amount - * is not yet assigned to a pot via the Product. - */ - uint64_t order_default_money_pot; - - /** - * Version of the contract terms. - */ - enum TALER_MERCHANT_ContractVersion 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; - - /** - * Tip included by the customer (part of the total amount). - */ - struct TALER_Amount tip; - - /** - * True if @e tip was not provided. - */ - bool no_tip; - - /** - * 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; /** @@ -956,14 +807,20 @@ clean_order (void *cls) json_decref (oc->set_exchanges.exchange_rejections); oc->set_exchanges.exchange_rejections = NULL; } - switch (oc->parse_order.version) + if (NULL != oc->parse_order.order) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - GNUNET_free (oc->set_max_fee.details.v1.max_fees); - GNUNET_free (oc->set_exchanges.details.v1.max_stefan_fees); - break; + switch (oc->parse_order.order->base->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + GNUNET_free (oc->set_max_fee.details.v1.max_fees); + GNUNET_free (oc->set_exchanges.details.v1.max_stefan_fees); + break; + } + TALER_MERCHANT_order_free (oc->parse_order.order); + oc->parse_order.order = NULL; + GNUNET_free (oc->parse_order.merchant_base_url); } if (NULL != oc->merge_inventory.products) { @@ -977,12 +834,6 @@ clean_order (void *cls) GNUNET_array_grow (oc->parse_choices.choices, oc->parse_choices.choices_len, 0); - for (size_t i = 0; i<oc->parse_order.products_len; i++) - { - TALER_MERCHANT_product_sold_free (&oc->parse_order.products[i]); - } - GNUNET_free (oc->parse_order.products); - oc->parse_order.products_len = 0; for (unsigned int i = 0; i<oc->parse_choices.token_families_len; i++) { struct TALER_MERCHANT_ContractTokenFamily *mctf @@ -1028,8 +879,6 @@ clean_order (void *cls) GNUNET_free (oc->parse_request.pos_key); json_decref (oc->parse_request.order); json_decref (oc->serialize_order.contract); - GNUNET_free (oc->parse_order.order_id); - GNUNET_free (oc->parse_order.merchant_base_url); GNUNET_free (oc); } @@ -1126,7 +975,7 @@ execute_transaction (struct OrderContext *oc) qs = TALER_MERCHANTDB_lookup_order (TMH_db, oc->hc->instance->settings.id, - oc->parse_order.order_id, + oc->parse_order.order->order_id, &oc->execute_order.token, &orig_post, &contract_terms); @@ -1160,10 +1009,10 @@ execute_transaction (struct OrderContext *oc) /* Setup order */ qs = TALER_MERCHANTDB_insert_order (TMH_db, oc->hc->instance->settings.id, - oc->parse_order.order_id, + oc->parse_order.order->order_id, oc->parse_request.session_id, &oc->parse_request.h_post_data, - oc->parse_order.pay_deadline, + oc->parse_order.order->pay_deadline, &oc->parse_request.claim_token, oc->serialize_order.contract, /* called 'contract terms' at database. */ oc->parse_request.pos_key, @@ -1196,7 +1045,7 @@ execute_transaction (struct OrderContext *oc) qs = TALER_MERCHANTDB_insert_order_lock ( TMH_db, oc->hc->instance->settings.id, - oc->parse_order.order_id, + oc->parse_order.order->order_id, oc->parse_request.inventory_products[i].product_id, oc->parse_request.inventory_products[i].quantity, oc->parse_request.inventory_products[i].quantity_frac); @@ -1217,11 +1066,12 @@ execute_transaction (struct OrderContext *oc) /* Get the order serial and timestamp for the order we just created to update long-poll clients. */ - qs = TALER_MERCHANTDB_lookup_order_summary (TMH_db, - oc->hc->instance->settings.id, - oc->parse_order.order_id, - &timestamp, - &order_serial); + qs = TALER_MERCHANTDB_lookup_order_summary ( + TMH_db, + oc->hc->instance->settings.id, + oc->parse_order.order->order_id, + &timestamp, + &order_serial); if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs) { TALER_MERCHANTDB_rollback (TMH_db); @@ -1233,7 +1083,7 @@ execute_transaction (struct OrderContext *oc) jhook = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), + oc->parse_order.order->order_id), GNUNET_JSON_pack_object_incref ("contract", oc->serialize_order.contract), GNUNET_JSON_pack_string ("instance_id", @@ -1287,9 +1137,9 @@ yield_success_response (struct OrderContext *oc, oc->connection, MHD_HTTP_OK, GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), + oc->parse_order.order->order_id), GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), + oc->parse_order.order->pay_deadline), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_data_auto ( "token", @@ -1315,7 +1165,7 @@ phase_execute_order (struct OrderContext *oc) GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Executing database transaction to create order '%s' for instance '%s'\n", - oc->parse_order.order_id, + oc->parse_order.order->order_id, settings->id); for (unsigned int i = 0; i<MAX_RETRIES; i++) { @@ -1344,7 +1194,7 @@ phase_execute_order (struct OrderContext *oc) oc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, - oc->parse_order.order_id); + oc->parse_order.order->order_id); return; } /* Other hard transaction error (disk full, etc.) */ @@ -1372,7 +1222,7 @@ phase_execute_order (struct OrderContext *oc) oc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_ALREADY_EXISTS, - oc->parse_order.order_id); + oc->parse_order.order->order_id); return; } @@ -1797,7 +1647,7 @@ add_input_token_family (struct OrderContext *oc, const char *slug) { struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); - struct GNUNET_TIME_Timestamp end = oc->parse_order.pay_deadline; + struct GNUNET_TIME_Timestamp end = oc->parse_order.order->pay_deadline; enum GNUNET_DB_QueryStatus qs; enum TALER_ErrorCode ec = TALER_EC_INVALID; /* make compiler happy */ unsigned int http_status = 0; /* make compiler happy */ @@ -1945,12 +1795,13 @@ add_output_token_family (struct OrderContext *oc, valid_at, key_index)) ) return GNUNET_OK; - qs = TALER_MERCHANTDB_lookup_token_family_key (TMH_db, - oc->hc->instance->settings.id, - slug, - valid_at, - oc->parse_order.pay_deadline, - &key_details); + qs = TALER_MERCHANTDB_lookup_token_family_key ( + TMH_db, + oc->hc->instance->settings.id, + slug, + valid_at, + oc->parse_order.order->pay_deadline, + &key_details); switch (qs) { case GNUNET_DB_STATUS_HARD_ERROR: @@ -2224,10 +2075,8 @@ output_contract_choices (struct OrderContext *oc) GNUNET_assert (0 == json_array_append_new ( choices, TALER_MERCHANT_json_from_contract_choice ( - &oc->parse_choices.choices[i], - false))); + &oc->parse_choices.choices[i]))); } - return choices; } @@ -2291,83 +2140,61 @@ phase_serialize_order (struct OrderContext *oc) } oc->serialize_order.contract = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_int64 ("version", - oc->parse_order.version), - 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_allow_null ( - GNUNET_JSON_pack_string ("public_reorder_url", - oc->parse_order.public_reorder_url)), - GNUNET_JSON_pack_allow_null ( - 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_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - oc->parse_order.fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_uint64 ("minimum_age", - oc->parse_order.minimum_age)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_uint64 ("default_money_pot", - oc->parse_order.order_default_money_pot)), - GNUNET_JSON_pack_array_incref ("products", - oc->merge_inventory.products), - GNUNET_JSON_pack_data_auto ("h_wire", - &oc->select_wire_method.wm->h_wire), - GNUNET_JSON_pack_string ("wire_method", - oc->select_wire_method.wm->wire_method), - GNUNET_JSON_pack_string ("order_id", - oc->parse_order.order_id), - GNUNET_JSON_pack_timestamp ("timestamp", - oc->parse_order.timestamp), - GNUNET_JSON_pack_timestamp ("pay_deadline", - oc->parse_order.pay_deadline), - GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", - oc->parse_order.wire_deadline), - GNUNET_JSON_pack_allow_null ( - 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_string ("merchant_base_url", - oc->parse_order.merchant_base_url), - GNUNET_JSON_pack_object_steal ("merchant", - merchant), - GNUNET_JSON_pack_data_auto ("merchant_pub", - &oc->hc->instance->merchant_pub), - GNUNET_JSON_pack_array_incref ("exchanges", - oc->select_wire_method.exchanges), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("extra", - (json_t *) oc->parse_order.extra)) - ); + GNUNET_JSON_pack_string ( + "order_id", + oc->parse_order.order->order_id), + GNUNET_JSON_pack_object_steal ( + NULL, + TALER_MERCHANT_base_terms_serialize (oc->parse_order.order->base)), + GNUNET_JSON_pack_array_incref ( + "products", + oc->merge_inventory.products), + GNUNET_JSON_pack_data_auto ( + "h_wire", + &oc->select_wire_method.wm->h_wire), + GNUNET_JSON_pack_string ( + "wire_method", + oc->select_wire_method.wm->wire_method), + GNUNET_JSON_pack_timestamp ( + "timestamp", + oc->parse_order.order->timestamp), + GNUNET_JSON_pack_timestamp ( + "pay_deadline", + oc->parse_order.order->pay_deadline), + GNUNET_JSON_pack_timestamp ( + "wire_transfer_deadline", + oc->parse_order.order->wire_transfer_deadline), + GNUNET_JSON_pack_string ( + "merchant_base_url", + oc->parse_order.merchant_base_url), + GNUNET_JSON_pack_object_steal ( + "merchant", + merchant), + GNUNET_JSON_pack_data_auto ( + "merchant_pub", + &oc->hc->instance->merchant_pub), + GNUNET_JSON_pack_array_incref ( + "exchanges", + oc->select_wire_method.exchanges)); { json_t *xtra; - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: xtra = GNUNET_JSON_PACK ( TALER_JSON_pack_amount ("max_fee", &oc->set_max_fee.details.v0.max_fee), GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - oc->parse_order.details.v0.no_tip + TALER_JSON_pack_amount ( + "tip", + oc->parse_order.order->details.v0.no_tip ? NULL - : &oc->parse_order.details.v0.tip)), - TALER_JSON_pack_amount ("amount", - &oc->parse_order.details.v0.brutto)); + : &oc->parse_order.order->details.v0.tip)), + TALER_JSON_pack_amount ( + "amount", + &oc->parse_order.order->details.v0.brutto)); break; case TALER_MERCHANT_CONTRACT_VERSION_1: { @@ -2399,19 +2226,22 @@ phase_serialize_order (struct OrderContext *oc) /* 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))); + json_object_set_new ( + oc->serialize_order.contract, + "refund_deadline", + GNUNET_JSON_from_timestamp ( + oc->parse_order.order->refund_deadline))); /* auto_refund should only be set if it is not 0 */ - if (! GNUNET_TIME_relative_is_zero (oc->parse_order.auto_refund)) + if (! GNUNET_TIME_relative_is_zero ( + oc->parse_order.order->base->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", - GNUNET_JSON_from_time_rel ( - oc->parse_order.auto_refund))); + json_object_set_new ( + oc->serialize_order.contract, + "auto_refund", + GNUNET_JSON_from_time_rel ( + oc->parse_order.order->base->auto_refund))); } oc->phase++; @@ -2472,12 +2302,12 @@ compute_fee (struct OrderContext *oc, static void phase_set_max_fee (struct OrderContext *oc) { - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: compute_fee (oc, - &oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.max_fee, + &oc->parse_order.order->details.v0.brutto, + &oc->parse_order.order->details.v0.max_fee, &oc->set_exchanges.details.v0.max_stefan_fee, &oc->set_max_fee.details.v0.max_fee); break; @@ -2525,11 +2355,11 @@ phase_select_wire_method (struct OrderContext *oc) { unsigned int num_choices = 0; - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: want_choices = 1; - ea = &oc->parse_order.details.v0.brutto; + ea = &oc->parse_order.order->details.v0.brutto; if (TALER_amount_is_zero (ea) || TALER_amount_set_test_above (&wmc->total_exchange_limits, ea)) @@ -2714,11 +2544,11 @@ static void update_stefan (struct OrderContext *oc, const struct TALER_EXCHANGE_Keys *keys) { - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: compute_stefan_fee (keys, - &oc->parse_order.details.v0.brutto, + &oc->parse_order.order->details.v0.brutto, &oc->set_exchanges.details.v0.max_stefan_fee); break; case TALER_MERCHANT_CONTRACT_VERSION_1: @@ -3219,14 +3049,14 @@ static void phase_add_payment_details (struct OrderContext *oc) { /* First, determine the maximum amounts that could be paid per currency */ - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: GNUNET_array_append (oc->add_payment_details.max_choice_limits, oc->add_payment_details.num_max_choice_limits, - oc->parse_order.details.v0.brutto); + oc->parse_order.order->details.v0.brutto); if (! TALER_amount_is_zero ( - &oc->parse_order.details.v0.brutto)) + &oc->parse_order.order->details.v0.brutto)) { oc->add_payment_details.need_exchange = true; } @@ -3242,7 +3072,8 @@ phase_add_payment_details (struct OrderContext *oc) { oc->add_payment_details.need_exchange = true; } - for (unsigned int j = 0; j<oc->add_payment_details.num_max_choice_limits; + for (unsigned int j = 0; + j<oc->add_payment_details.num_max_choice_limits; j++) { struct TALER_Amount *mx = &oc->add_payment_details.max_choice_limits[j]; @@ -3344,25 +3175,26 @@ uint64_cmp (const void *a, static void phase_merge_inventory (struct OrderContext *oc) { - uint64_t pots[oc->parse_order.products_len + 1]; + uint64_t pots[oc->parse_order.order->products_len + 1]; size_t pots_off = 0; - if (0 != oc->parse_order.order_default_money_pot) - pots[pots_off++] = oc->parse_order.order_default_money_pot; + if (0 != oc->parse_order.order->base->default_money_pot) + pots[pots_off++] = oc->parse_order.order->base->default_money_pot; /** * parse_request.inventory_products => instructions to add products to contract terms * parse_order.products => contains products that are not from the backend-managed inventory. */ oc->merge_inventory.products = json_array (); - for (size_t i = 0; i<oc->parse_order.products_len; i++) + for (size_t i = 0; i<oc->parse_order.order->products_len; i++) { GNUNET_assert ( 0 == json_array_append_new ( oc->merge_inventory.products, - TALER_MERCHANT_product_sold_serialize (&oc->parse_order.products[i]))); - if (0 != oc->parse_order.products[i].product_money_pot) - pots[pots_off++] = oc->parse_order.products[i].product_money_pot; + TALER_MERCHANT_product_sold_serialize ( + &oc->parse_order.order->products[i]))); + if (0 != oc->parse_order.order->products[i].product_money_pot) + pots[pots_off++] = oc->parse_order.order->products[i].product_money_pot; } /* make sure pots array only has distinct elements */ @@ -3486,8 +3318,8 @@ phase_merge_inventory (struct OrderContext *oc) return; } GNUNET_free (categories); - oc->parse_order.minimum_age - = GNUNET_MAX (oc->parse_order.minimum_age, + oc->parse_order.order->base->minimum_age + = GNUNET_MAX (oc->parse_order.order->base->minimum_age, pd.minimum_age); { const char *eparam; @@ -3538,7 +3370,7 @@ phase_merge_inventory (struct OrderContext *oc) .prices_are_net = pd.price_is_net, .image = pd.image, .taxes = pd.taxes, - .delivery_date = oc->parse_order.delivery_date, + .delivery_date = oc->parse_order.order->base->delivery_date, .product_money_pot = pd.money_pot_id, .unit = pd.unit, @@ -3665,9 +3497,7 @@ add_donau_output (struct OrderContext *oc, static void phase_parse_choices (struct OrderContext *oc) { - const json_t *jchoices; - - switch (oc->parse_order.version) + switch (oc->parse_order.order->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: oc->phase++; @@ -3679,245 +3509,157 @@ phase_parse_choices (struct OrderContext *oc) GNUNET_assert (0); } - jchoices = oc->parse_order.details.v1.choices; - - if (! json_is_array (jchoices)) - GNUNET_assert (0); - if (0 == json_array_size (jchoices)) - { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "choices"); - return; - } + /* Convert order choices to contract choices */ GNUNET_array_grow (oc->parse_choices.choices, oc->parse_choices.choices_len, - json_array_size (jchoices)); + oc->parse_order.order->details.v1.choices_len); for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { - struct TALER_MERCHANT_ContractChoice *choice + const struct TALER_MERCHANT_OrderChoice *ochoice + = &oc->parse_order.order->details.v1.choices[i]; + struct TALER_MERCHANT_ContractChoice *cchoice = &oc->parse_choices.choices[i]; - const char *error_name; - 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 ("tip", - &choice->tip), - &choice->no_tip), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("max_fee", - &choice->max_fee), - &no_fee), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("description", - &choice->description), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("description_i18n", - &choice->description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("inputs", - &jinputs), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("outputs", - &joutputs), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; - - ret = GNUNET_JSON_parse (json_array_get (jchoices, - i), - spec, - &error_name, - &error_line); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Choice parsing failed: %s:%u\n", - error_name, - error_line); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "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 ( (! choice->no_tip) && - (GNUNET_OK != - TALER_amount_cmp_currency (&choice->amount, - &choice->tip)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "tip and amount"); - return; - } + unsigned int off; if (! TMH_test_exchange_configured_for_currency ( - choice->amount.currency)) + ochoice->amount.currency)) { GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); reply_with_error (oc, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - choice->amount.currency); + ochoice->amount.currency); return; } - - if (NULL != jinputs) + cchoice->amount = ochoice->amount; + cchoice->tip = ochoice->tip; + cchoice->no_tip = ochoice->no_tip; + if (NULL != ochoice->description) + cchoice->description = GNUNET_strdup (ochoice->description); + if (NULL != ochoice->description_i18n) + cchoice->description_i18n = json_incref (ochoice->description_i18n); + cchoice->max_fee = ochoice->max_fee; + + /* convert inputs */ + GNUNET_array_grow (cchoice->inputs, + cchoice->inputs_len, + ochoice->inputs_len); + off = 0; + for (unsigned int j = 0; j < ochoice->inputs_len; j++) { - const json_t *jinput; - size_t idx; - json_array_foreach ((json_t *) jinputs, idx, jinput) - { - struct TALER_MERCHANT_ContractInput input = { - .details.token.count = 1 - }; + const struct TALER_MERCHANT_OrderInput *order_input + = &ochoice->inputs[j]; + struct TALER_MERCHANT_ContractInput *contract_input + = &cchoice->inputs[off]; + contract_input->type = order_input->type; + switch (order_input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == order_input->details.token.count) + continue; + contract_input->details.token.count + = order_input->details.token.count; + contract_input->details.token.token_family_slug + = order_input->details.token.token_family_slug; if (GNUNET_OK != - TALER_MERCHANT_parse_choice_input ((json_t *) jinput, - &input, - idx, - true)) + add_input_token_family (oc, + contract_input->details.token.token_family_slug)) { GNUNET_break_op (0); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "input"); + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + contract_input->details.token.token_family_slug); return; } + off++; + continue; + } /* switch input type */ + GNUNET_assert (0); + } /* for all inputs */ + GNUNET_array_grow (cchoice->inputs, + cchoice->inputs_len, + off); + + /* convert outputs */ + GNUNET_array_grow (cchoice->outputs, + cchoice->outputs_len, + ochoice->outputs_len); + off = 0; + for (unsigned int j = 0; j < ochoice->outputs_len; j++) + { + const struct TALER_MERCHANT_OrderOutput *order_output + = &ochoice->outputs[j]; + struct TALER_MERCHANT_ContractOutput *contract_output + = &cchoice->outputs[off]; - switch (input.type) + switch (order_output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + if (! order_output->details.donation_receipt.no_amount) { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == input.details.token.count) - continue; - - if (GNUNET_OK != - add_input_token_family (oc, - input.details.token.token_family_slug)) - - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, - input.details.token.token_family_slug); - return; - } - - GNUNET_array_append (choice->inputs, - choice->inputs_len, - input); - continue; + contract_output->details.donation_receipt.amount + = ochoice->amount; } - GNUNET_assert (0); - } - } - - if (NULL != joutputs) - { - const json_t *joutput; - size_t idx; - json_array_foreach ((json_t *) joutputs, idx, joutput) - { - struct TALER_MERCHANT_ContractOutput output = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_output ((json_t *) joutput, - &output, - idx, - true)) + else { - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "output"); + contract_output->details.donation_receipt.amount + = order_output->details.donation_receipt.amount; + } + if (! add_donau_output (oc, + contract_output)) + { + GNUNET_break (0); return; } + off++; + continue; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == order_output->details.token.count) + continue; + + contract_output->details.token.token_family_slug + = order_output->details.token.token_family_slug; + contract_output->details.token.count + = order_output->details.token.count; + if (0 == order_output->details.token.valid_at.abs_time.abs_value_us) + contract_output->details.token.valid_at + = GNUNET_TIME_timestamp_get (); + else + contract_output->details.token.valid_at + = order_output->details.token.valid_at; + if (GNUNET_OK != + add_output_token_family ( + oc, + contract_output->details.token.token_family_slug, + contract_output->details.token.valid_at, + &contract_output->details.token.key_index)) - switch (output.type) { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_assert (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - output.details.donation_receipt.amount = choice->amount; - if (! add_donau_output (oc, - &output)) - { - GNUNET_break (0); - return; - } - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - continue; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == output.details.token.count) - continue; - - if (0 == output.details.token.valid_at.abs_time.abs_value_us) - output.details.token.valid_at - = GNUNET_TIME_timestamp_get (); - if (GNUNET_OK != - add_output_token_family (oc, - output.details.token.token_family_slug, - output.details.token.valid_at, - &output.details.token.key_index)) - - { - /* note: reply_with_error() was already called */ - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Could not handle output token family `%s'\n", - output.details.token.token_family_slug); - return; - } - - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - continue; + /* note: reply_with_error() was already called */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Could not handle output token family `%s'\n", + contract_output->details.token.token_family_slug); + return; } - GNUNET_assert (0); - } - } - } + off++; + continue; + } /* end switch */ + GNUNET_assert (0); + } /* for outputs */ + GNUNET_array_grow (cchoice->outputs, + cchoice->outputs_len, + off); + } /* for all choices */ oc->phase++; } @@ -3936,249 +3678,40 @@ phase_parse_order (struct OrderContext *oc) { const struct TALER_MERCHANTDB_InstanceSettings *settings = &oc->hc->instance->settings; - const char *merchant_base_url = NULL; - uint64_t version = 0; - const json_t *jmerchant = NULL; - const json_t *products = NULL; - const char *order_id = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("version", - &version), - NULL), - GNUNET_JSON_spec_string ("summary", - &oc->parse_order.summary), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("products", - &products), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("summary_i18n", - &oc->parse_order.summary_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_slug ("order_id", - &order_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_message", - &oc->parse_order.fulfillment_message), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("fulfillment_message_i18n", - &oc->parse_order.fulfillment_message_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("fulfillment_url", - &oc->parse_order.fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("public_reorder_url", - &oc->parse_order.public_reorder_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("merchant_base_url", - &merchant_base_url), - NULL), - /* For sanity check, this field must NOT be present */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("merchant", - &jmerchant), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("timestamp", - &oc->parse_order.timestamp), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("refund_deadline", - &oc->parse_order.refund_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("pay_deadline", - &oc->parse_order.pay_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &oc->parse_order.wire_deadline), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("delivery_location", - &oc->parse_order.delivery_location), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &oc->parse_order.delivery_date), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &oc->parse_order.minimum_age), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("auto_refund", - &oc->parse_order.auto_refund), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_const ("extra", - &oc->parse_order.extra), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("order_default_money_pot", - &oc->parse_order.order_default_money_pot), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue ret; bool computed_refund_deadline = false; - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - oc->parse_order.wire_deadline = GNUNET_TIME_UNIT_FOREVER_TS; - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - spec); - if (GNUNET_OK != ret) + oc->parse_order.order + = TALER_MERCHANT_order_parse ( + oc->parse_request.order); + if (NULL == oc->parse_order.order) { GNUNET_break_op (0); - finalize_order2 (oc, - ret); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "order"); return; } - if ( (NULL != products) && - (0 != (oc->parse_order.products_len = json_array_size (products))) ) - { - size_t i; - json_t *p; - oc->parse_order.products - = GNUNET_new_array (oc->parse_order.products_len, - struct TALER_MERCHANT_ProductSold); - json_array_foreach (products, i, p) - { - if (GNUNET_OK != - TALER_MERCHANT_parse_product_sold (p, - &oc->parse_order.products[i])) - { - GNUNET_break_op (0); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "order.products"); - return; - } - } - } - switch (version) + switch (oc->parse_order.order->base->version) { - case 0: - { - bool no_fee; - const json_t *choices = NULL; - struct GNUNET_JSON_Specification specv0[] = { - TALER_JSON_spec_amount_any ( - "amount", - &oc->parse_order.details.v0.brutto), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ( - "tip", - &oc->parse_order.details.v0.tip), - &oc->parse_order.details.v0.no_tip), - 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 ( (! oc->parse_order.details.v0.no_tip) && - (GNUNET_OK != - TALER_amount_cmp_currency (&oc->parse_order.details.v0.brutto, - &oc->parse_order.details.v0.tip)) ) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_CURRENCY_MISMATCH, - "tip and amount"); - return; - } - if (! TMH_test_exchange_configured_for_currency ( - oc->parse_order.details.v0.brutto.currency)) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_CONFLICT, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, - oc->parse_order.details.v0.brutto.currency); - return; - } - if (NULL != choices) - { - GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_UNEXPECTED_REQUEST_ERROR, - "choices array must be null for v0 contracts"); - return; - } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_0; - break; - } - case 1: + case TALER_MERCHANT_CONTRACT_VERSION_0: + if (! TMH_test_exchange_configured_for_currency ( + oc->parse_order.order->details.v0.brutto.currency)) { - struct GNUNET_JSON_Specification specv1[] = { - GNUNET_JSON_spec_array_const ( - "choices", - &oc->parse_order.details.v1.choices), - GNUNET_JSON_spec_end () - }; - - ret = TALER_MHD_parse_json_data (oc->connection, - oc->parse_request.order, - specv1); - if (GNUNET_OK != ret) - { - GNUNET_break_op (0); - finalize_order2 (oc, - ret); - return; - } - oc->parse_order.version = TALER_MERCHANT_CONTRACT_VERSION_1; - break; + GNUNET_break_op (0); + reply_with_error ( + oc, + MHD_HTTP_CONFLICT, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_NO_EXCHANGE_FOR_CURRENCY, + oc->parse_order.order->details.v0.brutto.currency); + return; } + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + break; default: GNUNET_break_op (0); - GNUNET_JSON_parse_free (spec); reply_with_error (oc, MHD_HTTP_BAD_REQUEST, TALER_EC_GENERIC_VERSION_MALFORMED, @@ -4187,11 +3720,7 @@ phase_parse_order (struct OrderContext *oc) } /* Add order_id if it doesn't exist. */ - if (NULL != order_id) - { - oc->parse_order.order_id = GNUNET_strdup (order_id); - } - else + if (NULL == oc->parse_order.order->order_id) { char buf[256]; time_t timer; @@ -4204,7 +3733,6 @@ phase_parse_order (struct OrderContext *oc) tm_info = localtime (&timer); if (NULL == tm_info) { - GNUNET_JSON_parse_free (spec); reply_with_error ( oc, MHD_HTTP_INTERNAL_SERVER_ERROR, @@ -4231,17 +3759,15 @@ phase_parse_order (struct OrderContext *oc) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Assigning order ID `%s' server-side\n", buf); - - oc->parse_order.order_id = GNUNET_strdup (buf); - GNUNET_assert (NULL != oc->parse_order.order_id); + oc->parse_order.order->order_id = GNUNET_strdup (buf); } /* Patch fulfillment URL with order_id (implements #6467). */ - if (NULL != oc->parse_order.fulfillment_url) + if (NULL != oc->parse_order.order->base->fulfillment_url) { const char *pos; - pos = strstr (oc->parse_order.fulfillment_url, + pos = strstr (oc->parse_order.order->base->fulfillment_url, "${ORDER_ID}"); if (NULL != pos) { @@ -4260,32 +3786,36 @@ phase_parse_order (struct OrderContext *oc) return; } - GNUNET_asprintf (&nurl, - "%.*s%s%s", - /* first output URL until ${ORDER_ID} */ - (int) (pos - oc->parse_order.fulfillment_url), - oc->parse_order.fulfillment_url, - /* replace ${ORDER_ID} with the right order_id */ - oc->parse_order.order_id, - /* append rest of original URL */ - pos + strlen ("${ORDER_ID}")); - - oc->parse_order.fulfillment_url = GNUNET_strdup (nurl); - + GNUNET_asprintf ( + &nurl, + "%.*s%s%s", + /* first output URL until ${ORDER_ID} */ + (int) (pos - oc->parse_order.order->base->fulfillment_url), + oc->parse_order.order->base->fulfillment_url, + /* replace ${ORDER_ID} with the right order_id */ + oc->parse_order.order->order_id, + /* append rest of original URL */ + pos + strlen ("${ORDER_ID}")); + oc->parse_order.order->base->fulfillment_url = GNUNET_strdup (nurl); GNUNET_free (nurl); } } - if ( (GNUNET_TIME_absolute_is_zero (oc->parse_order.pay_deadline.abs_time)) || - (GNUNET_TIME_absolute_is_never (oc->parse_order.pay_deadline.abs_time)) ) + if ( (GNUNET_TIME_absolute_is_zero ( + oc->parse_order.order->pay_deadline.abs_time)) || + (GNUNET_TIME_absolute_is_never ( + oc->parse_order.order->pay_deadline.abs_time)) ) { - oc->parse_order.pay_deadline = GNUNET_TIME_relative_to_timestamp ( - settings->default_pay_delay); + oc->parse_order.order->pay_deadline + = GNUNET_TIME_relative_to_timestamp ( + settings->default_pay_delay); GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Pay deadline was zero (or never), setting to %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + GNUNET_TIME_timestamp2s ( + oc->parse_order.order->pay_deadline)); } - else if (GNUNET_TIME_absolute_is_past (oc->parse_order.pay_deadline.abs_time)) + else if (GNUNET_TIME_absolute_is_past ( + oc->parse_order.order->pay_deadline.abs_time)) { GNUNET_break_op (0); reply_with_error ( @@ -4297,7 +3827,8 @@ phase_parse_order (struct OrderContext *oc) } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Pay deadline is %s\n", - GNUNET_TIME_timestamp2s (oc->parse_order.pay_deadline)); + GNUNET_TIME_timestamp2s ( + oc->parse_order.order->pay_deadline)); /* Check soundness of refund deadline, and that a timestamp * is actually present. */ @@ -4305,35 +3836,38 @@ phase_parse_order (struct OrderContext *oc) struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get (); /* Add timestamp if it doesn't exist (or is zero) */ - if (GNUNET_TIME_absolute_is_zero (oc->parse_order.timestamp.abs_time)) + if (GNUNET_TIME_absolute_is_zero ( + oc->parse_order.order->timestamp.abs_time)) { - oc->parse_order.timestamp = now; + oc->parse_order.order->timestamp = now; } /* If no refund_deadline given, set one based on refund_delay. */ if (GNUNET_TIME_absolute_is_never ( - oc->parse_order.refund_deadline.abs_time)) + oc->parse_order.order->refund_deadline.abs_time)) { - if (GNUNET_TIME_relative_is_zero (oc->parse_request.refund_delay)) + if (GNUNET_TIME_relative_is_zero ( + oc->parse_request.refund_delay)) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Refund delay is zero, no refunds are possible for this order\n"); - oc->parse_order.refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; + oc->parse_order.order->refund_deadline = GNUNET_TIME_UNIT_ZERO_TS; } else { computed_refund_deadline = true; - oc->parse_order.refund_deadline + oc->parse_order.order->refund_deadline = GNUNET_TIME_absolute_to_timestamp ( - GNUNET_TIME_absolute_add (oc->parse_order.pay_deadline.abs_time, - oc->parse_request.refund_delay)); + GNUNET_TIME_absolute_add ( + oc->parse_order.order->pay_deadline.abs_time, + oc->parse_request.refund_delay)); } } if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.delivery_date.abs_time)) && + oc->parse_order.order->base->delivery_date.abs_time)) && (GNUNET_TIME_absolute_is_past ( - oc->parse_order.delivery_date.abs_time)) ) + oc->parse_order.order->base->delivery_date.abs_time)) ) { GNUNET_break_op (0); reply_with_error ( @@ -4346,9 +3880,9 @@ phase_parse_order (struct OrderContext *oc) } if ( (! GNUNET_TIME_absolute_is_zero ( - oc->parse_order.refund_deadline.abs_time)) && + oc->parse_order.order->refund_deadline.abs_time)) && (GNUNET_TIME_absolute_is_past ( - oc->parse_order.refund_deadline.abs_time)) ) + oc->parse_order.order->refund_deadline.abs_time)) ) { GNUNET_break_op (0); reply_with_error ( @@ -4359,14 +3893,15 @@ phase_parse_order (struct OrderContext *oc) return; } - if (GNUNET_TIME_absolute_is_never (oc->parse_order.wire_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_never ( + oc->parse_order.order->wire_transfer_deadline.abs_time)) { struct GNUNET_TIME_Absolute start; start = GNUNET_TIME_absolute_max ( - oc->parse_order.refund_deadline.abs_time, - oc->parse_order.pay_deadline.abs_time); - oc->parse_order.wire_deadline + oc->parse_order.order->refund_deadline.abs_time, + oc->parse_order.order->pay_deadline.abs_time); + oc->parse_order.order->wire_transfer_deadline = GNUNET_TIME_absolute_to_timestamp ( GNUNET_TIME_round_up ( GNUNET_TIME_absolute_add ( @@ -4374,7 +3909,7 @@ phase_parse_order (struct OrderContext *oc) settings->default_wire_transfer_delay), settings->default_wire_transfer_rounding_interval)); if (GNUNET_TIME_absolute_is_never ( - oc->parse_order.wire_deadline.abs_time)) + oc->parse_order.order->wire_transfer_deadline.abs_time)) { GNUNET_break_op (0); reply_with_error ( @@ -4390,13 +3925,15 @@ phase_parse_order (struct OrderContext *oc) /* if we computed the refund_deadline from default settings and did have a configured wire_deadline, make sure that the refund_deadline is at or below the wire_deadline. */ - oc->parse_order.refund_deadline - = GNUNET_TIME_timestamp_min (oc->parse_order.refund_deadline, - oc->parse_order.wire_deadline); + oc->parse_order.order->refund_deadline + = GNUNET_TIME_timestamp_min ( + oc->parse_order.order->refund_deadline, + oc->parse_order.order->wire_transfer_deadline); } - if (GNUNET_TIME_timestamp_cmp (oc->parse_order.wire_deadline, - <, - oc->parse_order.refund_deadline)) + if (GNUNET_TIME_timestamp_cmp ( + oc->parse_order.order->wire_transfer_deadline, + <, + oc->parse_order.order->refund_deadline)) { GNUNET_break_op (0); reply_with_error ( @@ -4407,23 +3944,6 @@ phase_parse_order (struct OrderContext *oc) return; } - if (NULL != merchant_base_url) - { - if (('\0' == *merchant_base_url) || - ('/' != merchant_base_url[strlen (merchant_base_url) - 1])) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "merchant_base_url is not valid"); - return; - } - oc->parse_order.merchant_base_url - = GNUNET_strdup (merchant_base_url); - } - else { char *url; @@ -4442,20 +3962,9 @@ phase_parse_order (struct OrderContext *oc) oc->parse_order.merchant_base_url = url; } - /* Merchant information must not already be present */ - if (NULL != jmerchant) - { - GNUNET_break_op (0); - reply_with_error ( - oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_PROPOSAL_PARSE_ERROR, - "'merchant' field already set, but must be provided by backend"); - return; - } - - if ( (NULL != oc->parse_order.delivery_location) && - (! TMH_location_object_valid (oc->parse_order.delivery_location)) ) + // FIXME: move to util during parsing! + if ( (NULL != oc->parse_order.order->base->delivery_location) && + (! TMH_location_object_valid (oc->parse_order.order->base->delivery_location)) ) { GNUNET_break_op (0); reply_with_error (oc, diff --git a/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c b/src/backend/taler-merchant-httpd_post-templates-TEMPLATE_ID.c @@ -1390,7 +1390,7 @@ handle_phase_compute_price (struct UseContext *uc) for (size_t i = 0; i < tcp->choices_len; i++) { /* Make deep copy, we're going to MODIFY it! */ - struct TALER_MERCHANT_ContractChoice choice + struct TALER_MERCHANT_OrderChoice choice = tcp->choices[i]; choice.no_tip = uc->parse_request.no_tip; @@ -1417,8 +1417,7 @@ handle_phase_compute_price (struct UseContext *uc) GNUNET_assert (0 == json_array_append_new ( choices, - TALER_MERCHANT_json_from_contract_choice (&choice, - true))); + TALER_MERCHANT_json_from_order_choice (&choice))); } if (0 == json_array_size (choices)) { diff --git a/src/include/taler/taler_merchant_util.h b/src/include/taler/taler_merchant_util.h @@ -103,6 +103,7 @@ TALER_MERCHANT_vk_process_quantity_inputs ( uint32_t *frac_out, const char **error_param); + /** * Format a fixed-decimal pair into canonical string representation. * Recognises INT64_MAX / INT32_MAX as the "-1" sentinel used for @@ -228,748 +229,962 @@ enum TALER_MERCHANT_MFA_CriticalOperation */ TALER_MERCHANT_MFA_CO_AUTH_TOKEN_CREATION - }; + /** - * Template type discriminator. + * Possible versions of the contract terms. */ -enum TALER_MERCHANT_TemplateType +enum TALER_MERCHANT_ContractVersion { - TALER_MERCHANT_TEMPLATE_TYPE_INVALID = 0, - TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER, - TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART, - TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA + + /** + * Version 0 + */ + TALER_MERCHANT_CONTRACT_VERSION_0 = 0, + + /** + * Version 1 + */ + TALER_MERCHANT_CONTRACT_VERSION_1 = 1 }; /** - * Determine template type from string. - * - * @param template_type string value (NULL means fixed-order) - * @return template type (defaults to fixed order) + * Possible token kinds. */ -enum TALER_MERCHANT_TemplateType -TALER_MERCHANT_template_type_from_string (const char *template_type); +enum TALER_MERCHANT_ContractTokenKind +{ + /** + * Token kind invalid + */ + TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID = 0, + + /** + * Subscription token kind + */ + TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION = 1, + + /** + * Discount token kind + */ + TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT = 2, +}; -/** - * Convert template type to its string representation. - * - * @param template_type template type to convert - * @return string name or NULL for invalid types - */ -const char * -TALER_MERCHANT_template_type_to_string ( - enum TALER_MERCHANT_TemplateType template_type); /** - * Determine template type from a template contract. + * Get enum value from contract token type string. * - * @param template_contract contract JSON - * @return template type (defaults to fixed order) + * @param str contract token type string + * @return enum value of token type */ -enum TALER_MERCHANT_TemplateType -TALER_MERCHANT_template_type_from_contract (const json_t *template_contract); +enum TALER_MERCHANT_ContractTokenKind +TALER_MERCHANT_contract_token_kind_from_string (const char *str); + /** - * Template contract fields for inventory templates. + * Possible input types for the contract terms. */ -struct TALER_MERCHANT_TemplateContractInventory +enum TALER_MERCHANT_ContractInputType { - /** - * Selected categories from the template contract. - */ - const json_t *selected_categories; /** - * Selected products from the template contract. + * Input type invalid */ - const json_t *selected_products; + TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID = 0, +#if FUTURE /** - * Whether all products are selectable. + * Input type coin */ - bool selected_all; - + TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN = 1, +#endif /** - * Template requires exactly one selection. + * Input type token */ - bool choose_one; - + TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN = 2 }; /** - * Template contract fields for paivana templates. + * Contract input (part of the v1 contract terms). */ -struct TALER_MERCHANT_TemplateContractPaivana +struct TALER_MERCHANT_ContractInput { /** - * Paivana website regular expression. - * NULL to allow any site. - */ - const char *website_regex; + * Type of the input. + */ + enum TALER_MERCHANT_ContractInputType type; - /** - * Array of possible specific contracts the wallet/customer may choose - * from by selecting the respective index when signing the deposit - * confirmation. - */ - struct TALER_MERCHANT_ContractChoice *choices; + union + { +#if FUTURE + /** + * Coin-based input (ration). (Future work, only here for reference) + */ + struct + { + /** + * Price to be paid. + */ + struct TALER_Amount price; - /** - * Length of the @e choices array. - */ - unsigned int choices_len; + /** + * Base URL of the ration authority. + */ + const char *ration_authority_url; + } coin; +#endif + + /** + * Token-based input. + */ + struct + { + /** + * Slug of the token family to be used. + */ + const char *token_family_slug; + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + } token; + } details; }; + /** - * Parsed template contract. + * Order input (part of the v1 orders). */ -struct TALER_MERCHANT_TemplateContract +struct TALER_MERCHANT_OrderInput { /** - * Template type. - */ - enum TALER_MERCHANT_TemplateType type; + * Type of the input. + */ + enum TALER_MERCHANT_ContractInputType type; - /** - * Summary from the template contract. - */ - const char *summary; + union + { +#if FUTURE + /** + * Coin-based input (ration). (Future work, only here for reference) + */ + struct + { + /** + * Price to be paid. + */ + struct TALER_Amount price; - /** - * Currency from the template contract. - */ - const char *currency; + /** + * Base URL of the ration authority. + */ + const char *ration_authority_url; + } coin; +#endif - /** - * Amount from the template contract. - */ - struct TALER_Amount amount; + /** + * Token-based input. + */ + struct + { + /** + * Slug of the token family to be used. + */ + const char *token_family_slug; + + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + } token; + } details; +}; + + +/** + * Possible output types for the contract terms. + */ +enum TALER_MERCHANT_ContractOutputType +{ /** - * True if @e amount was not provided. + * Invalid output type */ - bool no_amount; + TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID = 0, /** - * Template allows tips. + * Output type token */ - bool request_tip; + TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN = 1, /** - * Minimum age required by the template. + * Output type donation-receipt */ - uint32_t minimum_age; - + TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT = 2, +#if FUTURE /** - * How long does the customer have to pay for the order. - * 0 if not specified (use instance default). + * Output type coin */ - struct GNUNET_TIME_Relative pay_duration; + TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN = 3 +#endif +}; + +/** + * Contract output (part of the v1 contract terms). + */ +struct TALER_MERCHANT_ContractOutput +{ /** - * How long does the user have at most to access/pickup the (digital) - * goods or service they bought. Optional, defaults to FOREVER. + * Type of the output. */ - struct GNUNET_TIME_Relative max_pickup_duration; + enum TALER_MERCHANT_ContractOutputType type; union { - - /** - * Parsed fields for inventory templates. - */ - struct TALER_MERCHANT_TemplateContractInventory inventory; - +#if FUTURE /** - * Parsed fields for paivana templates. + * Coin-based output. */ - struct TALER_MERCHANT_TemplateContractPaivana paivana; - - } details; - -}; - -/** - * Parse template contract JSON into @a out. - * - * @param template_contract JSON object containing the template contract - * @param[out] out parsed template contract - * @param[out] error_name pointer to the name of the failed field, or NULL - * @return #GNUNET_SYSERR if @a template_contract is malformed; #GNUNET_OK otherwise - */ -enum GNUNET_GenericReturnValue -TALER_MERCHANT_template_contract_parse ( - const json_t *template_contract, - struct TALER_MERCHANT_TemplateContract *out, - const char **error_name); - - -/** - * Release memory from template contract @a tc. - * Does not free @a tc itself. - * - * @param[in] tc contract to free - */ -void -TALER_MERCHANT_template_contract_free ( - struct TALER_MERCHANT_TemplateContract *tc); + struct + { + /** + * Coins that will be yielded. This excludes any applicable withdraw fees. + */ + struct TALER_Amount brutto_yield; + /** + * Base URL of the exchange that will issue the coins. + * + * NOTE: Once implemented, check if we need to allocate this here or if + * we again reference the JSON as we do in other places. + */ + char *exchange_url; -/** - * Check if @a template_contract is valid. - * - * @param template_contract template contract to validate - * @return true if @a template_contract is valid - */ -bool -TALER_MERCHANT_template_contract_valid (const json_t *template_contract); + } coin; +#endif + /** + * DONAU-receipt output. + */ + struct + { + /** + * Amount of the donation. + */ + struct TALER_Amount amount; + /** + * Base URLs of the donation authorities that will issue the tax receipt. + */ + char **donau_urls; -/** - * Convert critical operation enumeration value to string - * suitable for human readers. - * - * @param co input to convert - * @return operation value as string - */ -const char * -TALER_MERCHANT_MFA_co2s ( - enum TALER_MERCHANT_MFA_CriticalOperation co); + /** + * Length of the @e donau_urls array. + */ + unsigned int donau_urls_len; + } donation_receipt; -/** - * Convert critical operation enumeration value to string. - * - * @param co input to convert - * @return operation value as string - */ -const char * -TALER_MERCHANT_MFA_co_to_string ( - enum TALER_MERCHANT_MFA_CriticalOperation co); + /** + * Token-based output. + */ + struct + { + /** + * Slug of the token family to be issued. + * Note: this is a pointer into the JSON of the + * respective contract/request and not owned here. + */ + const char *token_family_slug; + /** + * Index of the public key in the @a token_family_slug's token family + * ``keys`` array that this output token will have. + */ + unsigned int key_index; -/** - * Convert string to critical operation enumeration value. - * - * @param str input to convert - * @return #TALER_MERCHANT_MFA_CO_NONE on failure - */ -enum TALER_MERCHANT_MFA_CriticalOperation -TALER_MERCHANT_MFA_co_from_string (const char *str); + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + /** + * Determines when the output token should be valid. + * Optional, set to zero for not specified (then we + * use the current time). + */ + struct GNUNET_TIME_Timestamp valid_at; -/** - * Convert MFA channel enumeration value to string. - * - * @param ch input to convert - * @return operation value as string - */ -const char * -TALER_MERCHANT_MFA_channel_to_string ( - enum TALER_MERCHANT_MFA_Channel ch); + } token; + } details; -/** - * Convert string to MFA channel enumeration value. - * - * @param str input to convert - * @return #TALER_MERCHANT_MFA_CHANNEL_NONE on failure - */ -enum TALER_MERCHANT_MFA_Channel -TALER_MERCHANT_MFA_channel_from_string (const char *str); +}; /** - * @brief Salted hash of a body for a request that required MFA. + * Order output (part of the v1 orders). */ -struct TALER_MERCHANT_MFA_BodyHash +struct TALER_MERCHANT_OrderOutput { /** - * Hash of the body and salt. + * Type of the output. */ - struct GNUNET_ShortHashCode hash; + enum TALER_MERCHANT_ContractOutputType type; + + union + { +#if FUTURE + /** + * Coin-based output. + */ + struct + { + /** + * Coins that will be yielded. This excludes any applicable withdraw fees. + */ + struct TALER_Amount brutto_yield; + + /** + * Base URL of the exchange that will issue the coins. + * + * NOTE: Once implemented, check if we need to allocate this here or if + * we again reference the JSON as we do in other places. + */ + char *exchange_url; + + } coin; +#endif + /** + * DONAU-receipt output. + */ + struct + { + /** + * Amount of the donation. (optional) + */ + struct TALER_Amount amount; + + /** + * True if @e amount is NOT set. + */ + bool no_amount; + + } donation_receipt; + + /** + * Token-based output. + */ + struct + { + /** + * Slug of the token family to be issued. + * Note: this is a pointer into the JSON of the + * respective contract/request and not owned here. + */ + const char *token_family_slug; + + /** + * Number of tokens of this type required. Defaults to one if the + * field is not provided. + */ + unsigned int count; + + /** + * Determines when the output token should be valid. + * Optional, set to zero for not specified (then we + * use the current time). + */ + struct GNUNET_TIME_Timestamp valid_at; + + } token; + + } details; + }; /** - * @brief Salt used when computing a `struct TALER_MERCHANT_MFA_BodyHash` + * Contract choice (part of the v1 contract terms). */ -struct TALER_MERCHANT_MFA_BodySalt +struct TALER_MERCHANT_ContractChoice { + /** - * Salt. + * Amount to be paid for this choice. */ - uint64_t salt[128 / 64]; -}; + struct TALER_Amount amount; + /** + * Tip included by the customer (part of the total amount). + */ + struct TALER_Amount tip; -/** - * @brief Token used to authorize report generation. - */ -struct TALER_MERCHANT_ReportToken -{ /** - * Salt. + * True if @e tip was not provided. */ - uint64_t salt[256 / 64]; -}; + bool no_tip; + /** + * Human readable description of the semantics of the choice within the + * contract to be shown to the user at payment. + */ + char *description; -/** - * Hash the given request @a body with the given @a salt to - * produce @a h_body for MFA checks. - * - * @param body HTTP request body, NULL if body was empty - * @param salt salt to use - * @param h_body resulting hash - */ -void -TALER_MERCHANT_mfa_body_hash ( - const json_t *body, - const struct TALER_MERCHANT_MFA_BodySalt *salt, - struct TALER_MERCHANT_MFA_BodyHash *h_body); + /** + * Map from IETF BCP 47 language tags to localized description. + */ + json_t *description_i18n; + /** + * Maximum fee the merchant is willing to pay for this choice. + * Set to an invalid amount to use instance defaults (zero or STEFAN). + */ + struct TALER_Amount max_fee; -/** - * Possible versions of the contract terms. - */ -enum TALER_MERCHANT_ContractVersion -{ + /** + * List of inputs the wallet must provision (all of them) to satisfy the + * conditions for the contract. + */ + struct TALER_MERCHANT_ContractInput *inputs; /** - * Version 0 + * Length of the @e inputs array. */ - TALER_MERCHANT_CONTRACT_VERSION_0 = 0, + unsigned int inputs_len; /** - * Version 1 + * List of outputs the merchant promises to yield (all of them) once + * the contract is paid. */ - TALER_MERCHANT_CONTRACT_VERSION_1 = 1 + struct TALER_MERCHANT_ContractOutput *outputs; + + /** + * Length of the @e outputs array. + */ + unsigned int outputs_len; }; + /** - * Possible token kinds. + * Order choice (part of the v1 order). */ -enum TALER_MERCHANT_ContractTokenKind +struct TALER_MERCHANT_OrderChoice { + /** - * Token kind invalid + * Amount to be paid for this choice. */ - TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID = 0, + struct TALER_Amount amount; /** - * Subscription token kind + * Tip included by the customer (part of the total amount). */ - TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION = 1, + struct TALER_Amount tip; /** - * Discount token kind + * True if @e tip was not provided. */ - TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT = 2, -}; + bool no_tip; -/** - * Possible input types for the contract terms. - */ -enum TALER_MERCHANT_ContractInputType -{ + /** + * Human readable description of the semantics of the choice within the + * contract to be shown to the user at payment. + */ + char *description; + + /** + * Map from IETF BCP 47 language tags to localized description. + */ + json_t *description_i18n; + + /** + * Maximum fee the merchant is willing to pay for this choice. + * Set to an invalid amount to use instance defaults (zero or STEFAN). + */ + struct TALER_Amount max_fee; + + /** + * True if @e max_fee was not provided. + */ + bool no_max_fee; + + /** + * List of inputs the wallet must provision (all of them) to satisfy the + * conditions for the order. + */ + struct TALER_MERCHANT_OrderInput *inputs; /** - * Input type invalid + * Length of the @e inputs array. */ - TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID = 0, + unsigned int inputs_len; -#if FUTURE /** - * Input type coin + * List of outputs the merchant promises to yield (all of them) once + * the order is paid. */ - TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN = 1, -#endif + struct TALER_MERCHANT_OrderOutput *outputs; + /** - * Input type token + * Length of the @e outputs array. */ - TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN = 2 + unsigned int outputs_len; }; + /** - * Contract input (part of the v1 contract terms). + * Public key and corresponding metadata for a token family. */ -struct TALER_MERCHANT_ContractInput +struct TALER_MERCHANT_ContractTokenFamilyKey { /** - * Type of the input. - */ - enum TALER_MERCHANT_ContractInputType type; - - union - { -#if FUTURE - /** - * Coin-based input (ration). (Future work, only here for reference) - */ - struct - { - /** - * Price to be paid. - */ - struct TALER_Amount price; - - /** - * Base URL of the ration authority. - */ - const char *ration_authority_url; - } coin; -#endif + * Public key. + */ + struct TALER_TokenIssuePublicKey pub; - /** - * Token-based input. - */ - struct - { - /** - * Slug of the token family to be used. - */ - const char *token_family_slug; + /** + * Start time of the token family duration. + */ + struct GNUNET_TIME_Timestamp valid_after; - /** - * Number of tokens of this type required. Defaults to one if the - * field is not provided. - */ - unsigned int count; - } token; - } details; + /** + * Tokens signed by this key will be valid until this time. + */ + struct GNUNET_TIME_Timestamp valid_before; }; + /** - * Possible output types for the contract terms. + * Represents a family of tokens issued by merchants that can be used in contracts. */ -enum TALER_MERCHANT_ContractOutputType +struct TALER_MERCHANT_ContractTokenFamily { + /** + * Slug of the token family. + */ + char *slug; /** - * Invalid output type + * Human-readable name of the token family. */ - TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID = 0, + char *name; /** - * Output type token + * Human-readable description of the semantics of the tokens issued by + * this token family. */ - TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN = 1, + char *description; /** - * Output type donation-receipt + * Map from IETF BCP 47 language tags to localized description. */ - TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT = 2, -#if FUTURE + json_t *description_i18n; + /** - * Output type coin + * Relevant public keys of this token family for the given contract. */ - TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN = 3 -#endif + struct TALER_MERCHANT_ContractTokenFamilyKey *keys; -}; + /** + * Length of the @e keys array. + */ + unsigned int keys_len; -/** - * Contract output (part of the v1 contract terms). - */ -struct TALER_MERCHANT_ContractOutput -{ /** - * Type of the output. + * Must a wallet understand this token type to process contracts that + * consume or yield it? */ - enum TALER_MERCHANT_ContractOutputType type; + bool critical; + + /** + * Kind of the token family. + */ + enum TALER_MERCHANT_ContractTokenKind kind; + /** + * Kind-specific information about the token. + */ union { -#if FUTURE - /** - * Coin-based output. - */ - struct - { - /** - * Coins that will be yielded. This excludes any applicable withdraw fees. - */ - struct TALER_Amount brutto_yield; - - /** - * Base URL of the exchange that will issue the coins. - * - * NOTE: Once implemented, check if we need to allocate this here or if - * we again reference the JSON as we do in other places. - */ - char *exchange_url; - - } coin; -#endif /** - * DONAU-receipt output. + * Subscription token. */ struct { /** - * Amount of the donation. (optional) - */ - struct TALER_Amount amount; - - /** - * Base URLs of the donation authorities that will issue the tax receipt. + * Array of domain names where this subscription can be safely used + * (e.g. the issuer warrants that these sites will re-issue tokens of + * this type if the respective contract says so). May contain "*" for + * any domain or subdomain. */ - char **donau_urls; + char **trusted_domains; /** - * Length of the @e donau_urls array. + * Length of the @e trusted_domains array. */ - unsigned int donau_urls_len; - - } donation_receipt; + unsigned int trusted_domains_len; + } subscription; /** - * Token-based output. - */ + * Discount token. + */ struct { /** - * Slug of the token family to be issued. - * Note: this is a pointer into the JSON of the - * respective contract/request and not owned here. - */ - const char *token_family_slug; - - /** - * Index of the public key in the @a token_family_slug's token family - * ``keys`` array that this output token will have. - */ - unsigned int key_index; - - /** - * Number of tokens of this type required. Defaults to one if the - * field is not provided. + * Array of domain names where this discount token is intended to be + * used. May contain "*" for any domain or subdomain. Users should be + * warned about sites proposing to consume discount tokens of this + * type that are not in this list that the merchant is accepting a + * coupon from a competitor and thus may be attaching different + * semantics (like get 20% discount for my competitors 30% discount + * token). */ - unsigned int count; + char **expected_domains; /** - * Determines when the output token should be valid. - * Optional, set to zero for not specified (then we - * use the current time). + * Length of the @e expected_domains array. */ - struct GNUNET_TIME_Timestamp valid_at; - - } token; + unsigned int expected_domains_len; + } discount; } details; - }; /** - * Contract choice (part of the v1 contract terms). + * Specifies the quantity of a product (to be) sold. */ -struct TALER_MERCHANT_ContractChoice +struct TALER_MERCHANT_ProductQuantity { - /** - * Amount to be paid for this choice. + * Integer component of the quantity. */ - struct TALER_Amount amount; + uint64_t integer; /** - * Tip included by the customer (part of the total amount). + * Fractional component of the quantity, in the + * range of 0..TALER_MERCHANT_UNIT_FRAC_BASE-1. */ - struct TALER_Amount tip; + uint32_t fractional; - /** - * True if @e tip was not provided. - */ - bool no_tip; +}; + + +/** + * How to round when computing with amounts? + */ +enum TALER_MERCHANT_RoundMode +{ + TALER_MERCHANT_ROUND_NEAREST, + TALER_MERCHANT_ROUND_UP, + TALER_MERCHANT_ROUND_DOWN +}; + + +/** + * Template type discriminator. + */ +enum TALER_MERCHANT_TemplateType +{ + TALER_MERCHANT_TEMPLATE_TYPE_INVALID = 0, + TALER_MERCHANT_TEMPLATE_TYPE_FIXED_ORDER, + TALER_MERCHANT_TEMPLATE_TYPE_INVENTORY_CART, + TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA +}; + +/** + * Determine template type from string. + * + * @param template_type string value (NULL means fixed-order) + * @return template type (defaults to fixed order) + */ +enum TALER_MERCHANT_TemplateType +TALER_MERCHANT_template_type_from_string (const char *template_type); + +/** + * Convert template type to its string representation. + * + * @param template_type template type to convert + * @return string name or NULL for invalid types + */ +const char * +TALER_MERCHANT_template_type_to_string ( + enum TALER_MERCHANT_TemplateType template_type); - /** - * Human readable description of the semantics of the choice within the - * contract to be shown to the user at payment. - */ - char *description; +/** + * Determine template type from a template contract. + * + * @param template_contract contract JSON + * @return template type (defaults to fixed order) + */ +enum TALER_MERCHANT_TemplateType +TALER_MERCHANT_template_type_from_contract (const json_t *template_contract); - /** - * Map from IETF BCP 47 language tags to localized description. - */ - json_t *description_i18n; +/** + * Template contract fields for inventory templates. + */ +struct TALER_MERCHANT_TemplateContractInventory +{ /** - * Maximum fee the merchant is willing to pay for this choice. - * Set to an invalid amount to use instance defaults (zero or STEFAN). + * Selected categories from the template contract. */ - struct TALER_Amount max_fee; + const json_t *selected_categories; /** - * List of inputs the wallet must provision (all of them) to satisfy the - * conditions for the contract. + * Selected products from the template contract. */ - struct TALER_MERCHANT_ContractInput *inputs; + const json_t *selected_products; /** - * Length of the @e inputs array. + * Whether all products are selectable. */ - unsigned int inputs_len; + bool selected_all; /** - * List of outputs the merchant promises to yield (all of them) once - * the contract is paid. + * Template requires exactly one selection. */ - struct TALER_MERCHANT_ContractOutput *outputs; + bool choose_one; - /** - * Length of the @e outputs array. - */ - unsigned int outputs_len; }; /** - * Public key and corresponding metadata for a token family. + * Template contract fields for paivana templates. */ -struct TALER_MERCHANT_ContractTokenFamilyKey +struct TALER_MERCHANT_TemplateContractPaivana { /** - * Public key. + * Paivana website regular expression. + * NULL to allow any site. */ - struct TALER_TokenIssuePublicKey pub; + const char *website_regex; /** - * Start time of the token family duration. + * Array of possible specific contracts the wallet/customer may choose + * from by selecting the respective index when signing the deposit + * confirmation. */ - struct GNUNET_TIME_Timestamp valid_after; + struct TALER_MERCHANT_OrderChoice *choices; /** - * Tokens signed by this key will be valid until this time. + * Length of the @e choices array. */ - struct GNUNET_TIME_Timestamp valid_before; -}; + unsigned int choices_len; +}; /** - * Represents a family of tokens issued by merchants that can be used in contracts. + * Parsed template contract. */ -struct TALER_MERCHANT_ContractTokenFamily +struct TALER_MERCHANT_TemplateContract { /** - * Slug of the token family. + * Template type. */ - char *slug; + enum TALER_MERCHANT_TemplateType type; /** - * Human-readable name of the token family. + * Summary from the template contract. */ - char *name; + const char *summary; /** - * Human-readable description of the semantics of the tokens issued by - * this token family. + * Currency from the template contract. */ - char *description; + const char *currency; /** - * Map from IETF BCP 47 language tags to localized description. + * Amount from the template contract. */ - json_t *description_i18n; + struct TALER_Amount amount; /** - * Relevant public keys of this token family for the given contract. + * True if @e amount was not provided. */ - struct TALER_MERCHANT_ContractTokenFamilyKey *keys; + bool no_amount; /** - * Length of the @e keys array. + * Template allows tips. */ - unsigned int keys_len; + bool request_tip; /** - * Must a wallet understand this token type to process contracts that - * consume or yield it? + * Minimum age required by the template. */ - bool critical; + uint32_t minimum_age; /** - * Kind of the token family. + * How long does the customer have to pay for the order. + * 0 if not specified (use instance default). */ - enum TALER_MERCHANT_ContractTokenKind kind; + struct GNUNET_TIME_Relative pay_duration; /** - * Kind-specific information about the token. + * How long does the user have at most to access/pickup the (digital) + * goods or service they bought. Optional, defaults to FOREVER. */ + struct GNUNET_TIME_Relative max_pickup_duration; + union { + /** - * Subscription token. + * Parsed fields for inventory templates. */ - struct - { - /** - * Array of domain names where this subscription can be safely used - * (e.g. the issuer warrants that these sites will re-issue tokens of - * this type if the respective contract says so). May contain "*" for - * any domain or subdomain. - */ - char **trusted_domains; - - /** - * Length of the @e trusted_domains array. - */ - unsigned int trusted_domains_len; - } subscription; + struct TALER_MERCHANT_TemplateContractInventory inventory; /** - * Discount token. - */ - struct - { - /** - * Array of domain names where this discount token is intended to be - * used. May contain "*" for any domain or subdomain. Users should be - * warned about sites proposing to consume discount tokens of this - * type that are not in this list that the merchant is accepting a - * coupon from a competitor and thus may be attaching different - * semantics (like get 20% discount for my competitors 30% discount - * token). - */ - char **expected_domains; - - /** - * Length of the @e expected_domains array. - */ - unsigned int expected_domains_len; + * Parsed fields for paivana templates. + */ + struct TALER_MERCHANT_TemplateContractPaivana paivana; - } discount; } details; + +}; + +/** + * Parse template contract JSON into @a out. + * + * @param template_contract JSON object containing the template contract + * @param[out] out parsed template contract + * @param[out] error_name pointer to the name of the failed field, or NULL + * @return #GNUNET_SYSERR if @a template_contract is malformed; #GNUNET_OK otherwise + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_template_contract_parse ( + const json_t *template_contract, + struct TALER_MERCHANT_TemplateContract *out, + const char **error_name); + + +/** + * Release memory from template contract @a tc. + * Does not free @a tc itself. + * + * @param[in] tc contract to free + */ +void +TALER_MERCHANT_template_contract_free ( + struct TALER_MERCHANT_TemplateContract *tc); + + +/** + * Check if @a template_contract is valid. + * + * @param template_contract template contract to validate + * @return true if @a template_contract is valid + */ +bool +TALER_MERCHANT_template_contract_valid (const json_t *template_contract); + + +/** + * Convert critical operation enumeration value to string + * suitable for human readers. + * + * @param co input to convert + * @return operation value as string + */ +const char * +TALER_MERCHANT_MFA_co2s ( + enum TALER_MERCHANT_MFA_CriticalOperation co); + + +/** + * Convert critical operation enumeration value to string. + * + * @param co input to convert + * @return operation value as string + */ +const char * +TALER_MERCHANT_MFA_co_to_string ( + enum TALER_MERCHANT_MFA_CriticalOperation co); + + +/** + * Convert string to critical operation enumeration value. + * + * @param str input to convert + * @return #TALER_MERCHANT_MFA_CO_NONE on failure + */ +enum TALER_MERCHANT_MFA_CriticalOperation +TALER_MERCHANT_MFA_co_from_string (const char *str); + + +/** + * Convert MFA channel enumeration value to string. + * + * @param ch input to convert + * @return operation value as string + */ +const char * +TALER_MERCHANT_MFA_channel_to_string ( + enum TALER_MERCHANT_MFA_Channel ch); + + +/** + * Convert string to MFA channel enumeration value. + * + * @param str input to convert + * @return #TALER_MERCHANT_MFA_CHANNEL_NONE on failure + */ +enum TALER_MERCHANT_MFA_Channel +TALER_MERCHANT_MFA_channel_from_string (const char *str); + + +/** + * @brief Salted hash of a body for a request that required MFA. + */ +struct TALER_MERCHANT_MFA_BodyHash +{ + /** + * Hash of the body and salt. + */ + struct GNUNET_ShortHashCode hash; }; + /** - * Specifies the quantity of a product (to be) sold. + * @brief Salt used when computing a `struct TALER_MERCHANT_MFA_BodyHash` */ -struct TALER_MERCHANT_ProductQuantity +struct TALER_MERCHANT_MFA_BodySalt { /** - * Integer component of the quantity. + * Salt. */ - uint64_t integer; + uint64_t salt[128 / 64]; +}; + +/** + * @brief Token used to authorize report generation. + */ +struct TALER_MERCHANT_ReportToken +{ /** - * Fractional component of the quantity, in the - * range of 0..TALER_MERCHANT_UNIT_FRAC_BASE-1. + * Salt. */ - uint32_t fractional; - + uint64_t salt[256 / 64]; }; /** - * How to round when computing with amounts? + * Hash the given request @a body with the given @a salt to + * produce @a h_body for MFA checks. + * + * @param body HTTP request body, NULL if body was empty + * @param salt salt to use + * @param h_body resulting hash */ -enum TALER_MERCHANT_RoundMode -{ - TALER_MERCHANT_ROUND_NEAREST, - TALER_MERCHANT_ROUND_UP, - TALER_MERCHANT_ROUND_DOWN -}; +void +TALER_MERCHANT_mfa_body_hash ( + const json_t *body, + const struct TALER_MERCHANT_MFA_BodySalt *salt, + struct TALER_MERCHANT_MFA_BodyHash *h_body); /** @@ -997,7 +1212,11 @@ TALER_MERCHANT_amount_multiply_by_quantity ( /** - * Details about a product (to be) sold. + * Details about a product (to be) sold. This structure represents + * both the MinimalInventoryProduct and the ProductSold. Note that + * the two have different mandatory and/or optional fields but are + * both possible in `struct TALER_MERCHANT_Order`. However, only + * ProductSold is allowed in the contracts. */ struct TALER_MERCHANT_ProductSold { @@ -1085,61 +1304,54 @@ struct TALER_MERCHANT_ProductSold /** - * Struct to hold contract terms. + * Merchant information. */ -struct TALER_MERCHANT_Contract +struct TALER_MERCHANT_MetaData { /** - * URL where the same contract could be ordered again (if available). + * Legal name of the instance */ - char *public_reorder_url; + char *name; /** - * Our order ID. + * Merchant's site url */ - char *order_id; + char *website; /** - * Merchant base URL. + * Email contact for customers */ - char *merchant_base_url; + char *email; /** - * Merchant information. + * merchant's logo data uri */ - struct - { - /** - * Legal name of the instance - */ - char *name; + char *logo; - /** - * Merchant's site url - */ - char *website; + /** + * Merchant address + */ + json_t *address; - /** - * Email contact for customers - */ - char *email; + /** + * Jurisdiction of the business + */ + json_t *jurisdiction; - /** - * merchant's logo data uri - */ - char *logo; +}; - /** - * Merchant address - */ - json_t *address; - /** - * Jurisdiction of the business - */ - json_t *jurisdiction; +/** + * Struct to hold terms common to both orders and contracts and + * are basically present (or optional) all the time. + */ +struct TALER_MERCHANT_ContractBaseTerms +{ - } merchant; + /** + * Version of the contract terms. + */ + enum TALER_MERCHANT_ContractVersion version; /** * Summary of the contract. @@ -1147,31 +1359,100 @@ struct TALER_MERCHANT_Contract char *summary; /** - * Internationalized summary. + * Internationalized summary. Optional. */ json_t *summary_i18n; /** + * URL where the same contract could be ordered again (if available). + * Optional. + */ + char *public_reorder_url; + + /** * URL that will show that the contract was successful * after it has been paid for. + * Optional. */ char *fulfillment_url; /** * Message shown to the customer after paying for the contract. * Either fulfillment_url or fulfillment_message must be specified. + * Optional. */ char *fulfillment_message; /** * Map from IETF BCP 47 language tags to localized fulfillment messages. + * Optional. */ json_t *fulfillment_message_i18n; /** - * Length of the @e products array. + * Delivery location. + * Optional. */ - size_t products_len; + json_t *delivery_location; + + /** + * Delivery date. + * Optional. + */ + struct GNUNET_TIME_Timestamp delivery_date; + + /** + * Specifies for how long the wallet should try to get an + * automatic refund for the purchase. + * Optional, zero if not applicable. + */ + struct GNUNET_TIME_Relative auto_refund; + + /** + * Extra data that is only interpreted by the merchant frontend. + */ + json_t *extra; + + /** + * Minimum age the buyer must have (in years). + */ + uint8_t minimum_age; + + /** + * Default money pot to use for this order, applies to the + * amount remaining that was not claimed by money pots of + * products or taxes. Not useful to wallets, only for + * merchant-internal accounting. If zero, the remaining + * account is simply not accounted for in any money pot. + */ + uint64_t default_money_pot; + + /** + * Latest time until which the good or service may be + * picked up by the customer. This is usually for digital + * goods where the customer has a finite window for downloading. + * Optional. FOREVER if not set. + */ + struct GNUNET_TIME_Timestamp max_pickup_time; + +}; + + +/** + * Struct to hold an order. + */ +struct TALER_MERCHANT_Order +{ + + /** + * Shared base terms of contracts and orders. + */ + struct TALER_MERCHANT_ContractBaseTerms *base; + + /** + * Our order ID. Optional. + */ + char *order_id; /** * Array of products that are part of the purchase. @@ -1179,99 +1460,178 @@ struct TALER_MERCHANT_Contract struct TALER_MERCHANT_ProductSold *products; /** + * Length of the @e products array. + */ + size_t products_len; + + /** * Timestamp of the contract. + * Optional. Set to "now" on parsing if not given. */ struct GNUNET_TIME_Timestamp timestamp; /** * Deadline for refunds. + * Optional. Set to FOREVER if not given. */ struct GNUNET_TIME_Timestamp refund_deadline; /** - * Specifies for how long the wallet should try to get an - * automatic refund for the purchase. - */ - struct GNUNET_TIME_Relative auto_refund; - - /** * Payment deadline. + * Optional. Set to zero if not given. */ struct GNUNET_TIME_Timestamp pay_deadline; /** * Wire transfer deadline. + * Optional. Set to FOREVER if not given. */ - struct GNUNET_TIME_Timestamp wire_deadline; + struct GNUNET_TIME_Timestamp wire_transfer_deadline; /** - * Delivery date. + * Details depending on the @e base.version. */ - struct GNUNET_TIME_Timestamp delivery_date; + union + { + + /** + * Details for v0 orders. + */ + struct + { + + /** + * 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; + + /** + * Tip included by the customer (part of the total amount). + */ + struct TALER_Amount tip; + + /** + * True if @e tip was not provided. + */ + bool no_tip; + + /** + * Maximum fee as given by the client request. + */ + struct TALER_Amount max_fee; + + /** + * True if @e max_fee was not provided. + */ + bool no_max_fee; + + } v0; + + /** + * Details for v1 contracts. + */ + struct + { + + /** + * Array of possible specific payment choices the wallet/customer may choose + * from by selecting the respective index when signing the deposit +2 * confirmation. + */ + struct TALER_MERCHANT_OrderChoice *choices; + + /** + * Length of the @e choices array. + */ + unsigned int choices_len; + + } v1; + + } details; + +}; + + +/** + * Struct to hold proto-contracts. + */ +struct TALER_MERCHANT_ProtoContract +{ /** - * Latest time until which the good or service may be - * picked up by the customer. This is usually for digital - * goods where the customer has a finite window for downloading. + * Shared base terms of contracts and orders. */ - struct GNUNET_TIME_Timestamp max_pickup_time; + struct TALER_MERCHANT_ContractBaseTerms *base; /** - * Merchant public key. + * Our order ID. */ - struct TALER_MerchantPublicKeyP merchant_pub; + char *order_id; /** - * The hash of the merchant instance's wire details. + * Timestamp of the contract. */ - struct TALER_MerchantWireHashP h_wire; + struct GNUNET_TIME_Timestamp timestamp; /** - * Wire transfer method identifier for the wire method associated with - * @e h_wire. + * Deadline for refunds. */ - char *wire_method; + struct GNUNET_TIME_Timestamp refund_deadline; /** - * Exchanges that the merchant accepts even if it does not accept any auditors that audit them. - * TODO: appropriate type + * Payment deadline. */ - json_t *exchanges; + struct GNUNET_TIME_Timestamp pay_deadline; /** - * Delivery location. + * Wire transfer deadline. */ - json_t *delivery_location; + struct GNUNET_TIME_Timestamp wire_deadline; /** - * Nonce generated by the wallet and echoed by the merchant - * in this field when the proposal is generated. + * Merchant public key. */ - char *nonce; + struct TALER_MerchantPublicKeyP merchant_pub; /** - * Extra data that is only interpreted by the merchant frontend. + * Merchant base URL. */ - json_t *extra; + char *merchant_base_url; + + /** + * Metadata about the merchant. + */ + struct TALER_MERCHANT_MetaData merchant; + + /** + * Array of products that are part of the purchase. + */ + struct TALER_MERCHANT_ProductSold *products; + + /** + * Length of the @e products array. + */ + size_t products_len; /** - * Minimum age the buyer must have (in years). + * The hash of the merchant instance's wire details. */ - uint8_t minimum_age; + struct TALER_MerchantWireHashP h_wire; /** - * Default money pot to use for this product, applies to the - * amount remaining that was not claimed by money pots of - * products or taxes. Not useful to wallets, only for - * merchant-internal accounting. If zero, the remaining - * account is simply not accounted for in any money pot. + * Wire transfer method identifier for the wire method associated with + * @e h_wire. */ - uint64_t default_money_pot; + char *wire_method; /** - * Specified version of the contract. + * Exchanges that the merchant accepts even if it does not accept any auditors that audit them. + * TODO: appropriate type */ - enum TALER_MERCHANT_ContractVersion version; + json_t *exchanges; /** * Details depending on the @e version. @@ -1304,8 +1664,8 @@ struct TALER_MERCHANT_Contract bool no_tip; /** - * Maximum fee as given by the client request. - */ + * Maximum fee as given by the client request. + */ struct TALER_Amount max_fee; } v0; @@ -1346,15 +1706,23 @@ struct TALER_MERCHANT_Contract /** - * Parse JSON contract terms in @a input. - * - * @param[in] input JSON object containing contract terms - * @param nonce_optional whether `nonce' field is optional - * @return parsed contract terms; NULL if @a input is malformed + * Struct to hold contract terms. */ -struct TALER_MERCHANT_Contract * -TALER_MERCHANT_contract_parse (json_t *input, - bool nonce_optional); +struct TALER_MERCHANT_Contract +{ + + /** + * The proto-contract. + */ + struct TALER_MERCHANT_ProtoContract *pc; + + /** + * Nonce generated by the wallet and echoed by the merchant + * in this field when the proposal is generated. + */ + char *nonce; + +}; /** @@ -1366,8 +1734,72 @@ TALER_MERCHANT_contract_parse (json_t *input, * @return spec for parsing a contract input type */ struct GNUNET_JSON_Specification -TALER_MERCHANT_json_spec_cit (const char *name, - enum TALER_MERCHANT_ContractInputType *cit); +TALER_MERCHANT_json_spec_cit ( + const char *name, + enum TALER_MERCHANT_ContractInputType *cit); + + +/** + * Create JSON specification to parse a merchant contract + * version. + * + * @param name name of the field + * @param[out] version where to write the contract version + * @return JSON specification object + */ +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_contract_version ( + const char *name, + enum TALER_MERCHANT_ContractVersion *version); + + +/** + * Provide specification to parse given JSON object to merchant details in the + * order terms. All fields from @a order are copied. + * + * @param name name of the merchant details field in the JSON + * @param[out] order where the merchant details have to be written + */ +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_merchant_details ( + const char *name, + struct TALER_MERCHANT_MetaData *merchant); + + +/** + * Get JSON representation of merchant details. + * + * @param[in] merchant metadata to serialize + * @return JSON object with merchant details; NULL on error + */ +json_t * +TALER_MERCHANT_metadata_to_json ( + const struct TALER_MERCHANT_MetaData *merchant); + + +/** + * Free representation of merchant details. + * + * @param[in,out] merchant metadata to release, excluding the pointer itself + */ +void +TALER_MERCHANT_metadata_free ( + struct TALER_MERCHANT_MetaData *merchant); + + +/** + * Provide specification to parse given JSON array to order + * choices. All fields from @a choices elements are copied. + * + * @param name name of the choices field in the JSON + * @param[out] choices where the order choices array has to be written + * @param[out] choices_len length of the @a choices array + */ +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_order_choices ( + const char *name, + struct TALER_MERCHANT_OrderChoice **choices, + unsigned int *choices_len); /** @@ -1379,27 +1811,57 @@ TALER_MERCHANT_json_spec_cit (const char *name, * @param[out] choices_len length of the @a choices array */ struct GNUNET_JSON_Specification -TALER_MERCHANT_spec_choices ( +TALER_MERCHANT_spec_contract_choices ( const char *name, struct TALER_MERCHANT_ContractChoice **choices, unsigned int *choices_len); /** + * Provide specification to parse given JSON array to token families in the + * contract terms. All fields from @a families items are copied. + * + * @param name name of the token families field in the JSON + * @param[out] families where the token families array has to be written + * @param[out] families_len length of the @a families array + */ +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_token_families ( + const char *name, + struct TALER_MERCHANT_ContractTokenFamily **families, + unsigned int *families_len); + + +/** * Parse JSON contract terms choice input. * * @param[in] root JSON object containing choice input * @param[out] input parsed choice input, NULL if @a input is malformed * @param index index of choice input in inputs array - * @param order whether @a input is contained in order or contract terms * @return #GNUNET_SYSERR if @a input is malformed; #GNUNET_OK otherwise */ +// FIXME: make static? enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_choice_input ( +TALER_MERCHANT_parse_contract_choice_input ( json_t *root, struct TALER_MERCHANT_ContractInput *input, - size_t index, - bool order); + size_t index); + + +/** + * Parse JSON order choice input. + * + * @param[in] root JSON object containing choice input + * @param[out] input parsed choice input, NULL if @a input is malformed + * @param index index of choice input in inputs array + * @return #GNUNET_SYSERR if @a input is malformed; #GNUNET_OK otherwise + */ +// FIXME: make static? +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_order_choice_input ( + json_t *root, + struct TALER_MERCHANT_OrderInput *input, + size_t index); /** @@ -1411,20 +1873,8 @@ TALER_MERCHANT_parse_choice_input ( * entries are valid Taxes. */ bool -TALER_MERCHANT_taxes_array_valid (const json_t *taxes); - - -/** - * Parse JSON product given in @a p, returning the result in - * @a r. - * - * @param p JSON specifying a ``ProductSold`` to parse - * @param[out] r where to write the result - * @return #GNUNET_OK on success - */ -enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_product_sold (const json_t *p, - struct TALER_MERCHANT_ProductSold *r); +TALER_MERCHANT_taxes_array_valid ( + const json_t *taxes); /** @@ -1436,8 +1886,9 @@ TALER_MERCHANT_parse_product_sold (const json_t *p, * @return spec for parsing a contract output type */ struct GNUNET_JSON_Specification -TALER_MERCHANT_json_spec_cot (const char *name, - enum TALER_MERCHANT_ContractOutputType *cot); +TALER_MERCHANT_json_spec_cot ( + const char *name, + enum TALER_MERCHANT_ContractOutputType *cot); /** @@ -1446,52 +1897,75 @@ TALER_MERCHANT_json_spec_cot (const char *name, * @param[in] root JSON object containing choice output * @param[out] output parsed choice output, NULL if @a output is malformed * @param index index of choice output in outputs array - * @param order whether @a output is contained in order or contract terms * @return #GNUNET_SYSERR if @a output is malformed; #GNUNET_OK otherwise */ +// FIXME: make static? enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_choice_output ( +TALER_MERCHANT_parse_contract_choice_output ( json_t *root, struct TALER_MERCHANT_ContractOutput *output, - size_t index, - bool order); + size_t index); /** - * Serialize @a p to JSON. + * Parse JSON order choice output. * - * @param p product to serialize - * @return JSON object representing the product @a p + * @param[in] root JSON object containing choice output + * @param[out] output parsed choice output, NULL if @a output is malformed + * @param index index of choice output in outputs array + * @return #GNUNET_SYSERR if @a output is malformed; #GNUNET_OK otherwise */ -json_t * -TALER_MERCHANT_product_sold_serialize ( - const struct TALER_MERCHANT_ProductSold *p); +// FIXME: make static? +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_order_choice_output ( + json_t *root, + struct TALER_MERCHANT_OrderOutput *output, + size_t index); /** - * Serialize contract terms into JSON object. + * Get JSON representation of contract choice. * - * @param[in] input contract terms to serialize - * @param nonce_optional whether `nonce' field is optional - * @return JSON representation of @a input; NULL on error + * @param[in] choice contract choice to serialize + * @param order whether @a choice is contained in order or contract terms + * @return JSON representation of @a choice; NULL on error */ json_t * -TALER_MERCHANT_contract_serialize ( - const struct TALER_MERCHANT_Contract *input, - bool nonce_optional); +TALER_MERCHANT_json_from_contract_choice ( + const struct TALER_MERCHANT_ContractChoice *choice); /** - * Get JSON representation of contract choice. + * Free all the fields in the given @a choice, but not @a choice itself, since + * it is normally part of an array. + * + * @param[in] choice contract terms choice to free + */ +void +TALER_MERCHANT_contract_choice_free ( + struct TALER_MERCHANT_ContractChoice *choice); + + +/** + * Get JSON representation of an order choice. * * @param[in] choice contract choice to serialize - * @param order whether @a choice is contained in order or contract terms * @return JSON representation of @a choice; NULL on error */ json_t * -TALER_MERCHANT_json_from_contract_choice ( - const struct TALER_MERCHANT_ContractChoice *choice, - bool order); +TALER_MERCHANT_json_from_order_choice ( + const struct TALER_MERCHANT_OrderChoice *choice); + + +/** + * Free all the fields in the given @a choice, but not @a choice itself, since + * it is normally part of an array. + * + * @param[in] choice contract terms choice to free + */ +void +TALER_MERCHANT_order_choice_free ( + struct TALER_MERCHANT_OrderChoice *choice); /** @@ -1527,14 +2001,41 @@ TALER_MERCHANT_find_token_family_key ( /** - * Free all the fields in the given @a choice, but not @a choice itself, since + * Free all the fields in the given @a family, but not @a family itself, since * it is normally part of an array. * - * @param[in] choice contract terms choice to free + * @param[in] family contract token family to free */ void -TALER_MERCHANT_contract_choice_free ( - struct TALER_MERCHANT_ContractChoice *choice); +TALER_MERCHANT_contract_token_family_free ( + struct TALER_MERCHANT_ContractTokenFamily *family); + + +/** + * Parse JSON product given in @a p, returning the result in + * @a r. + * + * @param p JSON specifying a ``ProductSold`` to parse + * @param[out] r where to write the result + * @param allow_mip true to allow for MinimalInventoryProduct + * @return #GNUNET_OK on success + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_product_sold ( + const json_t *p, + struct TALER_MERCHANT_ProductSold *r, + bool allow_mip); + + +/** + * Serialize @a p to JSON. + * + * @param p product to serialize + * @return JSON object representing the product @a p + */ +json_t * +TALER_MERCHANT_product_sold_serialize ( + const struct TALER_MERCHANT_ProductSold *p); /** @@ -1543,7 +2044,130 @@ TALER_MERCHANT_contract_choice_free ( * @param[in] product data structure to clean up */ void -TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product); +TALER_MERCHANT_product_sold_free ( + struct TALER_MERCHANT_ProductSold *product); + + +/** + * Parse base terms (of orders and contracts) in @a input. + * + * @param[in] input JSON object containing contract terms + * @return NULL if @a input is malformed + */ +struct TALER_MERCHANT_ContractBaseTerms * +TALER_MERCHANT_base_terms_parse ( + json_t *input); + + +/** + * Serialize contract base terms into JSON object. + * + * @param[in] ct base contract terms to serialize + * @return JSON representation of @a ct; NULL on error + */ +json_t * +TALER_MERCHANT_base_terms_serialize ( + const struct TALER_MERCHANT_ContractBaseTerms *ct); + + +/** + * Release memory in @a ct + * + * @param[in] ct common terms to clean up + */ +void +TALER_MERCHANT_base_terms_free ( + struct TALER_MERCHANT_ContractBaseTerms *ct); + + +/** + * Parse JSON order in @a input. + * + * @param[in] input JSON object containing an order + * @return parsed contract terms; NULL if @a input is malformed + */ +struct TALER_MERCHANT_Order * +TALER_MERCHANT_order_parse ( + json_t *input); + + +#if NOT_NEEDED_YET +/** + * Serialize order into JSON object. + * + * @param[in] input contract terms to serialize + * @param nonce_optional whether `nonce' field is optional + * @return JSON representation of @a input; NULL on error + */ +json_t * +TALER_MERCHANT_order_serialize ( + const struct TALER_MERCHANT_Order *input); + +#endif + + +/** + * Free the @a order and all fields in it. + * + * @param[in] order order to free + */ +void +TALER_MERCHANT_order_free ( + struct TALER_MERCHANT_Order *order); + + +/** + * Parse JSON proto contract terms in @a input. + * + * @param[in] input JSON object containing contract terms + * @return NULL on failure + */ +struct TALER_MERCHANT_ProtoContract * +TALER_MERCHANT_proto_contract_parse ( + json_t *input); + + +/** + * Serialize proto contract into JSON object. + * + * @param[in] pc proto contract to serialize + * @return JSON representation of @a pc; NULL on error + */ +json_t * +TALER_MERCHANT_proto_contract_serialize ( + const struct TALER_MERCHANT_ProtoContract *pc); + + +/** + * Free the proto-contract at @a pc + * + * @param[in] pc proto-contract to free + */ +void +TALER_MERCHANT_proto_contract_free ( + struct TALER_MERCHANT_ProtoContract *pc); + + +/** + * Parse JSON contract terms in @a input. + * + * @param[in] input JSON object containing contract terms + * @return parsed contract terms; NULL if @a input is malformed + */ +struct TALER_MERCHANT_Contract * +TALER_MERCHANT_contract_parse ( + json_t *input); + + +/** + * Serialize contract terms into JSON object. + * + * @param[in] input contract terms to serialize + * @return JSON representation of @a input; NULL on error + */ +json_t * +TALER_MERCHANT_contract_serialize ( + const struct TALER_MERCHANT_Contract *input); /** @@ -1552,7 +2176,8 @@ TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product); * @param[in] contract contract to free */ void -TALER_MERCHANT_contract_free (struct TALER_MERCHANT_Contract *contract); +TALER_MERCHANT_contract_free ( + struct TALER_MERCHANT_Contract *contract); #endif diff --git a/src/testing/test_merchant_mfa.sh b/src/testing/test_merchant_mfa.sh @@ -60,7 +60,7 @@ echo -n "Self-provision instance ..." STATUS=$(curl -H "Content-Type: application/json" -X POST \ http://localhost:9966/instances \ - -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"1234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ + -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") @@ -81,19 +81,19 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") -if [ "$STATUS" != "204" ] +if [ "$STATUS" != "200" ] then jq < "$LAST_RESPONSE" - exit_fail "Expected 204 OK. Got: $STATUS" + exit_fail "Expected 200 OK. Got: $STATUS" fi echo "OK" -TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1) -ADDR=$(cat /tmp/test-merchant-sms-address.txt) +TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}') +ADDR=$(cat /tmp/test-merchant-email-address.txt) -if [ "$ADDR" != "1234" ] +if [ "$ADDR" != "self@example.com" ] then - exit_fail "Expected address '1234'. Got: $ADDR" + exit_fail "Expected address 'self@example.com'. Got: $ADDR" fi echo -n "Sending challenge $C1 solution " @@ -120,14 +120,14 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") -if [ "$STATUS" != "204" ] +if [ "$STATUS" != "200" ] then jq < "$LAST_RESPONSE" - exit_fail "Expected 204 OK. Got: $STATUS" + exit_fail "Expected 200 OK. Got: $STATUS" fi echo "OK" -TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1) +TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1 | awk '{print $1}') echo -n "Sending challenge $C2 solution " @@ -152,7 +152,7 @@ STATUS=$(curl \ -H "Taler-Challenge-Ids: $C1,$C2" \ -X POST \ http://localhost:9966/instances \ - -d '{"auth":{"method":"external"},"id":"self","name":"change","phone_number":"1234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ + -d '{"auth":{"method":"external"},"id":"self","name":"change","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") @@ -172,7 +172,7 @@ STATUS=$(curl \ -H "Taler-Challenge-Ids: $C1,$C2" \ -X POST \ http://localhost:9966/instances \ - -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"1234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ + -d '{"auth":{"method":"token", "password":"pass1234"},"id":"self","name":"default","phone_number":"+4171234","email":"self@example.com","address":{},"jurisdiction":{},"use_stefan":true,"default_wire_transfer_delay":{"d_us" : 50000000},"default_pay_delay":{"d_us": 60000000}}' \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") @@ -202,7 +202,7 @@ fi echo " OK" -echo -n "Trigger MFA to add bank account with 2-FA authorization " +echo -n "Do NOT Trigger MFA to add first bank account " STATUS=$(curl \ -H "Content-Type: application/json" \ -X POST \ @@ -212,6 +212,25 @@ STATUS=$(curl \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") +if [ "$STATUS" != "200" ] +then + jq < "$LAST_RESPONSE" + exit_fail "Expected 200 OK. Got: $STATUS" +fi + +echo " OK" + + +echo -n "Trigger MFA to add 2nd bank account with 2-FA authorization " +STATUS=$(curl \ + -H "Content-Type: application/json" \ + -X POST \ + -H 'Authorization: Bearer secret-token:pass1234' \ + http://localhost:9966/instances/self/private/accounts \ + -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/45?receiver-name=user45"}' \ + -w "%{http_code}" -s \ + -o "$LAST_RESPONSE") + if [ "$STATUS" != "202" ] then jq < "$LAST_RESPONSE" @@ -221,8 +240,10 @@ fi echo " OK" -C1=$(jq -r .challenges[1].challenge_id < "$LAST_RESPONSE") +C1=$(jq -r .challenges[0].challenge_id < "$LAST_RESPONSE") +# Delete old TANs. +rm /tmp/test-merchant-*-tan.txt echo -n "Requesting challenge $C1 " STATUS=$(curl -H "Content-Type: application/json" -X POST \ @@ -231,19 +252,19 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") -if [ "$STATUS" != "204" ] +if [ "$STATUS" != "200" ] then jq < "$LAST_RESPONSE" - exit_fail "Expected 204 OK. Got: $STATUS" + exit_fail "Expected 200 OK. Got: $STATUS" fi echo "OK" -TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1) -ADDR=$(cat /tmp/test-merchant-sms-address.txt) +TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}') +ADDR=$(cat /tmp/test-merchant-email-address.txt) -if [ "$ADDR" != "1234" ] +if [ "$ADDR" != "self@example.com" ] then - exit_fail "Expected address '1234'. Got: $ADDR" + exit_fail "Expected address 'self@example.com'. Got: $ADDR" fi echo -n "Sending challenge $C1 solution " @@ -263,14 +284,14 @@ then fi echo "OK" -echo -n "Finally, add bank account " +echo -n "Finally, add 2nd bank account " STATUS=$(curl \ -X POST \ -H "Content-Type: application/json" \ -H "Taler-Challenge-Ids: $C1" \ -H 'Authorization: Bearer secret-token:pass1234' \ http://localhost:9966/instances/self/private/accounts \ - -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/44?receiver-name=user44"}' \ + -d '{"payto_uri":"payto://x-taler-bank/localhost:8082/45?receiver-name=user45"}' \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") @@ -311,19 +332,19 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") -if [ "$STATUS" != "204" ] +if [ "$STATUS" != "200" ] then jq < "$LAST_RESPONSE" - exit_fail "Expected 204 OK. Got: $STATUS" + exit_fail "Expected 200 OK. Got: $STATUS" fi echo "OK" -TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1) -ADDR=$(cat /tmp/test-merchant-sms-address.txt) +TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1 | awk '{print $1}') +ADDR=$(cat /tmp/test-merchant-email-address.txt) -if [ "$ADDR" != "1234" ] +if [ "$ADDR" != "self@example.com" ] then - exit_fail "Expected address '1234'. Got: $ADDR" + exit_fail "Expected address 'self@example.com'. Got: $ADDR" fi echo -n "Sending challenge $C1 solution " @@ -350,14 +371,14 @@ STATUS=$(curl -H "Content-Type: application/json" -X POST \ -w "%{http_code}" -s \ -o "$LAST_RESPONSE") -if [ "$STATUS" != "204" ] +if [ "$STATUS" != "200" ] then jq < "$LAST_RESPONSE" - exit_fail "Expected 204 OK. Got: $STATUS" + exit_fail "Expected 200 OK. Got: $STATUS" fi echo "OK" -TAN=$(cat /tmp/test-merchant-email-tan.txt | head -n1) +TAN=$(cat /tmp/test-merchant-sms-tan.txt | head -n1 | awk '{print $1}') echo -n "Sending challenge $C2 solution " diff --git a/src/testing/test_merchant_statistics.sh b/src/testing/test_merchant_statistics.sh @@ -171,7 +171,7 @@ echo "OK" echo -n "Creating money pot..." STATUS=$(curl 'http://localhost:9966/private/pots' \ - -d '{"description":"First pot","pot_name":"#1"}' \ + -d '{"description":"First pot","pot_name":"pot1"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] @@ -247,7 +247,7 @@ echo "OK" echo -n "Patching money pot ..." STATUS=$(curl "http://localhost:9966/private/pots/$MONEY_POT" -X PATCH \ - -d '{"description":"Updated pot","expected_pot_totals":[],"new_pot_totals":["KUDOS:23"],"pot_name":"#1p"}' \ + -d '{"description":"Updated pot","expected_pot_totals":[],"new_pot_totals":["KUDOS:23"],"pot_name":"pot1p"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "204" ] diff --git a/src/util/base_terms_parse.c b/src/util/base_terms_parse.c @@ -0,0 +1,158 @@ +/* + This file is part of TALER + (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/base_terms_parse.c + * @brief shared logic for order and contract terms parsing + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdbool.h> +#include <stdint.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_util.h> +#include "taler/taler_merchant_util.h" + + +struct TALER_MERCHANT_ContractBaseTerms * +TALER_MERCHANT_base_terms_parse ( + json_t *input) +{ + struct TALER_MERCHANT_ContractBaseTerms *ct + = GNUNET_new (struct TALER_MERCHANT_ContractBaseTerms); + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_mark_optional ( + TALER_MERCHANT_spec_contract_version ("version", + &ct->version), + NULL), + GNUNET_JSON_spec_string_copy ("summary", + &ct->summary), + /* FIXME: do i18n_str validation in the future */ + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("summary_i18n", + &ct->summary_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("public_reorder_url", + &ct->public_reorder_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("fulfillment_url", + &ct->fulfillment_url), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("fulfillment_message", + &ct->fulfillment_message), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("fulfillment_message_i18n", + &ct->fulfillment_message_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("delivery_location", + &ct->delivery_location), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("delivery_date", + &ct->delivery_date), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("max_pickup_time", + &ct->max_pickup_time), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_relative_time ("auto_refund", + &ct->auto_refund), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("extra", + &ct->extra), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint8 ("minimum_age", + &ct->minimum_age), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("order_default_money_pot", + &ct->default_money_pot), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + GNUNET_assert (NULL != input); + ct->max_pickup_time = GNUNET_TIME_UNIT_FOREVER_TS; + ct->version = TALER_MERCHANT_CONTRACT_VERSION_0; + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse common terms at field %s\n", + ename); + goto cleanup; + } + + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse contract\n"); + goto cleanup; + } + return ct; + +cleanup: + TALER_MERCHANT_base_terms_free (ct); + return NULL; +} + + +void +TALER_MERCHANT_base_terms_free ( + struct TALER_MERCHANT_ContractBaseTerms *ct) +{ + if (NULL == ct) + return; + GNUNET_free (ct->public_reorder_url); + GNUNET_free (ct->summary); + GNUNET_free (ct->fulfillment_url); + GNUNET_free (ct->fulfillment_message); + if (NULL != ct->fulfillment_message_i18n) + { + json_decref (ct->fulfillment_message_i18n); + ct->fulfillment_message_i18n = NULL; + } + if (NULL != ct->delivery_location) + { + json_decref (ct->delivery_location); + ct->delivery_location = NULL; + } + if (NULL != ct->extra) + { + json_decref (ct->extra); + ct->extra = NULL; + } + GNUNET_free (ct); +} diff --git a/src/util/base_terms_serialize.c b/src/util/base_terms_serialize.c @@ -0,0 +1,88 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/base_terms_serialize.c + * @brief shared logic for contract base terms serialization + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + + +/** + * Serialize contract base terms into JSON object. + * + * @param[in] ct base contract terms to serialize + * @return JSON representation of @a ct; NULL on error + */ +json_t * +TALER_MERCHANT_base_terms_serialize ( + const struct TALER_MERCHANT_ContractBaseTerms *ct) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_uint64 ("version", + ct->version), + GNUNET_JSON_pack_string ("summary", + ct->summary), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("summary_i18n", + ct->summary_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("public_reorder_url", + ct->public_reorder_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_url", + ct->fulfillment_url)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("fulfillment_message", + ct->fulfillment_message)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("fulfillment_message_i18n", + ct->fulfillment_message_i18n)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("delivery_location", + ct->delivery_location)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_timestamp ("delivery_date", + ct->delivery_date)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_time_rel ("auto_refund", + ct->auto_refund)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("extra", + ct->extra)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_uint64 ("minimum_age", + ct->minimum_age)), + (0 == ct->default_money_pot) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("dummy", + NULL)) + : GNUNET_JSON_pack_uint64 ("order_default_money_pot", + ct->default_money_pot), + GNUNET_TIME_absolute_is_never (ct->max_pickup_time.abs_time) + ? GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("dummy", + NULL)) + : GNUNET_JSON_pack_timestamp ("max_pickup_time", + ct->max_pickup_time)); +} diff --git a/src/util/contract_choice_parse.c b/src/util/contract_choice_parse.c @@ -0,0 +1,468 @@ +/* + This file is part of TALER + (C) 2024, 2025, 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/contract_choice_parse.c + * @brief shared logic for contract choice parsing + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdbool.h> +#include <stdint.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_util.h> +#include "taler/taler_merchant_util.h" + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_contract_choice_input ( + json_t *root, + struct TALER_MERCHANT_ContractInput *input, + size_t index) +{ + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + TALER_MERCHANT_json_spec_cit ("type", + &input->type), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + ispec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + { + struct GNUNET_JSON_Specification spec[] = { + 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 (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'type' invalid in input #%u\n", + (unsigned int) index); + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_contract_choice_output ( + json_t *root, + struct TALER_MERCHANT_ContractOutput *output, + size_t index) +{ + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + TALER_MERCHANT_json_spec_cot ("type", + &output->type), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + ispec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("token_family_slug", + &output->details.token.token_family_slug), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint ("count", + &output->details.token.count), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("valid_at", + &output->details.token.valid_at), + NULL), + GNUNET_JSON_spec_uint ("key_index", + &output->details.token.key_index), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + return GNUNET_OK; + } + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + { + const json_t *donau_urls = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ( + "amount", + &output->details.donation_receipt.amount), + NULL), + GNUNET_JSON_spec_array_const ("donau_urls", + &donau_urls), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (output->details.donation_receipt.donau_urls, + output->details.donation_receipt.donau_urls_len, + json_array_size (donau_urls)); + + for (unsigned int i = 0; + i < output->details.donation_receipt.donau_urls_len; + i++) + { + const json_t *jurl; + + jurl = json_array_get (donau_urls, + i); + if (! json_is_string (jurl)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + output->details.donation_receipt.donau_urls[i] = + GNUNET_strdup (json_string_value (jurl)); + } + + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'type' invalid in output #%u\n", + (unsigned int) index); + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +/** + * Parse given JSON object to choices array. + * + * @param cls closure, pointer to array length + * @param root the json array representing the choices + * @param[out] ospec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_contract_choices ( + void *cls, + json_t *root, + struct GNUNET_JSON_Specification *ospec) +{ + struct TALER_MERCHANT_ContractChoice **choices = ospec->ptr; + unsigned int *choices_len = cls; + + if (! json_is_array (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == json_array_size (root)) + { + /* empty list of choices is not allowed */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *choices = NULL; + *choices_len = 0; + GNUNET_array_grow (*choices, + *choices_len, + json_array_size (root)); + + for (unsigned int i = 0; i < *choices_len; i++) + { + struct TALER_MERCHANT_ContractChoice *choice = &(*choices)[i]; + const json_t *jinputs = NULL; + const json_t *joutputs = NULL; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &choice->amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &choice->tip), + &choice->no_tip), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("description", + &choice->description), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("description_i18n", + &choice->description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_fee", + &choice->max_fee), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + NULL), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (root, i), + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (! choice->no_tip) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->tip)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Tip currency does not match amount currency in choice #%u\n", + i); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_ContractInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_contract_choice_input ((json_t *) jinput, + &input, + idx)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_break_op (0); + return GNUNET_SYSERR; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; + break; + } + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + } + } + + if (NULL != joutputs) + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_contract_choice_output ((json_t *) joutput, + &output, + idx)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break_op (0); + return GNUNET_SYSERR; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore output tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + break; + } + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + } + } + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_contract_choices ( + const char *name, + struct TALER_MERCHANT_ContractChoice **choices, + unsigned int *choices_len) +{ + struct GNUNET_JSON_Specification ret = { + .cls = (void *) choices_len, + .parser = &parse_contract_choices, + .field = name, + .ptr = choices, + }; + + return ret; +} + + +void +TALER_MERCHANT_contract_choice_free ( + struct TALER_MERCHANT_ContractChoice *choice) +{ + for (unsigned int i = 0; i < choice->outputs_len; i++) + { + struct TALER_MERCHANT_ContractOutput *output = &choice->outputs[i]; + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + for (unsigned int j = 0; + j<output->details.donation_receipt.donau_urls_len; + j++) + GNUNET_free (output->details.donation_receipt.donau_urls[j]); + GNUNET_array_grow (output->details.donation_receipt.donau_urls, + output->details.donation_receipt.donau_urls_len, + 0); + break; +#if FUTURE + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN: + GNUNET_free (output->details.coin.exchange_url); + break; +#endif + } + } + GNUNET_free (choice->description); + if (NULL != choice->description_i18n) + { + json_decref (choice->description_i18n); + choice->description_i18n = NULL; + } + GNUNET_free (choice->inputs); + GNUNET_free (choice->outputs); +} diff --git a/src/util/contract_choice_serialize.c b/src/util/contract_choice_serialize.c @@ -0,0 +1,166 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/contract_choice_serialize.c + * @brief shared logic for contract choice serialization + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + +/** + * Get JSON representation of contract choice input. + * + * @param[in] input contract terms choice input + * @return JSON representation of @a input; NULL on error + */ +static json_t * +json_from_contract_input ( + const struct TALER_MERCHANT_ContractInput *input) +{ + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "invalid contract input type"); + GNUNET_assert (0); + return NULL; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "token"), + GNUNET_JSON_pack_string ("token_family_slug", + input->details.token.token_family_slug), + GNUNET_JSON_pack_int64 ("count", + input->details.token.count)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "unsupported contract input type %d", + input->type); + GNUNET_assert (0); + return NULL; +} + + +/** + * Get JSON representation of contract choice output. + * + * @param[in] output contract terms choice output + * @return JSON representation of @a output; NULL on error + */ +static json_t * +json_from_contract_output ( + const struct TALER_MERCHANT_ContractOutput *output) +{ + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "invalid contract output type"); + GNUNET_assert (0); + return NULL; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "token"), + GNUNET_JSON_pack_string ("token_family_slug", + output->details.token.token_family_slug), + GNUNET_JSON_pack_uint64 ("count", + output->details.token.count), + GNUNET_JSON_pack_uint64 ("key_index", + output->details.token.key_index)); + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + { + json_t *donau_urls; + + donau_urls = json_array (); + GNUNET_assert (NULL != donau_urls); + for (unsigned i = 0; + i < output->details.donation_receipt.donau_urls_len; + i++) + GNUNET_assert (0 == + json_array_append_new ( + donau_urls, + json_string ( + output->details.donation_receipt.donau_urls[i]))); + + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "tax-receipt"), + GNUNET_JSON_pack_array_steal ("donau_urls", + donau_urls), + TALER_JSON_pack_amount ("amount", + &output->details.donation_receipt.amount)); + } + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unsupported contract output type %d", + output->type); + GNUNET_assert (0); + return NULL; +} + + +json_t * +TALER_MERCHANT_json_from_contract_choice ( + const struct TALER_MERCHANT_ContractChoice *choice) +{ + json_t *inputs; + json_t *outputs; + + inputs = json_array (); + GNUNET_assert (NULL != inputs); + for (unsigned int i = 0; i < choice->inputs_len; i++) + GNUNET_assert (0 == + json_array_append_new (inputs, + json_from_contract_input ( + &choice->inputs[i]))); + outputs = json_array (); + GNUNET_assert (NULL != outputs); + for (unsigned int i = 0; i < choice->outputs_len; i++) + GNUNET_assert (0 == + json_array_append_new (outputs, + json_from_contract_output ( + &choice->outputs[i]))); + + return GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &choice->amount), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + choice->no_tip + ? NULL + : &choice->tip)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("description", + choice->description)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + choice->description_i18n)), + TALER_JSON_pack_amount ("max_fee", + &choice->max_fee), + GNUNET_JSON_pack_array_steal ("inputs", + inputs), + GNUNET_JSON_pack_array_steal ("outputs", + outputs)); +} diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2024, 2025 Taler Systems SA + (C) 2024, 2025, 2026 Taler Systems SA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software @@ -31,1010 +31,6 @@ /** - * Parse merchant details of given JSON contract terms. - * - * @param cls closure, unused parameter - * @param root the JSON object representing data - * @param[out] ospec where to write the data - * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error - */ -static enum GNUNET_GenericReturnValue -parse_merchant_details (void *cls, - json_t *root, - struct GNUNET_JSON_Specification *ospec) -{ - struct TALER_MERCHANT_Contract *contract = ospec->ptr; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string_copy ("name", - &contract->merchant.name), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("email", - &contract->merchant.email), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("website", - &contract->merchant.website), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("logo", - &contract->merchant.logo), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("address", - &contract->merchant.address), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("jurisdiction", - &contract->merchant.jurisdiction), - NULL), - GNUNET_JSON_spec_end () - }; - const char *error_name; - unsigned int error_line; - - (void) cls; - if (GNUNET_OK != - GNUNET_JSON_parse (root, - spec, - &error_name, - &error_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - spec[error_line].field, - error_line, - error_name); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - return GNUNET_OK; -} - - -/** - * Provide specification to parse given JSON object to merchant details in the - * contract terms. All fields from @a contract are copied. - * - * @param name name of the merchant details field in the JSON - * @param[out] contract where the merchant details have to be written - */ -static struct GNUNET_JSON_Specification -spec_merchant_details (const char *name, - struct TALER_MERCHANT_Contract *contract) -{ - struct GNUNET_JSON_Specification ret = { - .parser = &parse_merchant_details, - .field = name, - .ptr = contract, - }; - - return ret; -} - - -/** - * Get enum value from contract token type string. - * - * @param str contract token type string - * @return enum value of token type - */ -static enum TALER_MERCHANT_ContractTokenKind -contract_token_kind_from_string (const char *str) -{ - if (0 == strcmp ("subscription", - str)) - return TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION; - if (0 == strcmp ("discount", - str)) - return TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT; - return TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID; -} - - -enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_choice_input ( - json_t *root, - struct TALER_MERCHANT_ContractInput *input, - size_t index, - bool order) -{ - const char *ename; - unsigned int eline; - struct GNUNET_JSON_Specification ispec[] = { - TALER_MERCHANT_json_spec_cit ("type", - &input->type), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - ispec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - ispec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - switch (input->type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_break (0); - break; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - { - struct GNUNET_JSON_Specification spec[] = { - 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 (root, - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - return GNUNET_OK; - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Field 'type' invalid in input #%u\n", - (unsigned int) index); - GNUNET_break_op (0); - return GNUNET_SYSERR; -} - - -enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_choice_output ( - json_t *root, - struct TALER_MERCHANT_ContractOutput *output, - size_t index, - bool order) -{ - const char *ename; - unsigned int eline; - struct GNUNET_JSON_Specification ispec[] = { - TALER_MERCHANT_json_spec_cot ("type", - &output->type), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - ispec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - ispec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_break (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - { - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("token_family_slug", - &output->details.token.token_family_slug), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint ("count", - &output->details.token.count), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("valid_at", - &output->details.token.valid_at), - NULL), - (! order) - ? GNUNET_JSON_spec_uint ("key_index", - &output->details.token.key_index) - : GNUNET_JSON_spec_end (), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - return GNUNET_OK; - } - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - { - const json_t *donau_urls = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("amount", - &output->details.donation_receipt.amount), - NULL), - (! order) - ? GNUNET_JSON_spec_array_const ("donau_urls", - &donau_urls) - : GNUNET_JSON_spec_end (), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - GNUNET_array_grow (output->details.donation_receipt.donau_urls, - output->details.donation_receipt.donau_urls_len, - json_array_size (donau_urls)); - - for (unsigned int i = 0; - i < output->details.donation_receipt.donau_urls_len; - i++) - { - const json_t *jurl; - - jurl = json_array_get (donau_urls, - i); - if (! json_is_string (jurl)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - output->details.donation_receipt.donau_urls[i] = - GNUNET_strdup (json_string_value (jurl)); - } - - return GNUNET_OK; - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Field 'type' invalid in output #%u\n", - (unsigned int) index); - GNUNET_break_op (0); - return GNUNET_SYSERR; -} - - -/** - * Parse given JSON object to choices array. - * - * @param cls closure, pointer to array length - * @param root the json array representing the choices - * @param[out] ospec where to write the data - * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error - */ -static enum GNUNET_GenericReturnValue -parse_choices ( - void *cls, - json_t *root, - struct GNUNET_JSON_Specification *ospec) -{ - struct TALER_MERCHANT_ContractChoice **choices = ospec->ptr; - unsigned int *choices_len = cls; - - if (! json_is_array (root)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (0 == json_array_size (root)) - { - /* empty list of choices is not allowed */ - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - *choices = NULL; - *choices_len = 0; - GNUNET_array_grow (*choices, - *choices_len, - json_array_size (root)); - - for (unsigned int i = 0; i < *choices_len; i++) - { - struct TALER_MERCHANT_ContractChoice *choice = &(*choices)[i]; - const json_t *jinputs = NULL; - const json_t *joutputs = NULL; - struct GNUNET_JSON_Specification spec[] = { - TALER_JSON_spec_amount_any ("amount", - &choice->amount), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("tip", - &choice->tip), - &choice->no_tip), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("description", - &choice->description), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("description_i18n", - &choice->description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("max_fee", - &choice->max_fee), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("inputs", - &jinputs), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("outputs", - &joutputs), - NULL), - GNUNET_JSON_spec_end () - }; - const char *ename; - unsigned int eline; - - if (GNUNET_OK != - GNUNET_JSON_parse (json_array_get (root, i), - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if ( (! choice->no_tip) && - (GNUNET_OK != - TALER_amount_cmp_currency (&choice->amount, - &choice->tip)) ) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Tip currency does not match amount currency in choice #%u\n", - i); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (NULL != jinputs) - { - const json_t *jinput; - size_t idx; - - json_array_foreach ((json_t *) jinputs, idx, jinput) - { - struct TALER_MERCHANT_ContractInput input = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_input ((json_t *) jinput, - &input, - idx, - false)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - switch (input.type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_break_op (0); - return GNUNET_SYSERR; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - /* Ignore inputs tokens with 'count' field set to 0 */ - if (0 == input.details.token.count) - continue; - break; - } - GNUNET_array_append (choice->inputs, - choice->inputs_len, - input); - } - } - - if (NULL != joutputs) - { - const json_t *joutput; - size_t idx; - json_array_foreach ((json_t *) joutputs, idx, joutput) - { - struct TALER_MERCHANT_ContractOutput output = { - .details.token.count = 1 - }; - - if (GNUNET_OK != - TALER_MERCHANT_parse_choice_output ((json_t *) joutput, - &output, - idx, - false)) - { - GNUNET_break (0); - return GNUNET_SYSERR; - } - switch (output.type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_break_op (0); - return GNUNET_SYSERR; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - /* Ignore output tokens with 'count' field set to 0 */ - if (0 == output.details.token.count) - continue; - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - break; - } - GNUNET_array_append (choice->outputs, - choice->outputs_len, - output); - } - } - } - - return GNUNET_OK; -} - - -struct GNUNET_JSON_Specification -TALER_MERCHANT_spec_choices ( - const char *name, - struct TALER_MERCHANT_ContractChoice **choices, - unsigned int *choices_len) -{ - struct GNUNET_JSON_Specification ret = { - .cls = (void *) choices_len, - .parser = &parse_choices, - .field = name, - .ptr = choices, - }; - - return ret; -} - - -void -TALER_MERCHANT_contract_choice_free ( - struct TALER_MERCHANT_ContractChoice *choice) -{ - for (unsigned int i = 0; i < choice->outputs_len; i++) - { - struct TALER_MERCHANT_ContractOutput *output = &choice->outputs[i]; - - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_break (0); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - for (unsigned int j = 0; - j<output->details.donation_receipt.donau_urls_len; - j++) - GNUNET_free (output->details.donation_receipt.donau_urls[j]); - GNUNET_array_grow (output->details.donation_receipt.donau_urls, - output->details.donation_receipt.donau_urls_len, - 0); - break; -#if FUTURE - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN: - GNUNET_free (output->details.coin.exchange_url); - break; -#endif - } - } - GNUNET_free (choice->description); - if (NULL != choice->description_i18n) - { - json_decref (choice->description_i18n); - choice->description_i18n = NULL; - } - GNUNET_free (choice->inputs); - GNUNET_free (choice->outputs); -} - - -/** - * Parse token details of the given JSON contract token family. - * - * @param cls closure, unused parameter - * @param root the JSON object representing data - * @param[out] ospec where to write the data - * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error - */ -static enum GNUNET_GenericReturnValue -parse_token_details (void *cls, - json_t *root, - struct GNUNET_JSON_Specification *ospec) -{ - struct TALER_MERCHANT_ContractTokenFamily *family = ospec->ptr; - const char *class; - const char *ename; - unsigned int eline; - struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("class", - &class), - GNUNET_JSON_spec_end () - }; - - (void) cls; - if (GNUNET_OK != - GNUNET_JSON_parse (root, - ispec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse %s at %u: %s\n", - ispec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - family->kind = contract_token_kind_from_string (class); - - switch (family->kind) - { - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: - { - const json_t *trusted_domains; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("trusted_domains", - &trusted_domains), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - GNUNET_array_grow (family->details.subscription.trusted_domains, - family->details.subscription.trusted_domains_len, - json_array_size (trusted_domains)); - - for (unsigned int i = 0; - i < family->details.subscription.trusted_domains_len; - i++) - { - const json_t *jdomain; - jdomain = json_array_get (trusted_domains, i); - - if (! json_is_string (jdomain)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - family->details.subscription.trusted_domains[i] = - GNUNET_strdup (json_string_value (jdomain)); - } - - return GNUNET_OK; - } - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: - { - const json_t *expected_domains; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_array_const ("expected_domains", - &expected_domains), - GNUNET_JSON_spec_end () - }; - - if (GNUNET_OK != - GNUNET_JSON_parse (root, - spec, - &ename, - &eline)) - { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse %s at %u: %s\n", - spec[eline].field, - eline, - ename); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - GNUNET_array_grow (family->details.discount.expected_domains, - family->details.discount.expected_domains_len, - json_array_size (expected_domains)); - - for (unsigned int i = 0; - i < family->details.discount.expected_domains_len; - i++) - { - const json_t *jdomain; - - jdomain = json_array_get (expected_domains, - i); - if (! json_is_string (jdomain)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - family->details.discount.expected_domains[i] = - GNUNET_strdup (json_string_value (jdomain)); - } - - return GNUNET_OK; - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Field 'type' invalid in token family\n"); - GNUNET_break_op (0); - return GNUNET_SYSERR; -} - - -/** - * Provide specification to parse given JSON object to contract token details - * in the contract token family. All fields from @a family are copied. - * - * @param name name of the token details field in the JSON - * @param[out] family token_family where the token details have to be written - */ -static struct GNUNET_JSON_Specification -spec_token_details (const char *name, - struct TALER_MERCHANT_ContractTokenFamily *family) -{ - struct GNUNET_JSON_Specification ret = { - .parser = &parse_token_details, - .field = name, - .ptr = family, - }; - - return ret; -} - - -/** - * Parse given JSON object to token families array. - * - * @param cls closure, pointer to array length - * @param root the json object representing the token families. The keys are - * the token family slugs. - * @param[out] ospec where to write the data - * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error - */ -static enum GNUNET_GenericReturnValue -parse_token_families (void *cls, - json_t *root, - struct GNUNET_JSON_Specification *ospec) -{ - struct TALER_MERCHANT_ContractTokenFamily **families = ospec->ptr; - unsigned int *families_len = cls; - json_t *jfamily; - const char *slug; - - if (! json_is_object (root)) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - json_object_foreach (root, slug, jfamily) - { - const json_t *keys; - struct TALER_MERCHANT_ContractTokenFamily family = { - .slug = GNUNET_strdup (slug) - }; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string_copy ("name", - &family.name), - GNUNET_JSON_spec_string_copy ("description", - &family.description), - GNUNET_JSON_spec_object_copy ("description_i18n", - &family.description_i18n), - GNUNET_JSON_spec_array_const ("keys", - &keys), - spec_token_details ("details", - &family), - GNUNET_JSON_spec_bool ("critical", - &family.critical), - GNUNET_JSON_spec_end () - }; - const char *error_name; - unsigned int error_line; - - if (GNUNET_OK != - GNUNET_JSON_parse (jfamily, - spec, - &error_name, - &error_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - spec[error_line].field, - error_line, - error_name); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - GNUNET_array_grow (family.keys, - family.keys_len, - json_array_size (keys)); - - for (unsigned int i = 0; i<family.keys_len; i++) - { - struct TALER_MERCHANT_ContractTokenFamilyKey *key = &family.keys[i]; - struct GNUNET_JSON_Specification key_spec[] = { - TALER_JSON_spec_token_pub ( - NULL, - &key->pub), - GNUNET_JSON_spec_timestamp ( - "signature_validity_start", - &key->valid_after), - GNUNET_JSON_spec_timestamp ( - "signature_validity_end", - &key->valid_before), - GNUNET_JSON_spec_end () - }; - const char *ierror_name; - unsigned int ierror_line; - - if (GNUNET_OK != - GNUNET_JSON_parse (json_array_get (keys, - i), - key_spec, - &ierror_name, - &ierror_line)) - { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Failed to parse %s at %u: %s\n", - key_spec[ierror_line].field, - ierror_line, - ierror_name); - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - } - - GNUNET_array_append (*families, - *families_len, - family); - } - - return GNUNET_OK; -} - - -/** - * Provide specification to parse given JSON array to token families in the - * contract terms. All fields from @a families items are copied. - * - * @param name name of the token families field in the JSON - * @param[out] families where the token families array has to be written - * @param[out] families_len length of the @a families array - */ -static struct GNUNET_JSON_Specification -spec_token_families ( - const char *name, - struct TALER_MERCHANT_ContractTokenFamily **families, - unsigned int *families_len) -{ - struct GNUNET_JSON_Specification ret = { - .cls = (void *) families_len, - .parser = &parse_token_families, - .field = name, - .ptr = families, - }; - - return ret; -} - - -enum GNUNET_GenericReturnValue -TALER_MERCHANT_find_token_family_key ( - const char *slug, - struct GNUNET_TIME_Timestamp valid_after, - const struct TALER_MERCHANT_ContractTokenFamily *families, - unsigned int families_len, - struct TALER_MERCHANT_ContractTokenFamily *family, - struct TALER_MERCHANT_ContractTokenFamilyKey *key) -{ - for (unsigned int i = 0; i < families_len; i++) - { - const struct TALER_MERCHANT_ContractTokenFamily *fami - = &families[i]; - - if (0 != strcmp (fami->slug, - slug)) - continue; - if (NULL != family) - *family = *fami; - for (unsigned int k = 0; k < fami->keys_len; k++) - { - struct TALER_MERCHANT_ContractTokenFamilyKey *ki = &fami->keys[k]; - - if (GNUNET_TIME_timestamp_cmp (ki->valid_after, - ==, - valid_after)) - { - if (NULL != key) - *key = *ki; - return GNUNET_OK; - } - } - /* matching family found, but no key. */ - return GNUNET_NO; - } - - /* no matching family found */ - return GNUNET_SYSERR; -} - - -/** - * Free all the fields in the given @a family, but not @a family itself, since - * it is normally part of an array. - * - * @param[in] family contract token family to free - */ -static void -contract_token_family_free ( - struct TALER_MERCHANT_ContractTokenFamily *family) -{ - GNUNET_free (family->slug); - GNUNET_free (family->name); - GNUNET_free (family->description); - if (NULL != family->description_i18n) - { - json_decref (family->description_i18n); - family->description_i18n = NULL; - } - for (unsigned int i = 0; i < family->keys_len; i++) - TALER_token_issue_pub_free (&family->keys[i].pub); - GNUNET_free (family->keys); - - switch (family->kind) - { - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: - for (unsigned int i = 0; i < family->details.discount.expected_domains_len; - i++) - GNUNET_free (family->details.discount.expected_domains[i]); - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: - for (unsigned int i = 0; i < family->details.subscription. - trusted_domains_len; i++) - GNUNET_free (family->details.subscription.trusted_domains[i]); - break; - } -} - - -/** - * Parse contract version of given JSON contract terms. - * - * @param cls closure, unused parameter - * @param root the JSON object representing data - * @param[out] spec where to write the data - * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error - */ -static enum GNUNET_GenericReturnValue -parse_contract_version (void *cls, - json_t *root, - struct GNUNET_JSON_Specification *spec) -{ - enum TALER_MERCHANT_ContractVersion *res - = (enum TALER_MERCHANT_ContractVersion *) spec->ptr; - - (void) cls; - if (json_is_integer (root)) - { - json_int_t version = json_integer_value (root); - - switch (version) - { - case 0: - *res = TALER_MERCHANT_CONTRACT_VERSION_0; - return GNUNET_OK; - case 1: - *res = TALER_MERCHANT_CONTRACT_VERSION_1; - return GNUNET_OK; - } - - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - - if (json_is_null (root)) - { - *res = TALER_MERCHANT_CONTRACT_VERSION_0; - return GNUNET_OK; - } - - GNUNET_break_op (0); - return GNUNET_SYSERR; -} - - -/** - * Create JSON specification to parse a merchant contract - * version. - * - * @param name name of the field - * @param[out] version where to write the contract version - * @return JSON specification object - */ -static struct GNUNET_JSON_Specification -spec_contract_version ( - const char *name, - enum TALER_MERCHANT_ContractVersion *version) -{ - struct GNUNET_JSON_Specification ret = { - .parser = &parse_contract_version, - .field = name, - .ptr = version - }; - - *version = TALER_MERCHANT_CONTRACT_VERSION_0; - return ret; -} - - -/** * Parse v0-specific fields of @a input JSON into @a contract. * * @param[in] input the JSON contract terms @@ -1044,7 +40,7 @@ spec_contract_version ( static enum GNUNET_GenericReturnValue parse_contract_v0 ( json_t *input, - struct TALER_MERCHANT_Contract *contract) + struct TALER_MERCHANT_ProtoContract *contract) { struct GNUNET_JSON_Specification espec[] = { TALER_JSON_spec_amount_any ("amount", @@ -1099,26 +95,26 @@ parse_contract_v0 ( /** - * Parse v1-specific fields of @a input JSON into @a contract. + * Parse v1-specific fields of @a input JSON into @a pc. * * @param[in] input the JSON contract terms - * @param[out] contract where to write the data + * @param[out] pc where to write the data * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error */ static enum GNUNET_GenericReturnValue parse_contract_v1 ( json_t *input, - struct TALER_MERCHANT_Contract *contract) + struct TALER_MERCHANT_ProtoContract *pc) { struct GNUNET_JSON_Specification espec[] = { - TALER_MERCHANT_spec_choices ( + TALER_MERCHANT_spec_contract_choices ( "choices", - &contract->details.v1.choices, - &contract->details.v1.choices_len), - spec_token_families ( + &pc->details.v1.choices, + &pc->details.v1.choices_len), + TALER_MERCHANT_spec_token_families ( "token_families", - &contract->details.v1.token_authorities, - &contract->details.v1.token_authorities_len), + &pc->details.v1.token_authorities, + &pc->details.v1.token_authorities_len), GNUNET_JSON_spec_end () }; @@ -1143,447 +139,204 @@ parse_contract_v1 ( } -/** - * Parse the given unit quantity string @a s and store the result in @a q. - * - * @param s quantity to parse - * @param[out] q where to store the result - * @return #GNUNET_OK on success - */ -static enum GNUNET_GenericReturnValue -parse_unit_quantity (const char *s, - struct TALER_MERCHANT_ProductQuantity *q) +struct TALER_MERCHANT_ProtoContract * +TALER_MERCHANT_proto_contract_parse ( + json_t *input) { - const char *ptr; - uint64_t integer = 0; - uint32_t frac = 0; - unsigned int digits = 0; + struct TALER_MERCHANT_ContractBaseTerms *base; + struct TALER_MERCHANT_ProtoContract *pc; + enum GNUNET_GenericReturnValue res; - if (NULL == s) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - ptr = s; - if ('\0' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if ('-' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - if (! isdigit ((unsigned char) *ptr)) + base = TALER_MERCHANT_base_terms_parse (input); + if (NULL == base) { GNUNET_break_op (0); - return GNUNET_SYSERR; - } - while (isdigit ((unsigned char) *ptr)) - { - unsigned int digit = (unsigned int) (*ptr - '0'); - - /* We intentionally allow at most INT64_MAX (as -1 has special meanings), - even though the data type would support UINT64_MAX */ - if (integer > (INT64_MAX - digit) / 10) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - integer = integer * 10 + digit; - ptr++; - } - if ('.' == *ptr) - { - ptr++; - if ('\0' == *ptr) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - while (isdigit ((unsigned char) *ptr)) - { - unsigned int digit = (unsigned int) (*ptr - '0'); - - if (digits >= TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) - { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - frac = (uint32_t) (frac * 10 + digit); - digits++; - ptr++; - } - while (digits < TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) - { - frac *= 10; - digits++; - } + return NULL; } - if ('\0' != *ptr) + pc = GNUNET_new (struct TALER_MERCHANT_ProtoContract); + pc->base = base; { - GNUNET_break_op (0); - return GNUNET_SYSERR; - } - q->integer = integer; - q->fractional = frac; - return GNUNET_OK; -} - - -bool -TALER_MERCHANT_taxes_array_valid (const json_t *taxes) -{ - json_t *tax; - size_t idx; + const json_t *products = NULL; + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_string_copy ("order_id", + &pc->order_id), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("products", + &products), + NULL), + GNUNET_JSON_spec_timestamp ("timestamp", + &pc->timestamp), + GNUNET_JSON_spec_timestamp ("refund_deadline", + &pc->refund_deadline), + GNUNET_JSON_spec_timestamp ("pay_deadline", + &pc->pay_deadline), + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &pc->wire_deadline), + GNUNET_JSON_spec_fixed_auto ("merchant_pub", + &pc->merchant_pub), + GNUNET_JSON_spec_fixed_auto ("h_wire", + &pc->h_wire), + GNUNET_JSON_spec_string_copy ("merchant_base_url", + &pc->merchant_base_url), + TALER_MERCHANT_spec_merchant_details ("merchant", + &pc->merchant), + GNUNET_JSON_spec_string_copy ("wire_method", + &pc->wire_method), + GNUNET_JSON_spec_array_copy ("exchanges", + &pc->exchanges), - if (! json_is_array (taxes)) - return false; - json_array_foreach (taxes, idx, tax) - { - struct TALER_Amount amount; - const char *name; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_string ("name", - &name), - TALER_JSON_spec_amount_any ("tax", - &amount), GNUNET_JSON_spec_end () }; - enum GNUNET_GenericReturnValue res; const char *ename; unsigned int eline; - res = GNUNET_JSON_parse (tax, - spec, + res = GNUNET_JSON_parse (input, + espec, &ename, &eline); if (GNUNET_OK != res) { GNUNET_break_op (0); - return false; + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse proto contract at field %s\n", + ename); + goto cleanup; } - } - return true; -} - - -enum GNUNET_GenericReturnValue -TALER_MERCHANT_parse_product_sold (const json_t *pj, - struct TALER_MERCHANT_ProductSold *r) -{ - bool no_quantity; - bool no_unit_quantity; - bool no_price; - uint64_t legacy_quantity; - const char *unit_quantity_s; - struct TALER_Amount price; - const json_t *prices = NULL; - struct GNUNET_JSON_Specification spec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("product_id", - &r->product_id), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("product_name", - &r->product_name), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("description", - &r->description), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("description_i18n", - &r->description_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("quantity", - &legacy_quantity), - &no_quantity), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string ("unit_quantity", - &unit_quantity_s), - &no_unit_quantity), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("unit", - &r->unit), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_amount_any ("price", - &price), - &no_price), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("prices", - &prices), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("image", - &r->image), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_copy ("taxes", - &r->taxes), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &r->delivery_date), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("product_money_pot", - &r->product_money_pot), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *ename; - unsigned int eline; - - r->delivery_date = GNUNET_TIME_UNIT_FOREVER_TS; - res = GNUNET_JSON_parse (pj, - spec, - &ename, - &eline); - if (GNUNET_OK != res) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse product at field %s\n", - ename); - return GNUNET_SYSERR; - } - if (! no_quantity) - { - r->unit_quantity.integer = legacy_quantity; - r->unit_quantity.fractional = 0; - } - if (! no_unit_quantity) - { - if (GNUNET_OK != - parse_unit_quantity (unit_quantity_s, - &r->unit_quantity)) + if (NULL != products) { - GNUNET_break (0); - return GNUNET_SYSERR; + pc->products_len = json_array_size (products); + if (0 != pc->products_len) + { + size_t i; + json_t *p; + + pc->products = GNUNET_new_array ( + pc->products_len, + struct TALER_MERCHANT_ProductSold); + json_array_foreach (products, i, p) + { + if (GNUNET_OK != + TALER_MERCHANT_parse_product_sold (p, + &pc->products[i], + false)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse product at offset %u\n", + (unsigned int) i); + goto cleanup; + } + } + } } } - if ( (! no_quantity) && (! no_unit_quantity) ) + switch (base->version) { - GNUNET_break ( (0 == r->unit_quantity.fractional) && - (legacy_quantity == r->unit_quantity.integer) ); + case TALER_MERCHANT_CONTRACT_VERSION_0: + if (GNUNET_OK == + parse_contract_v0 (input, + pc)) + return pc; + GNUNET_break_op (0); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (GNUNET_OK == + parse_contract_v1 (input, + pc)) + return pc; + GNUNET_break_op (0); + break; } - if ( (NULL != r->image) && - (! TALER_MERCHANT_image_data_url_valid (r->image)) ) +cleanup: + TALER_MERCHANT_proto_contract_free (pc); + return NULL; +} + + +/** + * Free the proto-contract at @a pc + * + * @param[in] pc proto-contract to free + */ +void +TALER_MERCHANT_proto_contract_free ( + struct TALER_MERCHANT_ProtoContract *pc) +{ + switch (pc->base->version) { - GNUNET_break_op (0); - return GNUNET_SYSERR; + case TALER_MERCHANT_CONTRACT_VERSION_0: + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + for (unsigned int i = 0; + i < pc->details.v1.choices_len; + i++) + TALER_MERCHANT_contract_choice_free ( + &pc->details.v1.choices[i]); + GNUNET_free (pc->details.v1.choices); + for (unsigned int i = 0; + i < pc->details.v1.token_authorities_len; + i++) + TALER_MERCHANT_contract_token_family_free ( + &pc->details.v1.token_authorities[i]); + GNUNET_free (pc->details.v1.token_authorities); + break; } - if ( (NULL != r->taxes) && - (! TALER_MERCHANT_taxes_array_valid (r->taxes)) ) + if (NULL != pc->products) { - GNUNET_break_op (0); - return GNUNET_SYSERR; + for (size_t i = 0; i<pc->products_len; i++) + TALER_MERCHANT_product_sold_free (&pc->products[i]); + GNUNET_free (pc->products); } - if (NULL != prices) + if (NULL != pc->base) { - size_t len = json_array_size (prices); - size_t i; - json_t *price_i; - - GNUNET_assert (len < UINT_MAX); - r->prices = GNUNET_new_array ((unsigned int) len, - struct TALER_Amount); - json_array_foreach (prices, i, price_i) - { - struct GNUNET_JSON_Specification pspec[] = { - TALER_JSON_spec_amount_any (NULL, - &r->prices[i]), - GNUNET_JSON_spec_end () - }; - - res = GNUNET_JSON_parse (price_i, - pspec, - &ename, - &eline); - if (GNUNET_OK != res) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse price at index %u\n", - (unsigned int) i); - return GNUNET_SYSERR; - } - } + TALER_MERCHANT_base_terms_free (pc->base); + pc->base = NULL; } - else if (! no_price) + GNUNET_free (pc->wire_method); + if (NULL != pc->exchanges) { - r->prices = GNUNET_new_array (1, - struct TALER_Amount); - r->prices[0] = price; + json_decref (pc->exchanges); + pc->exchanges = NULL; } - return GNUNET_OK; + GNUNET_free (pc->order_id); + GNUNET_free (pc); } struct TALER_MERCHANT_Contract * -TALER_MERCHANT_contract_parse (json_t *input, - bool nonce_optional) +TALER_MERCHANT_contract_parse (json_t *input) { struct TALER_MERCHANT_Contract *contract = GNUNET_new (struct TALER_MERCHANT_Contract); - const json_t *products = NULL; struct GNUNET_JSON_Specification espec[] = { - spec_contract_version ("version", - &contract->version), - GNUNET_JSON_spec_string_copy ("summary", - &contract->summary), - /* FIXME: do i18n_str validation in the future */ - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("summary_i18n", - &contract->summary_i18n), - NULL), - GNUNET_JSON_spec_string_copy ("order_id", - &contract->order_id), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("public_reorder_url", - &contract->public_reorder_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("fulfillment_url", - &contract->fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("fulfillment_message", - &contract->fulfillment_message), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("fulfillment_message_i18n", - &contract->fulfillment_message_i18n), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("products", - &products), - NULL), - GNUNET_JSON_spec_timestamp ("timestamp", - &contract->timestamp), - GNUNET_JSON_spec_timestamp ("refund_deadline", - &contract->refund_deadline), - GNUNET_JSON_spec_timestamp ("pay_deadline", - &contract->pay_deadline), - GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", - &contract->wire_deadline), - GNUNET_JSON_spec_fixed_auto ("merchant_pub", - &contract->merchant_pub), - GNUNET_JSON_spec_string_copy ("merchant_base_url", - &contract->merchant_base_url), - spec_merchant_details ("merchant", - contract), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &contract->h_wire), - GNUNET_JSON_spec_string_copy ("wire_method", - &contract->wire_method), - GNUNET_JSON_spec_array_copy ("exchanges", - &contract->exchanges), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("delivery_location", - &contract->delivery_location), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("delivery_date", - &contract->delivery_date), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_timestamp ("max_pickup_time", - &contract->max_pickup_time), - NULL), - (nonce_optional) - ? GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_string_copy ("nonce", - &contract->nonce), - NULL) - : GNUNET_JSON_spec_string_copy ("nonce", - &contract->nonce), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_relative_time ("auto_refund", - &contract->auto_refund), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_object_copy ("extra", - &contract->extra), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint8 ("minimum_age", - &contract->minimum_age), - NULL), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("default_money_pot", - &contract->default_money_pot), - NULL), + GNUNET_JSON_spec_string_copy ("nonce", + &contract->nonce), GNUNET_JSON_spec_end () }; - enum GNUNET_GenericReturnValue res; const char *ename; unsigned int eline; GNUNET_assert (NULL != input); - contract->max_pickup_time = GNUNET_TIME_UNIT_FOREVER_TS; + contract->pc = TALER_MERCHANT_proto_contract_parse (input); + if (NULL == contract->pc) + { + GNUNET_break_op (0); + GNUNET_free (contract); + return NULL; + } res = GNUNET_JSON_parse (input, espec, &ename, &eline); if (GNUNET_OK != res) { - GNUNET_break (0); + GNUNET_break_op (0); GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to parse contract at field %s\n", ename); goto cleanup; } - if (NULL != products) - { - contract->products_len = json_array_size (products); - if (0 != contract->products_len) - { - size_t i; - json_t *p; - - contract->products = GNUNET_new_array (contract->products_len, - struct TALER_MERCHANT_ProductSold); - json_array_foreach (products, i, p) - { - if (GNUNET_OK != - TALER_MERCHANT_parse_product_sold (p, - &contract->products[i])) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse product at offset %u\n", - (unsigned int) i); - goto cleanup; - } - } - } - } - switch (contract->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - res = parse_contract_v0 (input, - contract); - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - res = parse_contract_v1 (input, - contract); - break; - } - - if (GNUNET_OK != res) - { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse contract\n"); - goto cleanup; - } return contract; cleanup: @@ -1593,90 +346,16 @@ cleanup: void -TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product) -{ - GNUNET_free (product->product_id); - GNUNET_free (product->product_name); - GNUNET_free (product->description); - json_decref (product->description_i18n); - GNUNET_free (product->prices); - GNUNET_free (product->unit); - GNUNET_free (product->image); - json_decref (product->taxes); -} - - -void TALER_MERCHANT_contract_free ( struct TALER_MERCHANT_Contract *contract) { if (NULL == contract) return; - GNUNET_free (contract->public_reorder_url); - GNUNET_free (contract->order_id); - GNUNET_free (contract->merchant_base_url); - GNUNET_free (contract->merchant.name); - GNUNET_free (contract->merchant.website); - GNUNET_free (contract->merchant.email); - GNUNET_free (contract->merchant.logo); - if (NULL != contract->merchant.address) - { - json_decref (contract->merchant.address); - contract->merchant.address = NULL; - } - if (NULL != contract->merchant.jurisdiction) - { - json_decref (contract->merchant.jurisdiction); - contract->merchant.jurisdiction = NULL; - } - GNUNET_free (contract->summary); - GNUNET_free (contract->fulfillment_url); - GNUNET_free (contract->fulfillment_message); - if (NULL != contract->fulfillment_message_i18n) - { - json_decref (contract->fulfillment_message_i18n); - contract->fulfillment_message_i18n = NULL; - } - if (NULL != contract->products) - { - for (size_t i = 0; i<contract->products_len; i++) - TALER_MERCHANT_product_sold_free (&contract->products[i]); - GNUNET_free (contract->products); - } - GNUNET_free (contract->wire_method); - if (NULL != contract->exchanges) - { - json_decref (contract->exchanges); - contract->exchanges = NULL; - } - if (NULL != contract->delivery_location) + if (NULL != contract->pc) { - json_decref (contract->delivery_location); - contract->delivery_location = NULL; + TALER_MERCHANT_proto_contract_free (contract->pc); + contract->pc = NULL; } GNUNET_free (contract->nonce); - if (NULL != contract->extra) - { - json_decref (contract->extra); - contract->extra = NULL; - } - - switch (contract->version) - { - case TALER_MERCHANT_CONTRACT_VERSION_0: - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - for (unsigned int i = 0; - i < contract->details.v1.choices_len; - i++) - TALER_MERCHANT_contract_choice_free (&contract->details.v1.choices[i]); - GNUNET_free (contract->details.v1.choices); - for (unsigned int i = 0; - i < contract->details.v1.token_authorities_len; - i++) - contract_token_family_free (&contract->details.v1.token_authorities[i]); - GNUNET_free (contract->details.v1.token_authorities); - break; - } GNUNET_free (contract); } diff --git a/src/util/contract_serialize.c b/src/util/contract_serialize.c @@ -19,7 +19,6 @@ * @author Iván Ávalos * @author Christian Grothoff */ - #include "platform.h" #include <gnunet/gnunet_json_lib.h> #include <gnunet/gnunet_common.h> @@ -28,310 +27,6 @@ #include "taler/taler_util.h" #include "taler/taler_merchant_util.h" -/** - * Get JSON representation of merchant details. - * - * @param[in] contract contract terms - * @return JSON object with merchant details; NULL on error - */ -static json_t * -json_from_merchant_details ( - const struct TALER_MERCHANT_Contract *contract) -{ - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - contract->merchant.name), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("email", - contract->merchant.email)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("website", - contract->merchant.website)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("logo", - contract->merchant.logo)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("address", - contract->merchant.address)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("jurisdiction", - contract->merchant.jurisdiction))); -} - - -/** - * Get JSON representation of contract choice input. - * - * @param[in] input contract terms choice input - * @return JSON representation of @a input; NULL on error - */ -static json_t * -json_from_contract_input ( - const struct TALER_MERCHANT_ContractInput *input) -{ - switch (input->type) - { - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "invalid contract input type"); - GNUNET_assert (0); - return NULL; - case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "token"), - GNUNET_JSON_pack_string ("token_family_slug", - input->details.token.token_family_slug), - GNUNET_JSON_pack_int64 ("count", - input->details.token.count)); - } - - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "unsupported contract input type %d", - input->type); - GNUNET_assert (0); - return NULL; -} - - -/** - * Get JSON representation of contract choice output. - * - * @param[in] output contract terms choice output - * @return JSON representation of @a output; NULL on error - */ -static json_t * -json_from_contract_output ( - const struct TALER_MERCHANT_ContractOutput *output) -{ - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "invalid contract output type"); - GNUNET_assert (0); - return NULL; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "token"), - GNUNET_JSON_pack_string ("token_family_slug", - output->details.token.token_family_slug), - GNUNET_JSON_pack_uint64 ("count", - output->details.token.count), - GNUNET_JSON_pack_uint64 ("key_index", - output->details.token.key_index)); - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - { - json_t *donau_urls; - - donau_urls = json_array (); - GNUNET_assert (NULL != donau_urls); - for (unsigned i = 0; - i < output->details.donation_receipt.donau_urls_len; - i++) - GNUNET_assert (0 == - json_array_append_new ( - donau_urls, - json_string ( - output->details.donation_receipt.donau_urls[i]))); - - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("type", - "tax-receipt"), - GNUNET_JSON_pack_array_steal ("donau_urls", - donau_urls), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("amount", - &output->details.donation_receipt.amount))); - } - } - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Unsupported contract output type %d", - output->type); - GNUNET_assert (0); - return NULL; -} - - -json_t * -TALER_MERCHANT_json_from_contract_choice ( - const struct TALER_MERCHANT_ContractChoice *choice, - bool order) -{ - json_t *inputs; - json_t *outputs; - - inputs = json_array (); - GNUNET_assert (NULL != inputs); - for (unsigned int i = 0; i < choice->inputs_len; i++) - GNUNET_assert (0 == - json_array_append_new (inputs, - json_from_contract_input ( - &choice->inputs[i]))); - outputs = json_array (); - GNUNET_assert (NULL != outputs); - for (unsigned int i = 0; i < choice->outputs_len; i++) - GNUNET_assert (0 == - json_array_append_new (outputs, - json_from_contract_output ( - &choice->outputs[i]))); - - return GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &choice->amount), - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("tip", - choice->no_tip - ? NULL - : &choice->tip)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("description", - choice->description)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("description_i18n", - choice->description_i18n)), - (order) - ? GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ( - "max_fee", - /* workaround for nullable amount */ - (GNUNET_OK == - TALER_amount_is_valid (&choice->max_fee)) - ? &choice->max_fee - : NULL)) - : TALER_JSON_pack_amount ("max_fee", - &choice->max_fee), - (order) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("inputs", - inputs)) - : GNUNET_JSON_pack_array_steal ("inputs", - inputs), - (order) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_steal ("outputs", - outputs)) - : GNUNET_JSON_pack_array_steal ("outputs", - outputs)); -} - - -/** - * Get JSON representation of contract token family key. - * - * @param[in] key contract token family key - * @return JSON representation of @a key; NULL on error - */ -static json_t * -json_from_token_family_key ( - const struct TALER_MERCHANT_ContractTokenFamilyKey *key) -{ - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_timestamp ("signature_validity_start", - key->valid_after), - GNUNET_JSON_pack_timestamp ("signature_validity_end", - key->valid_before), - TALER_JSON_pack_token_pub (NULL, - &key->pub)); -} - - -/** - * Get JSON representation of contract token family details. - * - * @param[in] family contract token family - * @return JSON representation of @a family->details; NULL on error - */ -static json_t * -json_from_token_family_details ( - const struct TALER_MERCHANT_ContractTokenFamily *family) -{ - switch (family->kind) - { - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: - break; - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: - { - json_t *trusted_domains; - - trusted_domains = json_array (); - GNUNET_assert (NULL != trusted_domains); - for (unsigned int i = 0; - i < family->details.subscription.trusted_domains_len; - i++) - GNUNET_assert (0 == - json_array_append_new ( - trusted_domains, - json_string ( - family->details.subscription.trusted_domains[i]))); - - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("class", - "subscription"), - GNUNET_JSON_pack_array_steal ("trusted_domains", - trusted_domains)); - } - case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: - { - json_t *expected_domains; - - expected_domains = json_array (); - GNUNET_assert (NULL != expected_domains); - for (unsigned int i = 0; - i < family->details.discount.expected_domains_len; - i++) - GNUNET_assert (0 == - json_array_append_new ( - expected_domains, - json_string ( - family->details.discount.expected_domains[i]))); - - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("class", - "discount"), - GNUNET_JSON_pack_array_steal ("expected_domains", - expected_domains)); - } - } - - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "unsupported token family kind %d", - family->kind); - GNUNET_assert (0); - return NULL; -} - - -json_t * -TALER_MERCHANT_json_from_token_family ( - const struct TALER_MERCHANT_ContractTokenFamily *family) -{ - json_t *keys; - - keys = json_array (); - GNUNET_assert (NULL != keys); - for (unsigned int i = 0; i < family->keys_len; i++) - GNUNET_assert (0 == json_array_append_new ( - keys, - json_from_token_family_key ( - &family->keys[i]))); - - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - family->name), - GNUNET_JSON_pack_string ("description", - family->description), - GNUNET_JSON_pack_object_incref ("description_i18n", - family->description_i18n), - GNUNET_JSON_pack_array_steal ("keys", - keys), - GNUNET_JSON_pack_object_steal ("details", - json_from_token_family_details (family)), - GNUNET_JSON_pack_bool ("critical", - family->critical)); -} - /** * Get JSON object with contract terms v0-specific fields. @@ -341,7 +36,7 @@ TALER_MERCHANT_json_from_token_family ( */ static json_t * json_from_contract_v0 ( - const struct TALER_MERCHANT_Contract *input) + const struct TALER_MERCHANT_ProtoContract *input) { return GNUNET_JSON_PACK ( TALER_JSON_pack_amount ("amount", @@ -364,7 +59,7 @@ json_from_contract_v0 ( */ static json_t * json_from_contract_v1 ( - const struct TALER_MERCHANT_Contract *input) + const struct TALER_MERCHANT_ProtoContract *input) { json_t *choices; json_t *families; @@ -375,8 +70,7 @@ json_from_contract_v1 ( GNUNET_assert (0 == json_array_append_new ( choices, TALER_MERCHANT_json_from_contract_choice ( - &input->details.v1.choices[i], - false))); + &input->details.v1.choices[i]))); families = json_object (); GNUNET_assert (0 != families); @@ -396,206 +90,97 @@ json_from_contract_v1 ( /** - * Convert quantity @a q into a string for JSON serialization + * Serialize proto contract into JSON object. * - * @param q quantity to convert - * @return formatted string + * @param[in] pc proto contract to serialize + * @return JSON representation of @a pc; NULL on error */ -static const char * -quantity_to_string (const struct TALER_MERCHANT_ProductQuantity *q) -{ - static char res[64]; - - TALER_MERCHANT_vk_format_fractional_string (TALER_MERCHANT_VK_QUANTITY, - q->integer, - q->fractional, - sizeof (res), - res); - return res; -} - - -json_t * -TALER_MERCHANT_product_sold_serialize ( - const struct TALER_MERCHANT_ProductSold *p) -{ - json_t *prices; - - prices = json_array (); - GNUNET_assert (NULL != prices); - for (unsigned int i = 0; i<p->prices_length; i++) - GNUNET_assert (0 == - json_array_append_new (prices, - TALER_JSON_from_amount ( - &p->prices[i]))); - return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("product_id", - p->product_id)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("product_name", - p->product_name)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("description", - p->description)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("description_i18n", - (json_t *) p->description_i18n)), - GNUNET_JSON_pack_allow_null ( - ( (0 != p->unit_quantity.integer) || - (0 != p->unit_quantity.fractional) ) - ? GNUNET_JSON_pack_string ("unit_quantity", - quantity_to_string (&p->unit_quantity)) - : GNUNET_JSON_pack_string ("dummy", - NULL) ), - /* Legacy */ - GNUNET_JSON_pack_allow_null ( - (0 == p->unit_quantity.fractional) - ? GNUNET_JSON_pack_uint64 ("quantity", - p->unit_quantity.integer) - : GNUNET_JSON_pack_string ("dummy", - NULL) ), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("unit", - p->unit)), - /* Deprecated, use prices! */ - GNUNET_JSON_pack_allow_null ( - TALER_JSON_pack_amount ("price", - 0 < p->prices_length - ? &p->prices[0] - : NULL)), - GNUNET_JSON_pack_array_steal ("prices", - prices), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("image", - p->image)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_array_incref ("taxes", - (json_t *) p->taxes)), - GNUNET_JSON_pack_allow_null ( - GNUNET_TIME_absolute_is_never (p->delivery_date.abs_time) - ? GNUNET_JSON_pack_string ("dummy", - NULL) - : GNUNET_JSON_pack_timestamp ("delivery_date", - p->delivery_date)), - GNUNET_JSON_pack_uint64 ("product_money_pot", - p->product_money_pot)); -} - - json_t * -TALER_MERCHANT_contract_serialize ( - const struct TALER_MERCHANT_Contract *input, - bool nonce_optional) +TALER_MERCHANT_proto_contract_serialize ( + const struct TALER_MERCHANT_ProtoContract *pc) { + json_t *bj; json_t *details; json_t *products; - switch (input->version) + bj = TALER_MERCHANT_base_terms_serialize (pc->base); + switch (pc->base->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: - details = json_from_contract_v0 (input); + details = json_from_contract_v0 (pc); goto success; case TALER_MERCHANT_CONTRACT_VERSION_1: - details = json_from_contract_v1 (input); + details = json_from_contract_v1 (pc); goto success; } GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "unknown contract type version %d", - input->version); + pc->base->version); GNUNET_assert (0); return NULL; success: products = json_array (); GNUNET_assert (NULL != products); - for (size_t i = 0; i<input->products_len; i++) + for (size_t i = 0; i<pc->products_len; i++) { GNUNET_assert ( 0 == json_array_append_new (products, TALER_MERCHANT_product_sold_serialize ( - &input->products[i]))); + &pc->products[i]))); } return GNUNET_JSON_PACK ( - GNUNET_JSON_pack_uint64 ("version", - input->version), - GNUNET_JSON_pack_string ("summary", - input->summary), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("summary_i18n", - input->summary_i18n)), GNUNET_JSON_pack_string ("order_id", - input->order_id), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("public_reorder_url", - input->public_reorder_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_url", - input->fulfillment_url)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("fulfillment_message", - input->fulfillment_message)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("fulfillment_message_i18n", - input->fulfillment_message_i18n)), - GNUNET_JSON_pack_array_steal ("products", - products), + pc->order_id), + GNUNET_JSON_pack_object_steal (NULL, + bj), GNUNET_JSON_pack_timestamp ("timestamp", - input->timestamp), + pc->timestamp), GNUNET_JSON_pack_timestamp ("refund_deadline", - input->refund_deadline), + pc->refund_deadline), GNUNET_JSON_pack_timestamp ("pay_deadline", - input->pay_deadline), + pc->pay_deadline), GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", - input->wire_deadline), + pc->wire_deadline), GNUNET_JSON_pack_data_auto ("merchant_pub", - &input->merchant_pub.eddsa_pub), + &pc->merchant_pub.eddsa_pub), GNUNET_JSON_pack_string ("merchant_base_url", - input->merchant_base_url), - GNUNET_JSON_pack_object_steal ("merchant", - json_from_merchant_details (input)), + pc->merchant_base_url), + GNUNET_JSON_pack_object_steal ( + "merchant", + TALER_MERCHANT_metadata_to_json ( + &pc->merchant)), + GNUNET_JSON_pack_array_steal ("products", + products), GNUNET_JSON_pack_data_auto ("h_wire", - &input->h_wire), + &pc->h_wire), GNUNET_JSON_pack_string ("wire_method", - input->wire_method), + pc->wire_method), GNUNET_JSON_pack_array_steal ("exchanges", - input->exchanges), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("delivery_location", - input->delivery_location)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_timestamp ("delivery_date", - input->delivery_date)), - GNUNET_JSON_pack_allow_null ( - GNUNET_TIME_absolute_is_never (input->max_pickup_time.abs_time) - ? GNUNET_JSON_pack_string ("dummy", - NULL) - : GNUNET_JSON_pack_timestamp ("max_pickup_time", - input->max_pickup_time)), - (nonce_optional) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("nonce", - input->nonce)) - : GNUNET_JSON_pack_string ("nonce", - input->nonce), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_time_rel ("auto_refund", - input->auto_refund)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_steal ("extra", - input->extra)), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_uint64 ("minimum_age", - input->minimum_age)), - (0 == input->default_money_pot) - ? GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_string ("dummy", - NULL)) - : GNUNET_JSON_pack_uint64 ("default_money_pot", - input->default_money_pot), + pc->exchanges), GNUNET_JSON_pack_object_steal (NULL, details)); } + + +json_t * +TALER_MERCHANT_contract_serialize ( + const struct TALER_MERCHANT_Contract *input) +{ + json_t *pj; + + pj = TALER_MERCHANT_proto_contract_serialize (input->pc); + if (NULL == pj) + { + GNUNET_break (0); + return NULL; + } + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("nonce", + input->nonce), + GNUNET_JSON_pack_object_steal (NULL, + pj)); +} diff --git a/src/util/contract_version_parse.c b/src/util/contract_version_parse.c @@ -0,0 +1,84 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/contract_version_parse.c + * @brief parser for the contract version + * @author Christian Grothoff + */ +#include "platform.h" +#include <string.h> +#include "taler/taler_merchant_util.h" + +/** + * Parse contract version of given JSON contract terms. + * + * @param cls closure, unused parameter + * @param root the JSON object representing data + * @param[out] spec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_contract_version (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *spec) +{ + enum TALER_MERCHANT_ContractVersion *res + = (enum TALER_MERCHANT_ContractVersion *) spec->ptr; + + (void) cls; + if (json_is_integer (root)) + { + json_int_t version = json_integer_value (root); + + switch (version) + { + case 0: + *res = TALER_MERCHANT_CONTRACT_VERSION_0; + return GNUNET_OK; + case 1: + *res = TALER_MERCHANT_CONTRACT_VERSION_1; + return GNUNET_OK; + } + + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (json_is_null (root)) + { + *res = TALER_MERCHANT_CONTRACT_VERSION_0; + return GNUNET_OK; + } + + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_contract_version ( + const char *name, + enum TALER_MERCHANT_ContractVersion *version) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_contract_version, + .field = name, + .ptr = version + }; + + *version = TALER_MERCHANT_CONTRACT_VERSION_0; + return ret; +} diff --git a/src/util/merchant_parse.c b/src/util/merchant_parse.c @@ -0,0 +1,152 @@ +/* + This file is part of TALER + (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/merchant_parse.c + * @brief shared logic for merchant metadata parsing + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdbool.h> +#include <stdint.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_util.h> +#include "taler/taler_merchant_util.h" + + +/** + * Parse merchant details of given JSON order terms. + * + * @param cls closure, unused parameter + * @param root the JSON object representing data + * @param[out] ospec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_merchant_details (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *ospec) +{ + struct TALER_MERCHANT_MetaData *merchant = ospec->ptr; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string_copy ("name", + &merchant->name), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("email", + &merchant->email), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("website", + &merchant->website), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("logo", + &merchant->logo), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("address", + &merchant->address), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("jurisdiction", + &merchant->jurisdiction), + NULL), + GNUNET_JSON_spec_end () + }; + const char *error_name; + unsigned int error_line; + + (void) cls; + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &error_name, + &error_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[error_line].field, + error_line, + error_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; +} + + +json_t * +TALER_MERCHANT_metadata_to_json ( + const struct TALER_MERCHANT_MetaData *merchant) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + merchant->name), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("email", + merchant->email)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("website", + merchant->website)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("logo", + merchant->logo)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("address", + merchant->address)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ("jurisdiction", + merchant->jurisdiction))); +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_merchant_details ( + const char *name, + struct TALER_MERCHANT_MetaData *merchant) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_merchant_details, + .field = name, + .ptr = merchant, + }; + + return ret; +} + + +void +TALER_MERCHANT_metadata_free ( + struct TALER_MERCHANT_MetaData *merchant) +{ + GNUNET_free (merchant->name); + GNUNET_free (merchant->website); + GNUNET_free (merchant->email); + GNUNET_free (merchant->logo); + if (NULL != merchant->address) + { + json_decref (merchant->address); + merchant->address = NULL; + } + if (NULL != merchant->jurisdiction) + { + json_decref (merchant->jurisdiction); + merchant->jurisdiction = NULL; + } +} diff --git a/src/util/meson.build b/src/util/meson.build @@ -15,14 +15,28 @@ endforeach libtalermerchantutil_SOURCES = [ 'amount_quantity.c', + 'base_terms_parse.c', + 'base_terms_serialize.c', + 'contract_choice_parse.c', + 'contract_choice_serialize.c', 'contract_parse.c', 'contract_serialize.c', + 'contract_version_parse.c', 'json.c', + 'merchant_parse.c', 'mfa.c', + 'order_choice_parse.c', + 'order_choice_serialize.c', + 'order_parse.c', 'os_installation.c', + 'product_parse.c', + 'product_sold_serialize.c', 'template_parse.c', - 'value_kinds.c', + 'token_family_parse.c', + 'token_family_serialize.c', + 'util.c', 'validators.c', + 'value_kinds.c', ] libtalermerchantutil = library( diff --git a/src/util/order_choice_parse.c b/src/util/order_choice_parse.c @@ -0,0 +1,444 @@ +/* + This file is part of TALER + (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/order_choice_parse.c + * @brief shared logic for order choice parsing + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdbool.h> +#include <stdint.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_util.h> +#include "taler/taler_merchant_util.h" + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_order_choice_input ( + json_t *root, + struct TALER_MERCHANT_OrderInput *input, + size_t index) +{ + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + TALER_MERCHANT_json_spec_cit ("type", + &input->type), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + ispec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + { + struct GNUNET_JSON_Specification spec[] = { + 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 (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'type' invalid in input #%u\n", + (unsigned int) index); + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_order_choice_output ( + json_t *root, + struct TALER_MERCHANT_OrderOutput *output, + size_t index) +{ + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + TALER_MERCHANT_json_spec_cot ("type", + &output->type), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + ispec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("token_family_slug", + &output->details.token.token_family_slug), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint ("count", + &output->details.token.count), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("valid_at", + &output->details.token.valid_at), + NULL), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + return GNUNET_OK; + } + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + { + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("amount", + &output->details.donation_receipt.amount), + &output->details.donation_receipt.no_amount), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'type' invalid in output #%u\n", + (unsigned int) index); + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +/** + * Parse given JSON object to choices array. + * + * @param cls closure, pointer to array length + * @param root the json array representing the choices + * @param[out] ospec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_order_choices ( + void *cls, + json_t *root, + struct GNUNET_JSON_Specification *ospec) +{ + struct TALER_MERCHANT_OrderChoice **choices = ospec->ptr; + unsigned int *choices_len = cls; + + if (! json_is_array (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (0 == json_array_size (root)) + { + /* empty list of choices is not allowed */ + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + *choices = NULL; + *choices_len = 0; + GNUNET_array_grow (*choices, + *choices_len, + json_array_size (root)); + + for (unsigned int i = 0; i < *choices_len; i++) + { + struct TALER_MERCHANT_OrderChoice *choice = &(*choices)[i]; + const json_t *jinputs = NULL; + const json_t *joutputs = NULL; + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &choice->amount), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &choice->tip), + &choice->no_tip), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("description", + &choice->description), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("description_i18n", + &choice->description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_fee", + &choice->max_fee), + &choice->no_max_fee), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + NULL), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (root, i), + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (! choice->no_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->max_fee)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Fee currency does not match amount currency in choice #%u\n", + i); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (! choice->no_tip) && + (GNUNET_OK != + TALER_amount_cmp_currency (&choice->amount, + &choice->tip)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Tip currency does not match amount currency in choice #%u\n", + i); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_OrderInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_order_choice_input ((json_t *) jinput, + &input, + idx)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_break_op (0); + return GNUNET_SYSERR; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; + break; + } + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + } + } + + if (NULL != joutputs) + { + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_OrderOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_order_choice_output ((json_t *) joutput, + &output, + idx)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break_op (0); + return GNUNET_SYSERR; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore output tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + break; + } + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + } + } + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_order_choices ( + const char *name, + struct TALER_MERCHANT_OrderChoice **choices, + unsigned int *choices_len) +{ + struct GNUNET_JSON_Specification ret = { + .cls = (void *) choices_len, + .parser = &parse_order_choices, + .field = name, + .ptr = choices, + }; + + return ret; +} + + +void +TALER_MERCHANT_order_choice_free ( + struct TALER_MERCHANT_OrderChoice *choice) +{ + for (unsigned int i = 0; i < choice->outputs_len; i++) + { + struct TALER_MERCHANT_OrderOutput *output = &choice->outputs[i]; + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_break (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + break; +#if FUTURE + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN: + GNUNET_free (output->details.coin.exchange_url); + break; +#endif + } + } + GNUNET_free (choice->description); + if (NULL != choice->description_i18n) + { + json_decref (choice->description_i18n); + choice->description_i18n = NULL; + } + GNUNET_free (choice->inputs); + GNUNET_free (choice->outputs); +} diff --git a/src/util/order_choice_serialize.c b/src/util/order_choice_serialize.c @@ -0,0 +1,153 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/order_choice_serialize.c + * @brief shared logic for order choice serialization + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + +/** + * Get JSON representation of order choice input. + * + * @param[in] input order terms choice input + * @return JSON representation of @a input; NULL on error + */ +static json_t * +json_from_order_input ( + const struct TALER_MERCHANT_OrderInput *input) +{ + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "invalid order input type"); + GNUNET_assert (0); + return NULL; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "token"), + GNUNET_JSON_pack_string ("token_family_slug", + input->details.token.token_family_slug), + GNUNET_JSON_pack_int64 ("count", + input->details.token.count)); + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "unsupported order input type %d", + input->type); + GNUNET_assert (0); + return NULL; +} + + +/** + * Get JSON representation of order choice output. + * + * @param[in] output order terms choice output + * @return JSON representation of @a output; NULL on error + */ +static json_t * +json_from_order_output ( + const struct TALER_MERCHANT_OrderOutput *output) +{ + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "invalid order output type"); + GNUNET_assert (0); + return NULL; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "token"), + GNUNET_JSON_pack_string ("token_family_slug", + output->details.token.token_family_slug), + GNUNET_JSON_pack_uint64 ("count", + output->details.token.count)); + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "tax-receipt"), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("amount", + output->details.donation_receipt.no_amount + ? NULL + : &output->details.donation_receipt.amount))); + } + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unsupported order output type %d", + output->type); + GNUNET_assert (0); + return NULL; +} + + +json_t * +TALER_MERCHANT_json_from_order_choice ( + const struct TALER_MERCHANT_OrderChoice *choice) +{ + json_t *inputs; + json_t *outputs; + + inputs = json_array (); + GNUNET_assert (NULL != inputs); + for (unsigned int i = 0; i < choice->inputs_len; i++) + GNUNET_assert (0 == + json_array_append_new (inputs, + json_from_order_input ( + &choice->inputs[i]))); + outputs = json_array (); + GNUNET_assert (NULL != outputs); + for (unsigned int i = 0; i < choice->outputs_len; i++) + GNUNET_assert (0 == + json_array_append_new (outputs, + json_from_order_output ( + &choice->outputs[i]))); + + return GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &choice->amount), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("tip", + choice->no_tip + ? NULL + : &choice->tip)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("description", + choice->description)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + choice->description_i18n)), + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("max_fee", + choice->no_max_fee + ? NULL + : &choice->max_fee)), + GNUNET_JSON_pack_array_steal ("inputs", + inputs), + GNUNET_JSON_pack_array_steal ("outputs", + outputs)); +} diff --git a/src/util/order_parse.c b/src/util/order_parse.c @@ -0,0 +1,290 @@ +/* + This file is part of TALER + (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/order_parse.c + * @brief shared logic for order parsing + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include <jansson.h> +#include <stdbool.h> +#include <stdint.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_util.h> +#include "taler/taler_merchant_util.h" + + +/** + * Parse v0-specific fields of @a input JSON into @a order. + * + * @param[in] input the JSON order terms + * @param[out] order where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_order_v0 ( + json_t *input, + struct TALER_MERCHANT_Order *order) +{ + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount_any ("amount", + &order->details.v0.brutto), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("tip", + &order->details.v0.tip), + &order->details.v0.no_tip), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("max_fee", + &order->details.v0.max_fee), + &order->details.v0.no_max_fee), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse order v0 at field %s\n", + ename); + return GNUNET_SYSERR; + } + + if ( (! order->details.v0.no_max_fee) && + (GNUNET_OK != + TALER_amount_cmp_currency (&order->details.v0.max_fee, + &order->details.v0.brutto)) ) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "'max_fee' in database does not match currency of order price"); + return GNUNET_SYSERR; + } + if ( (! order->details.v0.no_tip) && + (GNUNET_OK != + TALER_amount_cmp_currency (&order->details.v0.tip, + &order->details.v0.brutto)) ) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "'tip' in database does not match currency of order price"); + return GNUNET_SYSERR; + } + + return res; +} + + +/** + * Parse v1-specific fields of @a input JSON into @a order. + * + * @param[in] input the JSON order terms + * @param[out] order where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_order_v1 ( + json_t *input, + struct TALER_MERCHANT_Order *order) +{ + struct GNUNET_JSON_Specification espec[] = { + TALER_MERCHANT_spec_order_choices ( + "choices", + &order->details.v1.choices, + &order->details.v1.choices_len), + GNUNET_JSON_spec_end () + }; + + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse order v1 at field %s\n", + ename); + return GNUNET_SYSERR; + } + + return res; +} + + +struct TALER_MERCHANT_Order * +TALER_MERCHANT_order_parse (json_t *input) +{ + struct TALER_MERCHANT_Order *order + = GNUNET_new (struct TALER_MERCHANT_Order); + enum GNUNET_GenericReturnValue res + = GNUNET_SYSERR; + + GNUNET_assert (NULL != input); + order->base = TALER_MERCHANT_base_terms_parse (input); + if (NULL == order->base) + { + GNUNET_break (0); + GNUNET_free (order); + return NULL; + } + order->refund_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + order->wire_transfer_deadline = GNUNET_TIME_UNIT_FOREVER_TS; + { + const json_t *products; + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("products", + &products), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("order_id", + &order->order_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("timestamp", + &order->timestamp), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("refund_deadline", + &order->refund_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("pay_deadline", + &order->pay_deadline), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("wire_transfer_deadline", + &order->wire_transfer_deadline), + NULL), + GNUNET_JSON_spec_end () + }; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse proto contract at field %s\n", + ename); + goto cleanup; + } + if (NULL != products) + { + order->products_len = json_array_size (products); + if (0 != order->products_len) + { + size_t i; + json_t *p; + + order->products = GNUNET_new_array ( + order->products_len, + struct TALER_MERCHANT_ProductSold); + json_array_foreach (products, i, p) + { + if (GNUNET_OK != + TALER_MERCHANT_parse_product_sold (p, + &order->products[i], + true)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse product at offset %u\n", + (unsigned int) i); + goto cleanup; + } + } + } + } + } + switch (order->base->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + res = parse_order_v0 (input, + order); + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + res = parse_order_v1 (input, + order); + break; + } + + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse order\n"); + goto cleanup; + } + return order; + +cleanup: + TALER_MERCHANT_order_free (order); + return NULL; +} + + +void +TALER_MERCHANT_order_free ( + struct TALER_MERCHANT_Order *order) +{ + if (NULL == order) + return; + switch (order->base->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + for (unsigned int i = 0; + i < order->details.v1.choices_len; + i++) + TALER_MERCHANT_order_choice_free (&order->details.v1.choices[i]); + GNUNET_free (order->details.v1.choices); + break; + } + if (NULL != order->products) + { + for (size_t i = 0; i<order->products_len; i++) + TALER_MERCHANT_product_sold_free (&order->products[i]); + GNUNET_free (order->products); + order->products_len = 0; + } + GNUNET_free (order->order_id); + if (NULL != order->base) + { + TALER_MERCHANT_base_terms_free (order->base); + order->base = NULL; + } + GNUNET_free (order); +} diff --git a/src/util/product_parse.c b/src/util/product_parse.c @@ -0,0 +1,285 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/product_parse.c + * @brief small helpers + * @author Christian Grothoff + */ +#include "platform.h" +#include <string.h> +#include "taler/taler_merchant_util.h" +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> + +/** + * Parse the given unit quantity string @a s and store the result in @a q. + * + * @param s quantity to parse + * @param[out] q where to store the result + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +parse_unit_quantity (const char *s, + struct TALER_MERCHANT_ProductQuantity *q) +{ + const char *ptr; + uint64_t integer = 0; + uint32_t frac = 0; + unsigned int digits = 0; + + if (NULL == s) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + ptr = s; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ('-' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (! isdigit ((unsigned char) *ptr)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + /* We intentionally allow at most INT64_MAX (as -1 has special meanings), + even though the data type would support UINT64_MAX */ + if (integer > (INT64_MAX - digit) / 10) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + integer = integer * 10 + digit; + ptr++; + } + if ('.' == *ptr) + { + ptr++; + if ('\0' == *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + while (isdigit ((unsigned char) *ptr)) + { + unsigned int digit = (unsigned int) (*ptr - '0'); + + if (digits >= TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + frac = (uint32_t) (frac * 10 + digit); + digits++; + ptr++; + } + while (digits < TALER_MERCHANT_UNIT_FRAC_MAX_DIGITS) + { + frac *= 10; + digits++; + } + } + if ('\0' != *ptr) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + q->integer = integer; + q->fractional = frac; + return GNUNET_OK; +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_product_sold (const json_t *pj, + struct TALER_MERCHANT_ProductSold *r, + bool allow_mip) +{ + // FIXME: properly implement distinction between MIP and non-MIP! + bool no_quantity; + bool no_unit_quantity; + bool no_price; + uint64_t legacy_quantity; + const char *unit_quantity_s; + struct TALER_Amount price; + const json_t *prices = NULL; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("product_id", + &r->product_id), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("product_name", + &r->product_name), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("description", + &r->description), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_object_copy ("description_i18n", + &r->description_i18n), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("quantity", + &legacy_quantity), + &no_quantity), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string ("unit_quantity", + &unit_quantity_s), + &no_unit_quantity), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("unit", + &r->unit), + NULL), + GNUNET_JSON_spec_mark_optional ( + TALER_JSON_spec_amount_any ("price", + &price), + &no_price), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_const ("prices", + &prices), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_string_copy ("image", + &r->image), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_array_copy ("taxes", + &r->taxes), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_timestamp ("delivery_date", + &r->delivery_date), + NULL), + GNUNET_JSON_spec_mark_optional ( + GNUNET_JSON_spec_uint64 ("product_money_pot", + &r->product_money_pot), + NULL), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + r->delivery_date = GNUNET_TIME_UNIT_FOREVER_TS; + res = GNUNET_JSON_parse (pj, + spec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse product at field %s\n", + ename); + return GNUNET_SYSERR; + } + if (! no_quantity) + { + r->unit_quantity.integer = legacy_quantity; + r->unit_quantity.fractional = 0; + } + if (! no_unit_quantity) + { + if (GNUNET_OK != + parse_unit_quantity (unit_quantity_s, + &r->unit_quantity)) + { + GNUNET_break (0); + return GNUNET_SYSERR; + } + } + if ( (! no_quantity) && (! no_unit_quantity) ) + { + GNUNET_break ( (0 == r->unit_quantity.fractional) && + (legacy_quantity == r->unit_quantity.integer) ); + } + if ( (NULL != r->image) && + (! TALER_MERCHANT_image_data_url_valid (r->image)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if ( (NULL != r->taxes) && + (! TALER_MERCHANT_taxes_array_valid (r->taxes)) ) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + if (NULL != prices) + { + size_t len = json_array_size (prices); + size_t i; + json_t *price_i; + + GNUNET_assert (len < UINT_MAX); + r->prices = GNUNET_new_array ((unsigned int) len, + struct TALER_Amount); + json_array_foreach (prices, i, price_i) + { + struct GNUNET_JSON_Specification pspec[] = { + TALER_JSON_spec_amount_any (NULL, + &r->prices[i]), + GNUNET_JSON_spec_end () + }; + + res = GNUNET_JSON_parse (price_i, + pspec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse price at index %u\n", + (unsigned int) i); + return GNUNET_SYSERR; + } + } + } + else if (! no_price) + { + r->prices = GNUNET_new_array (1, + struct TALER_Amount); + r->prices[0] = price; + } + return GNUNET_OK; +} + + +void +TALER_MERCHANT_product_sold_free (struct TALER_MERCHANT_ProductSold *product) +{ + GNUNET_free (product->product_id); + GNUNET_free (product->product_name); + GNUNET_free (product->description); + json_decref (product->description_i18n); + GNUNET_free (product->prices); + GNUNET_free (product->unit); + GNUNET_free (product->image); + json_decref (product->taxes); +} diff --git a/src/util/product_sold_serialize.c b/src/util/product_sold_serialize.c @@ -0,0 +1,116 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/product_sold_serialize.c + * @brief shared logic for product sold serialization + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + + +/** + * Convert quantity @a q into a string for JSON serialization + * + * @param q quantity to convert + * @return formatted string + */ +static const char * +quantity_to_string (const struct TALER_MERCHANT_ProductQuantity *q) +{ + static char res[64]; + + TALER_MERCHANT_vk_format_fractional_string (TALER_MERCHANT_VK_QUANTITY, + q->integer, + q->fractional, + sizeof (res), + res); + return res; +} + + +json_t * +TALER_MERCHANT_product_sold_serialize ( + const struct TALER_MERCHANT_ProductSold *p) +{ + json_t *prices; + + prices = json_array (); + GNUNET_assert (NULL != prices); + for (unsigned int i = 0; i<p->prices_length; i++) + GNUNET_assert (0 == + json_array_append_new (prices, + TALER_JSON_from_amount ( + &p->prices[i]))); + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("product_id", + p->product_id)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("product_name", + p->product_name)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("description", + p->description)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_incref ("description_i18n", + (json_t *) p->description_i18n)), + GNUNET_JSON_pack_allow_null ( + ( (0 != p->unit_quantity.integer) || + (0 != p->unit_quantity.fractional) ) + ? GNUNET_JSON_pack_string ("unit_quantity", + quantity_to_string (&p->unit_quantity)) + : GNUNET_JSON_pack_string ("dummy", + NULL) ), + /* Legacy */ + GNUNET_JSON_pack_allow_null ( + (0 == p->unit_quantity.fractional) + ? GNUNET_JSON_pack_uint64 ("quantity", + p->unit_quantity.integer) + : GNUNET_JSON_pack_string ("dummy", + NULL) ), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("unit", + p->unit)), + /* Deprecated, use prices! */ + GNUNET_JSON_pack_allow_null ( + TALER_JSON_pack_amount ("price", + 0 < p->prices_length + ? &p->prices[0] + : NULL)), + GNUNET_JSON_pack_array_steal ("prices", + prices), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_string ("image", + p->image)), + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_array_incref ("taxes", + (json_t *) p->taxes)), + GNUNET_JSON_pack_allow_null ( + GNUNET_TIME_absolute_is_never (p->delivery_date.abs_time) + ? GNUNET_JSON_pack_string ("dummy", + NULL) + : GNUNET_JSON_pack_timestamp ("delivery_date", + p->delivery_date)), + GNUNET_JSON_pack_uint64 ("product_money_pot", + p->product_money_pot)); +} diff --git a/src/util/template_parse.c b/src/util/template_parse.c @@ -100,21 +100,25 @@ parse_template_inventory (const json_t *template_contract, { struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("selected_all", - &out->details.inventory.selected_all), + GNUNET_JSON_spec_bool ( + "selected_all", + &out->details.inventory.selected_all), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("selected_categories", - &out->details.inventory.selected_categories) + GNUNET_JSON_spec_array_const ( + "selected_categories", + &out->details.inventory.selected_categories) , NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_array_const ("selected_products", - &out->details.inventory.selected_products), + GNUNET_JSON_spec_array_const ( + "selected_products", + &out->details.inventory.selected_products), NULL), GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_bool ("choose_one", - &out->details.inventory.choose_one), + GNUNET_JSON_spec_bool ( + "choose_one", + &out->details.inventory.choose_one), NULL), GNUNET_JSON_spec_end () }; @@ -194,9 +198,9 @@ parse_template_paivana (const json_t *template_contract, GNUNET_JSON_spec_string ("website_regex", &out->details.paivana.website_regex), NULL), - TALER_MERCHANT_spec_choices ("choices", - &out->details.paivana.choices, - &out->details.paivana.choices_len), + TALER_MERCHANT_spec_order_choices ("choices", + &out->details.paivana.choices, + &out->details.paivana.choices_len), GNUNET_JSON_spec_end () }; const char *en; @@ -348,7 +352,7 @@ TALER_MERCHANT_template_contract_free ( return; case TALER_MERCHANT_TEMPLATE_TYPE_PAIVANA: for (unsigned int i = 0; i<tc->details.paivana.choices_len; i++) - TALER_MERCHANT_contract_choice_free (&tc->details.paivana.choices[i]); + TALER_MERCHANT_order_choice_free (&tc->details.paivana.choices[i]); GNUNET_array_grow (tc->details.paivana.choices, tc->details.paivana.choices_len, 0); diff --git a/src/util/test_contract.c b/src/util/test_contract.c @@ -61,7 +61,7 @@ const char *contract_common = "{\n" " \"exchanges\": [],\n" " \"delivery_location\": {},\n" // TODO " \"delivery_date\": {\"t_s\": 1736174497},\n" - " \"nonce\": \"\",\n" // TODO + " \"nonce\": \"test-nonce\",\n" " \"auto_refund\": {\"d_us\": \"forever\"},\n" " \"extra\": {\n" " \"key0\": \"value0\",\n" @@ -181,12 +181,11 @@ main (int argc, GNUNET_assert (0 == json_object_update_new (v0, common)); - v0_parsed = TALER_MERCHANT_contract_parse (v0, false); + v0_parsed = TALER_MERCHANT_contract_parse (v0); GNUNET_assert (NULL != v0_parsed); - v0_serialized = TALER_MERCHANT_contract_serialize (v0_parsed, false); + v0_serialized = TALER_MERCHANT_contract_serialize (v0_parsed); GNUNET_assert (NULL != v0_serialized); - GNUNET_assert (1 == json_equal (v0, v0_serialized)); json_decref (v0_serialized); @@ -208,20 +207,20 @@ main (int argc, GNUNET_assert (0 == json_object_update_new (v1, common)); - v1_parsed = TALER_MERCHANT_contract_parse (v1, false); + v1_parsed = TALER_MERCHANT_contract_parse (v1); GNUNET_assert (NULL != v1_parsed); - v1_serialized = TALER_MERCHANT_contract_serialize (v1_parsed, false); + v1_serialized = TALER_MERCHANT_contract_serialize (v1_parsed); GNUNET_assert (NULL != v1_serialized); GNUNET_assert (1 == json_equal (v1, v1_serialized)); for (unsigned int i = 0; - i < v1_parsed->details.v1.token_authorities_len; + i < v1_parsed->pc->details.v1.token_authorities_len; i++) { struct TALER_MERCHANT_ContractTokenFamily *in = - &v1_parsed->details.v1.token_authorities[i]; + &v1_parsed->pc->details.v1.token_authorities[i]; for (unsigned int j = 0; j < in->keys_len; j++) { struct TALER_MERCHANT_ContractTokenFamilyKey *inkey = &in->keys[j]; @@ -230,8 +229,8 @@ main (int argc, GNUNET_assert (GNUNET_OK == TALER_MERCHANT_find_token_family_key ( in->slug, inkey->valid_after, - v1_parsed->details.v1.token_authorities, - v1_parsed->details.v1.token_authorities_len, + v1_parsed->pc->details.v1.token_authorities, + v1_parsed->pc->details.v1.token_authorities_len, &out, &outkey)); GNUNET_assert (0 == GNUNET_memcmp (in, &out)); diff --git a/src/util/token_family_parse.c b/src/util/token_family_parse.c @@ -0,0 +1,399 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/token_family_parse.c + * @brief parser for data about token families + * @author Christian Grothoff + */ +#include "platform.h" +#include <string.h> +#include "taler/taler_merchant_util.h" +#include <taler/taler_json_lib.h> +#include <gnunet/gnunet_json_lib.h> + + +void +TALER_MERCHANT_contract_token_family_free ( + struct TALER_MERCHANT_ContractTokenFamily *family) +{ + GNUNET_free (family->slug); + GNUNET_free (family->name); + GNUNET_free (family->description); + if (NULL != family->description_i18n) + { + json_decref (family->description_i18n); + family->description_i18n = NULL; + } + for (unsigned int i = 0; i < family->keys_len; i++) + TALER_token_issue_pub_free (&family->keys[i].pub); + GNUNET_free (family->keys); + + switch (family->kind) + { + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: + for (unsigned int i = 0; i < family->details.discount.expected_domains_len; + i++) + GNUNET_free (family->details.discount.expected_domains[i]); + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: + for (unsigned int i = 0; i < family->details.subscription. + trusted_domains_len; i++) + GNUNET_free (family->details.subscription.trusted_domains[i]); + break; + } +} + + +enum GNUNET_GenericReturnValue +TALER_MERCHANT_find_token_family_key ( + const char *slug, + struct GNUNET_TIME_Timestamp valid_after, + const struct TALER_MERCHANT_ContractTokenFamily *families, + unsigned int families_len, + struct TALER_MERCHANT_ContractTokenFamily *family, + struct TALER_MERCHANT_ContractTokenFamilyKey *key) +{ + for (unsigned int i = 0; i < families_len; i++) + { + const struct TALER_MERCHANT_ContractTokenFamily *fami + = &families[i]; + + if (0 != strcmp (fami->slug, + slug)) + continue; + if (NULL != family) + *family = *fami; + for (unsigned int k = 0; k < fami->keys_len; k++) + { + struct TALER_MERCHANT_ContractTokenFamilyKey *ki = &fami->keys[k]; + + if (GNUNET_TIME_timestamp_cmp (ki->valid_after, + ==, + valid_after)) + { + if (NULL != key) + *key = *ki; + return GNUNET_OK; + } + } + /* matching family found, but no key. */ + return GNUNET_NO; + } + + /* no matching family found */ + return GNUNET_SYSERR; +} + + +/** + * Parse token details of the given JSON contract token family. + * + * @param cls closure, unused parameter + * @param root the JSON object representing data + * @param[out] ospec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_token_details (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *ospec) +{ + struct TALER_MERCHANT_ContractTokenFamily *family = ospec->ptr; + const char *class; + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("class", + &class), + GNUNET_JSON_spec_end () + }; + + (void) cls; + if (GNUNET_OK != + GNUNET_JSON_parse (root, + ispec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse %s at %u: %s\n", + ispec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + family->kind = TALER_MERCHANT_contract_token_kind_from_string (class); + + switch (family->kind) + { + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: + { + const json_t *trusted_domains; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("trusted_domains", + &trusted_domains), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (family->details.subscription.trusted_domains, + family->details.subscription.trusted_domains_len, + json_array_size (trusted_domains)); + + for (unsigned int i = 0; + i < family->details.subscription.trusted_domains_len; + i++) + { + const json_t *jdomain; + jdomain = json_array_get (trusted_domains, i); + + if (! json_is_string (jdomain)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + family->details.subscription.trusted_domains[i] = + GNUNET_strdup (json_string_value (jdomain)); + } + + return GNUNET_OK; + } + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: + { + const json_t *expected_domains; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_array_const ("expected_domains", + &expected_domains), + GNUNET_JSON_spec_end () + }; + + if (GNUNET_OK != + GNUNET_JSON_parse (root, + spec, + &ename, + &eline)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse %s at %u: %s\n", + spec[eline].field, + eline, + ename); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (family->details.discount.expected_domains, + family->details.discount.expected_domains_len, + json_array_size (expected_domains)); + + for (unsigned int i = 0; + i < family->details.discount.expected_domains_len; + i++) + { + const json_t *jdomain; + + jdomain = json_array_get (expected_domains, + i); + if (! json_is_string (jdomain)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + family->details.discount.expected_domains[i] = + GNUNET_strdup (json_string_value (jdomain)); + } + + return GNUNET_OK; + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Field 'type' invalid in token family\n"); + GNUNET_break_op (0); + return GNUNET_SYSERR; +} + + +/** + * Provide specification to parse given JSON object to contract token details + * in the contract token family. All fields from @a family are copied. + * + * @param name name of the token details field in the JSON + * @param[out] family token_family where the token details have to be written + */ +static struct GNUNET_JSON_Specification +spec_token_details (const char *name, + struct TALER_MERCHANT_ContractTokenFamily *family) +{ + struct GNUNET_JSON_Specification ret = { + .parser = &parse_token_details, + .field = name, + .ptr = family, + }; + + return ret; +} + + +/** + * Parse given JSON object to token families array. + * + * @param cls closure, pointer to array length + * @param root the json object representing the token families. The keys are + * the token family slugs. + * @param[out] ospec where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +static enum GNUNET_GenericReturnValue +parse_token_families (void *cls, + json_t *root, + struct GNUNET_JSON_Specification *ospec) +{ + struct TALER_MERCHANT_ContractTokenFamily **families = ospec->ptr; + unsigned int *families_len = cls; + json_t *jfamily; + const char *slug; + + if (! json_is_object (root)) + { + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + json_object_foreach (root, slug, jfamily) + { + const json_t *keys; + struct TALER_MERCHANT_ContractTokenFamily family = { + .slug = GNUNET_strdup (slug) + }; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string_copy ("name", + &family.name), + GNUNET_JSON_spec_string_copy ("description", + &family.description), + GNUNET_JSON_spec_object_copy ("description_i18n", + &family.description_i18n), + GNUNET_JSON_spec_array_const ("keys", + &keys), + spec_token_details ("details", + &family), + GNUNET_JSON_spec_bool ("critical", + &family.critical), + GNUNET_JSON_spec_end () + }; + const char *error_name; + unsigned int error_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (jfamily, + spec, + &error_name, + &error_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + spec[error_line].field, + error_line, + error_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + + GNUNET_array_grow (family.keys, + family.keys_len, + json_array_size (keys)); + + for (unsigned int i = 0; i<family.keys_len; i++) + { + struct TALER_MERCHANT_ContractTokenFamilyKey *key = &family.keys[i]; + struct GNUNET_JSON_Specification key_spec[] = { + TALER_JSON_spec_token_pub ( + NULL, + &key->pub), + GNUNET_JSON_spec_timestamp ( + "signature_validity_start", + &key->valid_after), + GNUNET_JSON_spec_timestamp ( + "signature_validity_end", + &key->valid_before), + GNUNET_JSON_spec_end () + }; + const char *ierror_name; + unsigned int ierror_line; + + if (GNUNET_OK != + GNUNET_JSON_parse (json_array_get (keys, + i), + key_spec, + &ierror_name, + &ierror_line)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to parse %s at %u: %s\n", + key_spec[ierror_line].field, + ierror_line, + ierror_name); + GNUNET_break_op (0); + return GNUNET_SYSERR; + } + } + + GNUNET_array_append (*families, + *families_len, + family); + } + + return GNUNET_OK; +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_spec_token_families ( + const char *name, + struct TALER_MERCHANT_ContractTokenFamily **families, + unsigned int *families_len) +{ + struct GNUNET_JSON_Specification ret = { + .cls = (void *) families_len, + .parser = &parse_token_families, + .field = name, + .ptr = families, + }; + + return ret; +} diff --git a/src/util/token_family_serialize.c b/src/util/token_family_serialize.c @@ -0,0 +1,144 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024, 2025 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/token_family_serialize.c + * @brief shared logic for token family serialization + * @author Iván Ávalos + * @author Christian Grothoff + */ +#include "platform.h" +#include <gnunet/gnunet_json_lib.h> +#include <gnunet/gnunet_common.h> +#include <taler/taler_json_lib.h> +#include <jansson.h> +#include "taler/taler_util.h" +#include "taler/taler_merchant_util.h" + + +/** + * Get JSON representation of contract token family key. + * + * @param[in] key contract token family key + * @return JSON representation of @a key; NULL on error + */ +static json_t * +json_from_token_family_key ( + const struct TALER_MERCHANT_ContractTokenFamilyKey *key) +{ + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_timestamp ("signature_validity_start", + key->valid_after), + GNUNET_JSON_pack_timestamp ("signature_validity_end", + key->valid_before), + TALER_JSON_pack_token_pub (NULL, + &key->pub)); +} + + +/** + * Get JSON representation of contract token family details. + * + * @param[in] family contract token family + * @return JSON representation of @a family->details; NULL on error + */ +static json_t * +json_from_token_family_details ( + const struct TALER_MERCHANT_ContractTokenFamily *family) +{ + switch (family->kind) + { + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID: + break; + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION: + { + json_t *trusted_domains; + + trusted_domains = json_array (); + GNUNET_assert (NULL != trusted_domains); + for (unsigned int i = 0; + i < family->details.subscription.trusted_domains_len; + i++) + GNUNET_assert (0 == + json_array_append_new ( + trusted_domains, + json_string ( + family->details.subscription.trusted_domains[i]))); + + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("class", + "subscription"), + GNUNET_JSON_pack_array_steal ("trusted_domains", + trusted_domains)); + } + case TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT: + { + json_t *expected_domains; + + expected_domains = json_array (); + GNUNET_assert (NULL != expected_domains); + for (unsigned int i = 0; + i < family->details.discount.expected_domains_len; + i++) + GNUNET_assert (0 == + json_array_append_new ( + expected_domains, + json_string ( + family->details.discount.expected_domains[i]))); + + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("class", + "discount"), + GNUNET_JSON_pack_array_steal ("expected_domains", + expected_domains)); + } + } + + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "unsupported token family kind %d", + family->kind); + GNUNET_assert (0); + return NULL; +} + + +json_t * +TALER_MERCHANT_json_from_token_family ( + const struct TALER_MERCHANT_ContractTokenFamily *family) +{ + json_t *keys; + + keys = json_array (); + GNUNET_assert (NULL != keys); + for (unsigned int i = 0; i < family->keys_len; i++) + GNUNET_assert (0 == json_array_append_new ( + keys, + json_from_token_family_key ( + &family->keys[i]))); + + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("name", + family->name), + GNUNET_JSON_pack_string ("description", + family->description), + GNUNET_JSON_pack_object_incref ("description_i18n", + family->description_i18n), + GNUNET_JSON_pack_array_steal ("keys", + keys), + GNUNET_JSON_pack_object_steal ("details", + json_from_token_family_details (family)), + GNUNET_JSON_pack_bool ("critical", + family->critical)); +} diff --git a/src/util/util.c b/src/util/util.c @@ -0,0 +1,75 @@ +/* + This file is part of TALER + (C) 2026 Taler Systems SA + + TALER is free software; you can redistribute it and/or modify it under the + terms of the GNU Lesser General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + TALER is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** + * @file src/util/util.c + * @brief small helpers + * @author Christian Grothoff + */ +#include "platform.h" +#include <string.h> +#include "taler/taler_merchant_util.h" +#include <taler/taler_json_lib.h> +#include <gnunet/gnunet_json_lib.h> + + +bool +TALER_MERCHANT_taxes_array_valid (const json_t *taxes) +{ + json_t *tax; + size_t idx; + + if (! json_is_array (taxes)) + return false; + json_array_foreach (taxes, idx, tax) + { + struct TALER_Amount amount; + const char *name; + struct GNUNET_JSON_Specification spec[] = { + GNUNET_JSON_spec_string ("name", + &name), + TALER_JSON_spec_amount_any ("tax", + &amount), + GNUNET_JSON_spec_end () + }; + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + res = GNUNET_JSON_parse (tax, + spec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break_op (0); + return false; + } + } + return true; +} + + +enum TALER_MERCHANT_ContractTokenKind +TALER_MERCHANT_contract_token_kind_from_string (const char *str) +{ + if (0 == strcmp ("subscription", + str)) + return TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION; + if (0 == strcmp ("discount", + str)) + return TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT; + return TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID; +}