merchant

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

commit 5ab70d21b9f6ee7e2a5f1bf5fc977b785b46c8bc
parent 3916ca1e80fb4717ba337a867246c28ff89b74c2
Author: bohdan-potuzhnyi <bohdan.potuzhnyi@gmail.com>
Date:   Sat,  1 Feb 2025 13:54:43 +0100

Merge branch 'master' into dev/bohdan-potuzhnyi/donau-integration

Diffstat:
M.gitmodules | 4++--
Mconfigure.ac | 6+++---
Mdebian/changelog | 18++++++++++++++++++
Mdebian/rules | 3++-
Mdoc/doxygen/taler.doxy | 2+-
Msrc/backend/taler-merchant-httpd.c | 7+------
Msrc/backend/taler-merchant-httpd_config.c | 2+-
Msrc/backend/taler-merchant-httpd_contract.c | 8+++++++-
Msrc/backend/taler-merchant-httpd_exchanges.c | 29+++++------------------------
Msrc/backend/taler-merchant-httpd_get-orders-ID.c | 216+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Msrc/backend/taler-merchant-httpd_mhd.c | 44+++++++++++---------------------------------
Msrc/backend/taler-merchant-httpd_post-orders-ID-abort.c | 51+++++++++++++++++++++++++++++++++++++--------------
Msrc/backend/taler-merchant-httpd_post-orders-ID-pay.c | 369++++++++++++++++++++-----------------------------------------------------------
Msrc/backend/taler-merchant-httpd_post-orders-ID-refund.c | 35+++++++++++++++++++----------------
Msrc/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c | 2+-
Msrc/backend/taler-merchant-httpd_private-get-orders-ID.c | 15+++++++++------
Msrc/backend/taler-merchant-httpd_private-post-orders.c | 414++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/backend/taler-merchant-httpd_statics.c | 15+++++++++------
Msrc/backend/taler-merchant-kyccheck.c | 26+++++---------------------
Msrc/backend/taler-merchant-reconciliation.c | 4++--
Msrc/backenddb/Makefile.am | 210++++++++++++++++++++++++++++++++++++++++----------------------------------------
Msrc/backenddb/merchantdb_helper.c | 13-------------
Msrc/backenddb/pg_account_kyc_get_status.c | 1+
Msrc/backenddb/pg_insert_issued_token.c | 16++++++++--------
Msrc/backenddb/pg_insert_spent_token.c | 4++--
Msrc/backenddb/pg_lookup_contract_terms3.c | 4++--
Msrc/backenddb/pg_select_category.c | 13+++++++------
Msrc/backenddb/pg_select_category_by_name.c | 15+++++++--------
Msrc/backenddb/pg_select_exchange_keys.c | 3++-
Msrc/include/taler_merchant_service.h | 1+
Msrc/include/taler_merchant_util.h | 172++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/include/taler_merchantdb_plugin.h | 32--------------------------------
Msrc/lib/merchant_api_get_config.c | 4++--
Msrc/lib/merchant_api_post_order_abort.c | 4+++-
Msrc/merchant-tools/benchmark-cs.conf | 2+-
Msrc/merchant-tools/benchmark-rsa.conf | 2+-
Msrc/testing/test_key_rotation.conf | 4++--
Msrc/testing/test_merchant_order_creation.sh | 65++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/testing/testing_api_cmd_pay_order.c | 11++++++-----
Msrc/testing/testing_api_cmd_post_orders.c | 4++--
Msrc/util/Makefile.am | 20++++++++++++++++++++
Asrc/util/contract_parse.c | 1313+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/contract_serialize.c | 474+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/currencies.conf | 89+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/util/test_contract.c | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
45 files changed, 3046 insertions(+), 943 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -1,8 +1,8 @@ [submodule "doc/prebuilt"] path = doc/prebuilt - url = git://git.taler.net/docs.git + url = ../taler-docs.git branch = prebuilt [submodule "contrib/wallet-core"] path = contrib/wallet-core - url = git://git.taler.net/wallet-core + url = ../taler-typescript-core.git branch = prebuilt diff --git a/configure.ac b/configure.ac @@ -18,7 +18,7 @@ # This configure file is in the public domain AC_PREREQ([2.69]) -AC_INIT([taler-merchant],[0.14.1],[taler-bug@gnunet.org]) +AC_INIT([taler-merchant],[0.14.4],[taler-bug@gnunet.org]) AC_CONFIG_SRCDIR([src/backend/taler-merchant-httpd.c]) AC_CONFIG_HEADERS([taler_merchant_config.h]) # support for non-recursive builds @@ -277,12 +277,12 @@ AS_CASE([$with_exchange], CPPFLAGS="-I$with_exchange/include $CPPFLAGS $POSTGRESQL_CPPFLAGS"]) AC_CHECK_HEADERS([taler/taler_exchange_service.h], - [AC_CHECK_LIB([talerexchange], [TALER_EXCHANGE_test_account_allowed], libtalerexchange=1)]) + [AC_CHECK_LIB([talerexchange], [TALER_EXCHANGE_keys_test_account_allowed], libtalerexchange=1)]) AM_CONDITIONAL(HAVE_TALEREXCHANGE, test x$libtalerexchange = x1) AS_IF([test $libtalerexchange != 1], [AC_MSG_ERROR([[ *** -*** You need libtalerexchange >= 0.14.0 to build this program. +*** You need libtalerexchange >= 15:0:0 to build this program. *** This library is part of the GNU Taler exchange, available at *** https://taler.net *** ]])]) diff --git a/debian/changelog b/debian/changelog @@ -1,3 +1,21 @@ +taler-merchant (0.14.4) unstable; urgency=low + + * Release 0.14.4. + + -- Florian Dold <florian@dold.me> Mon, 27 Jan 2025 18:47:24 +0100 + +taler-merchant (0.14.3) unstable; urgency=low + + * Release 0.14.3. + + -- Florian Dold <florian@dold.me> Mon, 27 Jan 2025 18:39:13 +0100 + +taler-merchant (0.14.2) unstable; urgency=low + + * Release version 0.14.2 + + -- Florian Dold <dold@taler.net> Thu, 23 Jan 2025 20:28:54 +0100 + taler-merchant (0.14.1) unstable; urgency=low * Release version 0.14.1 diff --git a/debian/rules b/debian/rules @@ -37,10 +37,11 @@ override_dh_auto_clean: override_dh_installsystemd: # Need to specify units manually, since we have multiple # and dh_installsystemd by default only looks for "<package>.service". + dh_installsystemd -ptaler-merchant --name=taler-merchant-depositcheck --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-httpd --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-exchangekeyupdate --no-start --no-enable + dh_installsystemd -ptaler-merchant --name=taler-merchant-kyccheck --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-reconciliation --no-start --no-enable - dh_installsystemd -ptaler-merchant --name=taler-merchant-depositcheck --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-webhook --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant-wirewatch --no-start --no-enable dh_installsystemd -ptaler-merchant --name=taler-merchant --no-start --no-enable diff --git a/doc/doxygen/taler.doxy b/doc/doxygen/taler.doxy @@ -5,7 +5,7 @@ #--------------------------------------------------------------------------- DOXYFILE_ENCODING = UTF-8 PROJECT_NAME = "GNU Taler: Merchant" -PROJECT_NUMBER = 0.14.1 +PROJECT_NUMBER = 0.14.4 PROJECT_LOGO = logo.svg OUTPUT_DIRECTORY = . CREATE_SUBDIRS = YES diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -510,7 +510,7 @@ handle_mhd_completion_callback (void *cls, json_decref (hc->request_body); if (NULL != hc->instance) TMH_instance_decref (hc->instance); - TMH_db->preflight(TMH_db->cls); + TMH_db->preflight (TMH_db->cls); GNUNET_free (hc->full_url); GNUNET_free (hc); *con_cls = NULL; @@ -2299,11 +2299,6 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } - - /* TODO: Load config variables for merchant token family - cipher type "rsa" or "cs" and key size. - Defaults to "rsa" and 2048 bits. */ - if (GNUNET_OK != TALER_CONFIG_parse_currencies (cfg, TMH_currency, diff --git a/src/backend/taler-merchant-httpd_config.c b/src/backend/taler-merchant-httpd_config.c @@ -43,7 +43,7 @@ * #MERCHANT_PROTOCOL_CURRENT and #MERCHANT_PROTOCOL_AGE in * merchant_api_config.c! */ -#define MERCHANT_PROTOCOL_VERSION "17:0:13" +#define MERCHANT_PROTOCOL_VERSION "18:0:14" /** diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c @@ -58,8 +58,10 @@ TMH_string_from_contract_input_type (enum TALER_MERCHANT_ContractInputType t) { case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: return "token"; +#if FUTURE case TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN: return "coin"; +#endif default: return "invalid"; } @@ -73,8 +75,10 @@ TMH_string_from_contract_output_type (enum TALER_MERCHANT_ContractOutputType t) { case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: return "token"; +#if FUTURE case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN: return "coin"; +#endif case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: return "donation_receipt"; default: @@ -220,7 +224,9 @@ parse_choices (void *cls, GNUNET_JSON_spec_string ("kind", &kind), GNUNET_JSON_spec_string ("token_family_slug", - &output.details.token.token_family_slug), + // FIXME... + (const char **) &output.details.token. + token_family_slug), GNUNET_JSON_spec_uint32 ("key_index", &ki), GNUNET_JSON_spec_mark_optional ( diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -675,7 +675,7 @@ TMH_exchange_check_debit ( { const struct TALER_EXCHANGE_Keys *keys = exchange->keys; struct TALER_NormalizedPayto np; - bool account_ok = false; + bool account_ok; bool have_kyc = false; struct TALER_Amount kyc_limit; bool unlimited = true; @@ -693,30 +693,11 @@ TMH_exchange_check_debit ( max_amount->currency); return GNUNET_SYSERR; } - np = TALER_payto_normalize (wm->payto_uri); - - /* For all accounts of the exchange */ - for (unsigned int i = 0; i<keys->accounts_len; i++) - { - const struct TALER_EXCHANGE_WireAccount *account - = &keys->accounts[i]; - if (NULL != account->conversion_url) - { - GNUNET_log (GNUNET_ERROR_TYPE_INFO, - "Exchange %s account requires currency conversion (not supported)\n", - exchange->url); - continue; /* never use accounts with conversion */ - } - if (GNUNET_YES != - TALER_EXCHANGE_test_account_allowed (account, - false, /* debit */ - np)) - continue; - account_ok = true; - /* Check legitimization limits we have with this - account at this exchange, if we have any, apply them */ - } + np = TALER_payto_normalize (wm->payto_uri); + account_ok = TALER_EXCHANGE_keys_test_account_allowed (keys, + false, + np); GNUNET_free (np.normalized_payto); if (keys->kyc_enabled) { diff --git a/src/backend/taler-merchant-httpd_get-orders-ID.c b/src/backend/taler-merchant-httpd_get-orders-ID.c @@ -22,16 +22,19 @@ #include "platform.h" #include <jansson.h> #include <gnunet/gnunet_uri_lib.h> +#include <gnunet/gnunet_common.h> #include <taler/taler_signatures.h> #include <taler/taler_dbevents.h> #include <taler/taler_json_lib.h> #include <taler/taler_templating_lib.h> #include <taler/taler_exchange_service.h> -#include "taler-merchant-httpd_exchanges.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_get-orders-ID.h" #include "taler-merchant-httpd_mhd.h" #include "taler-merchant-httpd_qr.h" +#include "taler/taler_error_codes.h" +#include "taler/taler_util.h" +#include "taler_merchant_util.h" /** * How often do we retry DB transactions on serialization failures? @@ -123,43 +126,25 @@ struct GetOrderData const char *order_id; /** - * Where to get the contract - */ - const char *contract_url; - - /** - * fulfillment URL of the contract (valid as long as @e contract_terms is - * valid; but can also be NULL if the contract_terms does not come with - * a fulfillment URL). - */ - const char *fulfillment_url; - - /** * session of the client */ const char *session_id; /** - * Contract terms of the payment we are checking. NULL when they - * are not (yet) known. + * choice index (contract v1) */ - json_t *contract_terms; + int16_t choice_index; /** - * Merchant base URL from @e contract_terms. - */ - const char *merchant_base_url; - - /** - * Public reorder URL from @e contract_terms. - * Could be NULL if contract does not have one. + * Contract terms of the payment we are checking. NULL when they + * are not (yet) known. */ - const char *public_reorder_url; + json_t *contract_terms_json; /** - * Total amount in contract. + * Parsed contract terms, NULL when parsing failed. */ - struct TALER_Amount contract_total; + struct TALER_MERCHANT_Contract *contract_terms; /** * Total refunds granted for this payment. Only initialized @@ -314,14 +299,19 @@ suspend_god (struct GetOrderData *god) /* We reset the contract terms and start by looking them up again, as while we are suspended fundamental things could change (such as the contract being claimed) */ + if (NULL != god->contract_terms_json) + { + json_decref (god->contract_terms_json); + god->contract_terms->fulfillment_url = NULL; + god->contract_terms_json = NULL; + god->contract_parsed = false; + god->contract_terms->merchant_base_url = NULL; + god->contract_terms->public_reorder_url = NULL; + } if (NULL != god->contract_terms) { - json_decref (god->contract_terms); - god->fulfillment_url = NULL; + TALER_MERCHANT_contract_free (god->contract_terms); god->contract_terms = NULL; - god->contract_parsed = false; - god->merchant_base_url = NULL; - god->public_reorder_url = NULL; } GNUNET_assert (! god->suspended); god->contract_parsed = false; @@ -347,9 +337,14 @@ god_cleanup (void *cls) { struct GetOrderData *god = cls; + if (NULL != god->contract_terms_json) + { + json_decref (god->contract_terms_json); + god->contract_terms_json = NULL; + } if (NULL != god->contract_terms) { - json_decref (god->contract_terms); + TALER_MERCHANT_contract_free (god->contract_terms); god->contract_terms = NULL; } if (NULL != god->session_eh) @@ -567,7 +562,6 @@ phase_init (struct GetOrderData *god) &resume_by_event, god); } - } @@ -586,18 +580,26 @@ phase_lookup_terms (struct GetOrderData *god) /* Convert order_id to h_contract_terms */ TMH_db->preflight (TMH_db->cls); - GNUNET_assert (NULL == god->contract_terms); + GNUNET_assert (NULL == god->contract_terms_json); { enum GNUNET_DB_QueryStatus qs; - qs = TMH_db->lookup_contract_terms ( + bool paid; + bool wired; + bool session_matches; + qs = TMH_db->lookup_contract_terms3 ( TMH_db->cls, god->hc->instance->settings.id, god->order_id, - &god->contract_terms, + NULL, + &god->contract_terms_json, &order_serial, - &db_claim_token); + &paid, + &wired, + &session_matches, + &db_claim_token, + &god->choice_index); if (0 > qs) { /* single, read-only SQL statements should never cause @@ -620,14 +622,14 @@ phase_lookup_terms (struct GetOrderData *god) } /* Check if client provided the right hash code of the contract terms */ - if (NULL != god->contract_terms) + if (NULL != god->contract_terms_json) { god->contract_available = true; if (GNUNET_YES == GNUNET_is_zero (&god->h_contract_terms)) { if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, + TALER_JSON_contract_hash (god->contract_terms_json, &god->h_contract_terms)) { GNUNET_break (0); @@ -643,7 +645,7 @@ phase_lookup_terms (struct GetOrderData *god) struct TALER_PrivateContractHashP h; if (GNUNET_OK != - TALER_JSON_contract_hash (god->contract_terms, + TALER_JSON_contract_hash (god->contract_terms_json, &h)) { GNUNET_break (0); @@ -683,8 +685,8 @@ phase_lookup_terms (struct GetOrderData *god) god->order_id, &db_claim_token, &unused, - (NULL == god->contract_terms) - ? &god->contract_terms + (NULL == god->contract_terms_json) + ? &god->contract_terms_json : NULL); if (0 > qs) { @@ -729,41 +731,12 @@ phase_lookup_terms (struct GetOrderData *god) static void phase_parse_contract (struct GetOrderData *god) { - struct GNUNET_JSON_Specification espec[] = { - TALER_JSON_spec_amount_any ("amount", - &god->contract_total), - TALER_JSON_spec_web_url ("merchant_base_url", - &god->merchant_base_url), - GNUNET_JSON_spec_mark_optional ( - /* this one does NOT have to be a Web URL! */ - GNUNET_JSON_spec_string ("fulfillment_url", - &god->fulfillment_url), - NULL), - GNUNET_JSON_spec_mark_optional ( - TALER_JSON_spec_web_url ("public_reorder_url", - &god->public_reorder_url), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - const char *ename; - unsigned int eline; - - GNUNET_assert (NULL != god->contract_terms); - if (god->contract_parsed) - return; /* not sure this is possible... */ - - res = GNUNET_JSON_parse (god->contract_terms, - espec, - &ename, - &eline); - if (GNUNET_OK != res) + god->contract_terms = TALER_MERCHANT_contract_parse ( + god->contract_terms_json, + true); + + if (NULL == god->contract_terms) { - GNUNET_break (0); - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Failed to parse contract %s in DB at field %s\n", - god->order_id, - ename); phase_fail (god, MHD_HTTP_INTERNAL_SERVER_ERROR, TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, @@ -772,7 +745,7 @@ phase_parse_contract (struct GetOrderData *god) } god->contract_parsed = true; if ( (NULL != god->session_id) && - (NULL != god->fulfillment_url) && + (NULL != god->contract_terms->fulfillment_url) && (NULL == god->session_eh) ) { struct TMH_SessionEventP session_eh = { @@ -787,8 +760,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->fulfillment_url, - strlen (god->fulfillment_url), + GNUNET_CRYPTO_hash (god->contract_terms->fulfillment_url, + strlen (god->contract_terms->fulfillment_url), &session_eh.h_fulfillment_url); god->session_eh = TMH_db->event_listen ( @@ -847,7 +820,7 @@ phase_check_client_access (struct GetOrderData *god) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Neither claim token nor contract matched\n"); /* Client has no rights to this order */ - if (NULL == god->public_reorder_url) + if (NULL == god->contract_terms->public_reorder_url) { /* We cannot give the client a new order, just fail */ if (! GNUNET_is_zero (&god->h_contract_terms)) @@ -891,9 +864,10 @@ phase_check_client_access (struct GetOrderData *god) return; } GNUNET_break (MHD_YES == - MHD_add_response_header (reply, - MHD_HTTP_HEADER_LOCATION, - god->public_reorder_url)); + MHD_add_response_header ( + reply, + MHD_HTTP_HEADER_LOCATION, + god->contract_terms->public_reorder_url)); ret = MHD_queue_response (god->sc.con, MHD_HTTP_FOUND, reply); @@ -907,8 +881,9 @@ phase_check_client_access (struct GetOrderData *god) TALER_MHD_REPLY_JSON_PACK ( god->sc.con, MHD_HTTP_ACCEPTED, - GNUNET_JSON_pack_string ("public_reorder_url", - god->public_reorder_url))); + GNUNET_JSON_pack_string ( + "public_reorder_url", + god->contract_terms->public_reorder_url))); return; } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, @@ -935,7 +910,7 @@ get_order_summary (const struct GetOrderData *god) MHD_HTTP_HEADER_ACCEPT_LANGUAGE); if (NULL == language_pattern) language_pattern = "en"; - ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms, + ret = json_string_value (TALER_JSON_extract_i18n (god->contract_terms_json, language_pattern, "summary")); if (NULL == ret) @@ -1015,11 +990,11 @@ send_pay_request (struct GetOrderData *god, { struct MHD_Response *reply; - GNUNET_assert (NULL != god->fulfillment_url); + 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->fulfillment_url); + god->contract_terms->fulfillment_url); reply = MHD_create_response_from_buffer (0, NULL, MHD_RESPMEM_PERSISTENT); @@ -1031,9 +1006,10 @@ send_pay_request (struct GetOrderData *god, return false; } GNUNET_break (MHD_YES == - MHD_add_response_header (reply, - MHD_HTTP_HEADER_LOCATION, - god->fulfillment_url)); + MHD_add_response_header ( + reply, + MHD_HTTP_HEADER_LOCATION, + god->contract_terms->fulfillment_url)); { ret = MHD_queue_response (god->sc.con, MHD_HTTP_FOUND, @@ -1099,7 +1075,7 @@ send_pay_request (struct GetOrderData *god, taler_pay_uri), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", - god->fulfillment_url)), + god->contract_terms->fulfillment_url)), GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("already_paid_order_id", already_paid_order_id))); @@ -1156,7 +1132,7 @@ static bool phase_redirect_to_paid_order (struct GetOrderData *god) { if ( (NULL != god->session_id) && - (NULL != god->fulfillment_url) ) + (NULL != god->contract_terms->fulfillment_url) ) { /* Check if client paid for this fulfillment article already within this session, but using a different @@ -1172,11 +1148,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->fulfillment_url); + god->contract_terms->fulfillment_url); qs = TMH_db->lookup_order_by_fulfillment ( TMH_db->cls, god->hc->instance->settings.id, - god->fulfillment_url, + god->contract_terms->fulfillment_url, god->session_id, TALER_EXCHANGE_YNA_NO != god->allow_refunded_for_repurchase, &already_paid_order_id); @@ -1321,27 +1297,60 @@ static bool phase_check_refunded (struct GetOrderData *god) { enum GNUNET_DB_QueryStatus qs; + struct TALER_Amount refund_amount; + const char *refund_currency; + + switch (god->contract_terms->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + refund_amount = god->contract_terms->details.v0.brutto; + refund_currency = god->contract_terms->details.v0.brutto.currency; + break; + case TALER_MERCHANT_CONTRACT_VERSION_1: + if (god->choice_index < 0) + { + // order was not paid, no refund to be checked + god->phase++; + 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)); + break; + default: + { + GNUNET_break (0); + phase_fail (god, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_MERCHANT_GET_ORDERS_ID_INVALID_CONTRACT_VERSION, + NULL); + return false; + } + } if ( (god->sc.awaiting_refund) && (GNUNET_OK != - TALER_amount_cmp_currency (&god->contract_total, + TALER_amount_cmp_currency (&refund_amount, &god->sc.refund_expected)) ) { GNUNET_break (0); phase_fail (god, MHD_HTTP_CONFLICT, TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH, - god->contract_total.currency); + refund_currency); return false; } /* At this point, we know the contract was paid. Let's check for refunds. First, clear away refunds found from previous invocations. */ GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (god->contract_total.currency, + TALER_amount_set_zero (refund_currency, &god->refund_amount)); GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (god->contract_total.currency, + TALER_amount_set_zero (refund_currency, &god->refund_taken)); qs = TMH_db->lookup_refunds_detailed ( TMH_db->cls, @@ -1470,7 +1479,8 @@ phase_return_status (struct GetOrderData *god) MHD_HTTP_OK, GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_string ("fulfillment_url", - god->fulfillment_url)), + god->contract_terms->fulfillment_url + )), GNUNET_JSON_pack_bool ("refunded", god->refunded), GNUNET_JSON_pack_bool ("refund_pending", @@ -1487,8 +1497,8 @@ phase_return_status (struct GetOrderData *god) char *qr; char *uri; - GNUNET_assert (NULL != god->contract_terms); - uri = make_taler_refund_uri (god->merchant_base_url, + GNUNET_assert (NULL != god->contract_terms_json); + uri = make_taler_refund_uri (god->contract_terms->merchant_base_url, god->order_id); if (NULL == uri) { @@ -1551,7 +1561,7 @@ phase_return_status (struct GetOrderData *god) context = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_incref ("contract_terms", - god->contract_terms), + god->contract_terms_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_mhd.c b/src/backend/taler-merchant-httpd_mhd.c @@ -56,44 +56,22 @@ TMH_MHD_handler_agpl_redirect (const struct TMH_RequestHandler *rh, bool TMH_MHD_test_html_desired (struct MHD_Connection *connection) { - bool ret = false; const char *accept; + double json_q; + double html_q; - // FIXME: use TALER_MHD_check_accept here! accept = MHD_lookup_connection_value (connection, MHD_HEADER_KIND, MHD_HTTP_HEADER_ACCEPT); - if (NULL != accept) - { - char *a = GNUNET_strdup (accept); - char *saveptr; - - for (char *t = strtok_r (a, ",", &saveptr); - NULL != t; - t = strtok_r (NULL, ",", &saveptr)) - { - char *end; - - /* skip leading whitespace */ - while (isspace ((unsigned char) t[0])) - t++; - /* trim of ';q=' parameter and everything after space */ - end = strchr (t, ';'); - if (NULL != end) - *end = '\0'; - end = strchr (t, ' '); - if (NULL != end) - *end = '\0'; - if (0 == strcasecmp ("text/html", - t)) - { - ret = true; - break; - } - } - GNUNET_free (a); - } - return ret; + if (NULL == accept) + return false; /* nothing selected, we read this as not HTML */ + html_q = TALER_pattern_matches (accept, + "text/html"); + json_q = TALER_pattern_matches (accept, + "application/json"); + if (html_q > json_q) + return true; + return false; } diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-abort.c b/src/backend/taler-merchant-httpd_post-orders-ID-abort.c @@ -115,6 +115,10 @@ struct RefundDetails */ bool processed; + /** + * Did we find the deposit in our own database? + */ + bool found_deposit; }; @@ -362,13 +366,25 @@ generate_success_response (struct AbortContext *ac) struct RefundDetails *rdi = &ac->rd[i]; json_t *detail; - if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) && - (MHD_HTTP_NOT_FOUND != rdi->http_status) && - (MHD_HTTP_GONE != rdi->http_status) ) || - (0 == rdi->http_status) || - (NULL == rdi->exchange_reply) ) - hc = MHD_HTTP_BAD_GATEWAY; - if (MHD_HTTP_OK != rdi->http_status) + if (rdi->found_deposit) + { + if ( ( (MHD_HTTP_BAD_REQUEST <= rdi->http_status) && + (MHD_HTTP_NOT_FOUND != rdi->http_status) && + (MHD_HTTP_GONE != rdi->http_status) ) || + (0 == rdi->http_status) || + (NULL == rdi->exchange_reply) ) + { + hc = MHD_HTTP_BAD_GATEWAY; + } + } + if (! rdi->found_deposit) + { + detail = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("type", + "undeposited")); + } + else if (MHD_HTTP_OK != rdi->http_status) + { detail = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "failure"), @@ -384,7 +400,9 @@ generate_success_response (struct AbortContext *ac) GNUNET_JSON_pack_allow_null ( GNUNET_JSON_pack_object_incref ("exchange_reply", rdi->exchange_reply))); - else + } + else if (rdi->found_deposit) + { detail = GNUNET_JSON_PACK ( GNUNET_JSON_pack_string ("type", "success"), @@ -394,6 +412,7 @@ generate_success_response (struct AbortContext *ac) &rdi->exchange_sig), GNUNET_JSON_pack_data_auto ("exchange_pub", &rdi->exchange_pub)); + } GNUNET_assert (0 == json_array_append_new (refunds, detail)); @@ -535,6 +554,11 @@ process_abort_with_exchange (void *cls, continue; rdi->processed = true; ac->pending--; + if (! rdi->found_deposit) + { + /* Coin wasn't even deposited yet, we do not need to refund it. */ + continue; + } rdi->rh = TALER_EXCHANGE_refund ( TMH_curl_ctx, ac->current_exchange, @@ -557,6 +581,9 @@ process_abort_with_exchange (void *cls, } ac->pending_at_ce++; } + /* Still continue if no coins for this exchange were deposited. */ + if (0 == ac->pending_at_ce) + find_next_exchange (ac); } @@ -633,7 +660,6 @@ refund_coins (void *cls, struct AbortContext *ac = cls; struct GNUNET_TIME_Timestamp now; - (void) amount_with_fee; (void) deposit_fee; (void) refund_fee; now = GNUNET_TIME_timestamp_get (); @@ -649,7 +675,8 @@ refund_coins (void *cls, strcmp (exchange_url, rdi->exchange_url)) ) continue; /* not in request */ - + rdi->found_deposit = true; + rdi->amount_with_fee = *amount_with_fee; /* Store refund in DB */ qs = TMH_db->refund_coin (TMH_db->cls, ac->hc->instance->settings.id, @@ -884,10 +911,6 @@ parse_abort (struct MHD_Connection *connection, struct RefundDetails *rd = &ac->rd[coins_index]; const char *exchange_url; struct GNUNET_JSON_Specification ispec[] = { - /* FIXME: this is breaking multi-currency support! */ - TALER_JSON_spec_amount ("contribution", - TMH_currency, - &rd->amount_with_fee), TALER_JSON_spec_web_url ("exchange_url", &exchange_url), GNUNET_JSON_spec_fixed_auto ("coin_pub", diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-pay.c b/src/backend/taler-merchant-httpd_post-orders-ID-pay.c @@ -41,10 +41,10 @@ #include <taler/taler_exchange_service.h> #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_exchanges.h" -#include "taler-merchant-httpd_contract.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_post-orders-ID-pay.h" #include "taler-merchant-httpd_private-get-orders.h" +#include "taler_merchant_util.h" #include "taler_merchantdb_plugin.h" @@ -102,7 +102,7 @@ enum PayPhase PP_CHECK_CONTRACT, /** - * Validate provided tokens and token evelopes. + * Validate provided tokens and token envelopes. */ PP_VALIDATE_TOKENS, @@ -441,7 +441,13 @@ struct PayContext /** * Our contract (or NULL if not available). */ - json_t *contract_terms; + json_t *contract_terms_json; + + + /** + * Parsed contract terms, NULL when parsing failed. + */ + struct TALER_MERCHANT_Contract *contract_terms; /** * Wallet data json object from the request. Containing additional @@ -476,11 +482,6 @@ struct PayContext const char *order_id; /** - * Fulfillment URL from the contract, or NULL if we don't have one. - */ - char *fulfillment_url; - - /** * Serial number of this order in the database (set once we did the lookup). */ uint64_t order_serial; @@ -490,69 +491,6 @@ struct PayContext */ struct TALER_PrivateContractHashP h_contract_terms; - - /** - * "h_wire" from @e contract_terms. Used to identify - * the instance's wire transfer method. - */ - struct TALER_MerchantWireHashP h_wire; - - /** - * Version of the contract terms. - */ - enum TALER_MERCHANT_ContractVersion version; - - /** - * @e version dependent details from our contract. - */ - union - { - - struct - { - /** - * Maximum fee the merchant is willing to pay, from @e root. - * Note that IF the total fee of the exchange is higher, that is - * acceptable to the merchant if the customer is willing to - * pay the difference - * (i.e. amount - max_fee <= actual_amount - actual_fee). - */ - struct TALER_Amount max_fee; - - /** - * Amount from @e root. This is the amount the merchant expects - * to make, minus @e max_fee. - */ - struct TALER_Amount amount; - } v0; - - struct - { - - /** - * Array with @e choices_len choices from the contract terms. - */ - struct TALER_MERCHANT_ContractChoice *choices; - - /** - * Array with @e token_families_len token families from the contract terms. - */ - struct TALER_MERCHANT_ContractTokenFamily *token_families; - - /** - * Length of the @e choices array. - */ - unsigned int choices_len; - - /** - * Length of the @e token_families array. - */ - unsigned int token_families_len; - - } v1; - - } details; - /** * Results from the phase_validate_tokens() */ @@ -597,27 +535,6 @@ struct PayContext struct TALER_Amount total_refunded; /** - * Wire transfer deadline. How soon would the merchant like the - * wire transfer to be executed? - */ - struct GNUNET_TIME_Timestamp wire_transfer_deadline; - - /** - * Timestamp from @e contract_terms. - */ - struct GNUNET_TIME_Timestamp timestamp; - - /** - * Refund deadline from @e contract_terms. - */ - struct GNUNET_TIME_Timestamp refund_deadline; - - /** - * Deadline for the customer to pay for this proposal. - */ - struct GNUNET_TIME_Timestamp pay_deadline; - - /** * Set to the POS key, if applicable for this order. */ char *pos_key; @@ -628,11 +545,6 @@ struct PayContext enum TALER_MerchantConfirmationAlgorithm pos_alg; /** - * Minimum age required for this purchase. - */ - unsigned int minimum_age; - - /** * Number of coins this payment is made of. Length * of the @e dc array. */ @@ -954,7 +866,7 @@ batch_deposit_transaction (const struct ExchangeGroup *eg, dr->details.ok.deposit_timestamp, &pc->h_contract_terms, eg->exchange_url, - pc->wire_transfer_deadline, + pc->contract_terms->wire_deadline, &total_without_fees, &eg->wire_fee, &pc->wm->h_wire, @@ -974,7 +886,7 @@ batch_deposit_transaction (const struct ExchangeGroup *eg, continue; if (dc->found_in_db) continue; - /* NOTE: We might want to check if the order was fully paid concurrently + /* FIXME-#9457: We might want to check if the order was fully paid concurrently by some other wallet here, and if so, issue an auto-refund. Right now, it is possible to over-pay if two wallets literally make a concurrent payment, as the earlier check for 'paid' is not in the same transaction @@ -1104,8 +1016,8 @@ notify_kyc_required (const struct ExchangeGroup *eg) char *extra; hws = GNUNET_STRINGS_data_to_string_alloc ( - &eg->pc->h_wire, - sizeof (eg->pc->h_wire)); + &eg->pc->contract_terms->h_wire, + sizeof (eg->pc->contract_terms->h_wire)); GNUNET_asprintf (&extra, "%s %s", hws, @@ -1406,7 +1318,7 @@ process_pay_with_keys ( is_age_restricted_denom = (0 != denom_details->key.age_mask.bits); if (is_age_restricted_denom && - (0 < pc->minimum_age)) + (0 < pc->contract_terms->minimum_age)) { /* Minimum age given and restricted coin provided: We need to verify the * minimum age */ @@ -1430,7 +1342,7 @@ process_pay_with_keys ( if (GNUNET_OK != TALER_age_commitment_verify ( &dc->age_commitment, - pc->minimum_age, + pc->contract_terms->minimum_age, &dc->minimum_age_sig)) code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED; AGE_FAIL: @@ -1493,14 +1405,14 @@ AGE_FAIL: { struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size]; struct TALER_EXCHANGE_DepositContractDetail dcd = { - .wire_deadline = pc->wire_transfer_deadline, + .wire_deadline = pc->contract_terms->wire_deadline, .merchant_payto_uri = pc->wm->payto_uri, .wire_salt = pc->wm->wire_salt, .h_contract_terms = pc->h_contract_terms, .wallet_data_hash = pc->h_wallet_data, - .wallet_timestamp = pc->timestamp, + .wallet_timestamp = pc->contract_terms->timestamp, .merchant_pub = hc->instance->merchant_pub, - .refund_deadline = pc->refund_deadline + .refund_deadline = pc->contract_terms->refund_deadline }; enum TALER_ErrorCode ec; size_t off = 0; @@ -1739,7 +1651,7 @@ phase_success_response (struct PayContext *pc) : TALER_build_pos_confirmation (pc->pos_key, pc->pos_alg, &pc->validate_tokens.brutto, - pc->timestamp); + pc->contract_terms->timestamp); token_sigs = (0 >= pc->output_tokens_len) ? NULL : build_token_sigs (pc); @@ -1787,7 +1699,7 @@ phase_payment_notification (struct PayContext *pc) 0); } if ( (NULL != pc->session_id) && - (NULL != pc->fulfillment_url) ) + (NULL != pc->contract_terms->fulfillment_url) ) { struct TMH_SessionEventP session_eh = { .header.size = htons (sizeof (session_eh)), @@ -1798,12 +1710,12 @@ phase_payment_notification (struct PayContext *pc) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Notifying clients about session change to %s for %s\n", pc->session_id, - pc->fulfillment_url); + pc->contract_terms->fulfillment_url); GNUNET_CRYPTO_hash (pc->session_id, strlen (pc->session_id), &session_eh.h_session_id); - GNUNET_CRYPTO_hash (pc->fulfillment_url, - strlen (pc->fulfillment_url), + GNUNET_CRYPTO_hash (pc->contract_terms->fulfillment_url, + strlen (pc->contract_terms->fulfillment_url), &session_eh.h_fulfillment_url); TMH_db->event_notify (TMH_db->cls, &session_eh.header, @@ -2446,7 +2358,7 @@ phase_execute_pay_transaction (struct PayContext *pc) TMH_notify_order_change (hc->instance, TMH_OSF_CLAIMED | TMH_OSF_PAID, - pc->timestamp, + pc->contract_terms->timestamp, pc->order_serial); { enum GNUNET_DB_QueryStatus qs; @@ -2454,7 +2366,7 @@ phase_execute_pay_transaction (struct PayContext *pc) jhook = GNUNET_JSON_PACK ( GNUNET_JSON_pack_object_incref ("contract_terms", - pc->contract_terms), + pc->contract_terms_json), GNUNET_JSON_pack_string ("order_id", pc->order_id) ); @@ -2622,6 +2534,20 @@ find_valid_input_tokens ( } +/** + * Sign the tokens provided by the wallet for a particular @a key. + * + * @param[in,out] payment we are processing + * @param key token family data + * @param priv private key to use to sign with + * @param critical true if the token must exist, if false + * and the client did not provide an envelope, that's OK and + * we just also skimp on the signature + * @param index offset in the token envelope array (from other families) + * @param expected_num number of tokens of this type that we should create + * @return #GNUNET_NO on failure + * #GNUNET_OK on success + */ static enum GNUNET_GenericReturnValue sign_token_envelopes (struct PayContext *pc, struct TALER_MERCHANT_ContractTokenFamilyKey *key, @@ -2638,7 +2564,8 @@ sign_token_envelopes (struct PayContext *pc, const struct TokenEnvelope *env = &pc->token_envelopes[pos]; struct SignedOutputToken *output = &pc->output_tokens[pos]; - if (pos > pc->token_envelopes_cnt || pos > pc->output_tokens_len) + if ( (pos >= pc->token_envelopes_cnt) || + (pos >= pc->output_tokens_len) ) { GNUNET_assert (0); /* this should not happen */ return GNUNET_NO; @@ -2699,10 +2626,12 @@ static const struct TALER_MERCHANT_ContractTokenFamily * find_family (const struct PayContext *pc, const char *slug) { - for (unsigned int i = 0; i<pc->details.v1.token_families_len; i++) + for (unsigned int i = 0; + i < pc->contract_terms->details.v1.token_authorities_len; + i++) { const struct TALER_MERCHANT_ContractTokenFamily *tfi - = &pc->details.v1.token_families[i]; + = &pc->contract_terms->details.v1.token_authorities[i]; if (0 == strcmp (tfi->slug, slug)) @@ -2729,18 +2658,18 @@ find_family (const struct PayContext *pc, static void phase_validate_tokens (struct PayContext *pc) { - switch (pc->version) + switch (pc->contract_terms->version) { case TALER_MERCHANT_CONTRACT_VERSION_0: /* No tokens to validate */ pc->phase = PP_PAY_TRANSACTION; - pc->validate_tokens.max_fee = pc->details.v0.max_fee; - pc->validate_tokens.brutto = pc->details.v0.amount; + pc->validate_tokens.max_fee = pc->contract_terms->details.v0.max_fee; + pc->validate_tokens.brutto = pc->contract_terms->details.v0.brutto; break; case TALER_MERCHANT_CONTRACT_VERSION_1: { const struct TALER_MERCHANT_ContractChoice *selected - = &pc->details.v1.choices[pc->choice_index]; + = &pc->contract_terms->details.v1.choices[pc->choice_index]; pc->validate_tokens.max_fee = selected->max_fee; pc->validate_tokens.brutto = selected->amount; @@ -2835,8 +2764,8 @@ phase_validate_tokens (struct PayContext *pc) TMH_db->cls, pc->hc->instance->settings.id, family->slug, - pc->timestamp, - pc->pay_deadline, + pc->contract_terms->timestamp, + pc->contract_terms->pay_deadline, &details); if (qs <= 0) { @@ -2844,8 +2773,10 @@ phase_validate_tokens (struct PayContext *pc) GNUNET_ERROR_TYPE_ERROR, "Did not find key for %s at [%llu,%llu]\n", family->slug, - (unsigned long long) pc->timestamp.abs_time.abs_value_us, - (unsigned long long) pc->pay_deadline.abs_time.abs_value_us); + (unsigned long long) pc->contract_terms->timestamp.abs_time. + abs_value_us, + (unsigned long long) pc->contract_terms->pay_deadline.abs_time. + abs_value_us); GNUNET_break (0); pay_end (pc, TALER_MHD_reply_with_error ( @@ -2862,7 +2793,7 @@ phase_validate_tokens (struct PayContext *pc) sign_token_envelopes (pc, key, &details.priv, - /* TODO: Use critical field stored in database here instead. */ + /* FIXME: Use critical field stored in database here instead. */ details.token_family.kind == TALER_MERCHANTDB_TFK_Subscription, i, @@ -2886,7 +2817,7 @@ phase_validate_tokens (struct PayContext *pc) GNUNET_break_op (0); fprintf (stderr, "HERE (%u): %s != %s\n", - (unsigned int) pc->version, + (unsigned int) pc->contract_terms->version, dc->cdd.amount.currency, TALER_amount2s (&pc->validate_tokens.brutto)); pay_end (pc, @@ -3027,7 +2958,7 @@ phase_contract_paid (struct PayContext *pc) { enum GNUNET_DB_QueryStatus qs; - /* TODO: Use h_contract instead of order_serial here? */ + /* FIXME: Use h_contract instead of order_serial here? */ qs = TMH_db->lookup_spent_tokens_by_order (TMH_db->cls, pc->order_serial, &input_tokens_paid_check, @@ -3063,7 +2994,7 @@ phase_contract_paid (struct PayContext *pc) TALER_merchant_pay_sign (&pc->h_contract_terms, &pc->hc->instance->merchant_priv, &sig); - /* TODO: Add token_sigs to response body. */ + /* FIXME: Add token_sigs to response body. */ pay_end (pc, TALER_MHD_REPLY_JSON_PACK ( pc->connection, @@ -3073,7 +3004,7 @@ phase_contract_paid (struct PayContext *pc) return; } /* Conflict, double-payment detected! */ - /* TODO: What should we do with input tokens? + /* FIXME: What should we do with input tokens? Currently there is no refund for tokens. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Client attempted to pay extra for already paid order `%s'\n", @@ -3133,15 +3064,22 @@ phase_check_contract (struct PayContext *pc) enum GNUNET_DB_QueryStatus qs; bool paid = false; + if (NULL != pc->contract_terms_json) + { + json_decref (pc->contract_terms_json); + pc->contract_terms_json = NULL; + } + if (NULL != pc->contract_terms) { - json_decref (pc->contract_terms); + TALER_MERCHANT_contract_free (pc->contract_terms); pc->contract_terms = NULL; } + qs = TMH_db->lookup_contract_terms2 (TMH_db->cls, pc->hc->instance->settings.id, pc->order_id, - &pc->contract_terms, + &pc->contract_terms_json, &pc->order_serial, &paid, NULL, @@ -3173,11 +3111,11 @@ phase_check_contract (struct PayContext *pc) return; } /* hash contract (needed later) */ - json_dumpf (pc->contract_terms, + json_dumpf (pc->contract_terms_json, stderr, JSON_INDENT (2)); if (GNUNET_OK != - TALER_JSON_contract_hash (pc->contract_terms, + TALER_JSON_contract_hash (pc->contract_terms_json, &pc->h_contract_terms)) { GNUNET_break (0); @@ -3202,9 +3140,11 @@ phase_check_contract (struct PayContext *pc) pc->order_id, GNUNET_h2s (&pc->h_contract_terms.hash)); - /* basic sanity check on the contract */ - if (NULL == json_object_get (pc->contract_terms, - "merchant")) + pc->contract_terms = TALER_MERCHANT_contract_parse ( + pc->contract_terms_json, + true); + + if (NULL == pc->contract_terms) { /* invalid contract */ GNUNET_break (0); @@ -3212,95 +3152,17 @@ phase_check_contract (struct PayContext *pc) TALER_MHD_reply_with_error ( pc->connection, MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING, - NULL)); + TALER_EC_MERCHANT_GENERIC_DB_CONTRACT_CONTENT_INVALID, + pc->order_id)); return; } /* Get details from contract and check fundamentals */ { - const char *fulfillment_url = NULL; - uint64_t version = 0; - struct GNUNET_JSON_Specification espec[] = { - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint64 ("version", - &version), - NULL), - GNUNET_JSON_spec_mark_optional ( - /* This one does not have to be a Web URL */ - GNUNET_JSON_spec_string ("fulfillment_url", - &fulfillment_url), - NULL), - 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_transfer_deadline), - GNUNET_JSON_spec_fixed_auto ("h_wire", - &pc->h_wire), - GNUNET_JSON_spec_mark_optional ( - GNUNET_JSON_spec_uint32 ("minimum_age", - &pc->minimum_age), - NULL), - GNUNET_JSON_spec_end () - }; - enum GNUNET_GenericReturnValue res; - - pc->minimum_age = 0; - res = TALER_MHD_parse_internal_json_data (pc->connection, - pc->contract_terms, - espec); - if (GNUNET_YES != res) - { - GNUNET_break (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - if (NULL != fulfillment_url) - pc->fulfillment_url = GNUNET_strdup (fulfillment_url); - switch (version) + switch (pc->contract_terms->version) { - case 0: + case TALER_MERCHANT_CONTRACT_VERSION_0: { - struct GNUNET_JSON_Specification v0spec[] = { - TALER_JSON_spec_amount_any ("amount", - &pc->details.v0.amount), - TALER_JSON_spec_amount_any ("max_fee", - &pc->details.v0.max_fee), - GNUNET_JSON_spec_end () - }; - res = TALER_MHD_parse_internal_json_data (pc->connection, - pc->contract_terms, - v0spec); - if (GNUNET_YES != res) - { - GNUNET_break (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } - if (GNUNET_OK != - TALER_amount_cmp_currency (&pc->details.v0.max_fee, - &pc->details.v0.amount)) - { - GNUNET_break (0); - pay_end (pc, - TALER_MHD_reply_with_error ( - pc->connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "'max_fee' in database does not match currency of contract price")); - return; - } - if (pc->choice_index > 0) { GNUNET_break (0); @@ -3313,31 +3175,9 @@ phase_check_contract (struct PayContext *pc) return; } } - pc->version = TALER_MERCHANT_CONTRACT_VERSION_0; break; - case 1: + case TALER_MERCHANT_CONTRACT_VERSION_1: { - struct GNUNET_JSON_Specification v1spec[] = { - TALER_JSON_spec_choices ("choices", - &pc->details.v1.choices, - &pc->details.v1.choices_len), - TALER_JSON_spec_token_families ("token_families", - &pc->details.v1.token_families, - &pc->details.v1.token_families_len), - GNUNET_JSON_spec_end () - }; - res = TALER_MHD_parse_internal_json_data (pc->connection, - pc->contract_terms, - v1spec); - if (GNUNET_YES != res) - { - GNUNET_break (0); - pay_end (pc, - (GNUNET_NO == res) - ? MHD_YES - : MHD_NO); - return; - } if (pc->choice_index < 0) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, @@ -3353,13 +3193,13 @@ phase_check_contract (struct PayContext *pc) NULL)); return; } - if (pc->choice_index >= pc->details.v1.choices_len) + if (pc->choice_index >= pc->contract_terms->details.v1.choices_len) { GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Order `%s' has choices array with %u elements but " "request has 'choice_index' field with value %ld\n", pc->order_id, - pc->details.v1.choices_len, + pc->contract_terms->details.v1.choices_len, pc->choice_index); GNUNET_break (0); pay_end (pc, @@ -3371,7 +3211,6 @@ phase_check_contract (struct PayContext *pc) return; } } - pc->version = TALER_MERCHANT_CONTRACT_VERSION_1; break; default: GNUNET_break (0); @@ -3387,9 +3226,9 @@ phase_check_contract (struct PayContext *pc) } - if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline, + if (GNUNET_TIME_timestamp_cmp (pc->contract_terms->wire_deadline, <, - pc->refund_deadline)) + pc->contract_terms->refund_deadline)) { /* This should already have been checked when creating the order! */ GNUNET_break (0); @@ -3401,7 +3240,7 @@ phase_check_contract (struct PayContext *pc) NULL)); return; } - if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time)) + if (GNUNET_TIME_absolute_is_past (pc->contract_terms->pay_deadline.abs_time)) { /* too late */ pay_end (pc, @@ -3418,7 +3257,7 @@ phase_check_contract (struct PayContext *pc) struct TMH_WireMethod *wm; wm = pc->hc->instance->wm_head; - while (0 != GNUNET_memcmp (&pc->h_wire, + while (0 != GNUNET_memcmp (&pc->contract_terms->h_wire, &wm->h_wire)) wm = wm->next; if (NULL == wm) @@ -3856,10 +3695,10 @@ pay_context_cleanup (void *cls) GNUNET_SCHEDULER_cancel (pc->timeout_task); pc->timeout_task = NULL; } - if (NULL != pc->contract_terms) + if (NULL != pc->contract_terms_json) { - json_decref (pc->contract_terms); - pc->contract_terms = NULL; + json_decref (pc->contract_terms_json); + pc->contract_terms_json = NULL; } for (unsigned int i = 0; i<pc->coins_cnt; i++) { @@ -3878,38 +3717,16 @@ pay_context_cleanup (void *cls) GNUNET_free (eg); } GNUNET_free (pc->egs); - switch (pc->version) + if (NULL != pc->contract_terms) { - case TALER_MERCHANT_CONTRACT_VERSION_0: - break; - case TALER_MERCHANT_CONTRACT_VERSION_1: - for (unsigned int i = 0; i< pc->details.v1.token_families_len; i++) - { - struct TALER_MERCHANT_ContractTokenFamily *tf - = &pc->details.v1.token_families[i]; - - GNUNET_free (tf->slug); - GNUNET_free (tf->name); - GNUNET_free (tf->description); - json_decref (tf->description_i18n); - for (unsigned int j = 0; j<tf->keys_len; j++) - { - struct TALER_MERCHANT_ContractTokenFamilyKey *key - = &tf->keys[j]; - - TALER_token_issue_pub_free (&key->pub); - } - GNUNET_free (tf->keys); - } - GNUNET_free (pc->details.v1.token_families); - break; + TALER_MERCHANT_contract_free (pc->contract_terms); + pc->contract_terms = NULL; } if (NULL != pc->response) { MHD_destroy_response (pc->response); pc->response = NULL; } - GNUNET_free (pc->fulfillment_url); GNUNET_free (pc->session_id); GNUNET_CONTAINER_DLL_remove (pc_head, pc_tail, diff --git a/src/backend/taler-merchant-httpd_post-orders-ID-refund.c b/src/backend/taler-merchant-httpd_post-orders-ID-refund.c @@ -666,23 +666,26 @@ TMH_post_orders_ID_refund (const struct TMH_RequestHandler *rh, NULL); } + qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, + hc->instance->settings.id, + &prd->h_contract_terms, + &process_refunds_cb, + prd); + if (0 > qs) { - GNUNET_assert (GNUNET_OK == - TALER_amount_set_zero (TMH_currency, - &prd->refund_amount)); - qs = TMH_db->lookup_refunds_detailed (TMH_db->cls, - hc->instance->settings.id, - &prd->h_contract_terms, - &process_refunds_cb, - prd); - if (0 > qs) - { - GNUNET_break (0); - return TALER_MHD_reply_with_error (connection, - MHD_HTTP_INTERNAL_SERVER_ERROR, - TALER_EC_GENERIC_DB_FETCH_FAILED, - "detailed refunds"); - } + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "detailed refunds"); + } + if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs) + { + GNUNET_break (0); + return TALER_MHD_reply_with_error (connection, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TALER_EC_GENERIC_DB_FETCH_FAILED, + "no coins found that could be refunded"); } /* Now launch exchange interactions, unless we already have the diff --git a/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c b/src/backend/taler-merchant-httpd_private-get-instances-ID-kyc.c @@ -728,7 +728,7 @@ determine_eligible_accounts ( } else { - /* FIXME: history fee should be globally renamed to KYC fee... */ + /* FIXME-#9427: history fee should be globally renamed to KYC fee... */ kyc_amount = gf->fees.history; } } diff --git a/src/backend/taler-merchant-httpd_private-get-orders-ID.c b/src/backend/taler-merchant-httpd_private-get-orders-ID.c @@ -619,7 +619,6 @@ phase_fetch_contract (struct GetOrderRequestContext *gorc) gorc->order_only = false; } TMH_db->preflight (TMH_db->cls); - /* TODO: Check if choice_index is actually set to NULL if not in db. */ qs = TMH_db->lookup_contract_terms3 (TMH_db->cls, hc->instance->settings.id, hc->infix, @@ -1387,7 +1386,7 @@ phase_reply_result (struct GetOrderRequestContext *gorc) struct TMH_HandlerContext *hc = gorc->hc; MHD_RESULT ret; char *order_status_url; - json_t *choice_index = json_null (); + json_t *choice_index; { struct TALER_PrivateContractHashP *h_contract = NULL; @@ -1422,6 +1421,10 @@ phase_reply_result (struct GetOrderRequestContext *gorc) gorc->choice_index); choice_index = json_integer ((json_int_t) gorc->choice_index); } + else + { + choice_index = NULL; + } ret = TALER_MHD_REPLY_JSON_PACK ( gorc->sc.con, MHD_HTTP_OK, @@ -1460,10 +1463,10 @@ phase_reply_result (struct GetOrderRequestContext *gorc) gorc->refund_details), GNUNET_JSON_pack_string ("order_status_url", order_status_url), - { - .field_name = "choice_index", - .object = choice_index, - }); + GNUNET_JSON_pack_allow_null ( + GNUNET_JSON_pack_object_steal ( + "choice_index", + choice_index))); GNUNET_free (order_status_url); gorc->wire_details = NULL; gorc->refund_details = NULL; diff --git a/src/backend/taler-merchant-httpd_private-post-orders.c b/src/backend/taler-merchant-httpd_private-post-orders.c @@ -36,6 +36,8 @@ #include <taler/taler_signatures.h> #include <taler/taler_json_lib.h> #include <taler/taler_dbevents.h> +#include <taler/taler_util.h> +#include <taler_merchant_util.h> #include <time.h> #include "taler-merchant-httpd.h" #include "taler-merchant-httpd_private-post-orders.h" @@ -43,6 +45,7 @@ #include "taler-merchant-httpd_contract.h" #include "taler-merchant-httpd_helper.h" #include "taler-merchant-httpd_private-get-orders.h" + #include "taler_merchantdb_plugin.h" @@ -633,7 +636,6 @@ struct OrderContext ORDER_PHASE_PARSE_ORDER, ORDER_PHASE_PARSE_CHOICES, #ifdef HAVE_DONAU_DONAU_SERVICE_H - // TODO: PLACE HOLDER FOR THE PARSE DONAU INSTANCES, maybe it needs to be lower in the order ORDER_PHASE_PARSE_DONAU, #endif ORDER_PHASE_MERGE_INVENTORY, @@ -1901,43 +1903,11 @@ output_token_families (struct OrderContext *oc) { const struct TALER_MERCHANT_ContractTokenFamily *family = &oc->parse_choices.token_families[i]; - json_t *keys; json_t *jfamily; - keys = json_array (); - GNUNET_assert (NULL != keys); - for (unsigned int j = 0; j<family->keys_len; j++) - { - const struct TALER_MERCHANT_ContractTokenFamilyKey *key - = &family->keys[j]; - json_t *jkey = GNUNET_JSON_PACK ( - TALER_JSON_pack_token_pub ("public_key", - &key->pub), - GNUNET_JSON_pack_timestamp ("valid_after", - key->valid_after), - GNUNET_JSON_pack_timestamp ("valid_before", - key->valid_before) - ); + jfamily = TALER_MERCHANT_json_from_token_family (family); - GNUNET_assert (0 == - json_array_append_new (keys, - jkey)); - } - - /* FIXME: Add 'details' field. */ - jfamily = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("name", - family->name), - GNUNET_JSON_pack_string ("description", - family->description), - GNUNET_JSON_pack_allow_null ( - GNUNET_JSON_pack_object_incref ("description_i18n", - family->description_i18n)), - GNUNET_JSON_pack_array_steal ("keys", - keys), - GNUNET_JSON_pack_bool ("critical", - family->critical) - ); + GNUNET_assert (jfamily != NULL); GNUNET_assert (0 == json_object_set_new (token_families, @@ -1963,107 +1933,15 @@ output_contract_choices (struct OrderContext *oc) GNUNET_assert (NULL != choices); for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { - const struct TALER_MERCHANT_ContractChoice *choice - = &oc->parse_choices.choices[i]; - json_t *inputs = json_array (); - json_t *outputs = json_array (); - - GNUNET_assert (NULL != inputs); - GNUNET_assert (NULL != outputs); - for (unsigned int j = 0; j<choice->inputs_len; j++) - { - const struct TALER_MERCHANT_ContractInput *input - = &choice->inputs[j]; - json_t *jinput; - - /* For now, only tokens are supported for inputs */ - GNUNET_assert (TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN == input->type); - jinput = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("kind", - TMH_string_from_contract_input_type ( - input->type)), - GNUNET_JSON_pack_string ("token_family_slug", - input->details.token.token_family_slug), - GNUNET_JSON_pack_int64 ("count", - input->details.token.count) - ); - - GNUNET_assert (0 == - json_array_append_new (inputs, - jinput)); - } - - for (unsigned int j = 0; j<choice->outputs_len; j++) - { - struct TALER_MERCHANT_ContractOutput *output = &choice->outputs[j]; - json_t *joutput; - - /* For now, only tokens are supported */ - switch (output->type) - { - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: - /* How did we get here? */ - GNUNET_assert (0); - /* mostly to make compiler happy... */ - finalize_order (oc, - MHD_NO); - json_decref (choices); - return NULL; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: - joutput = GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("kind", - TMH_string_from_contract_output_type ( - output->type)), - GNUNET_JSON_pack_string ("token_family_slug", - output->details.token.token_family_slug), - GNUNET_JSON_pack_int64 ("count", - output->details.token.count), - GNUNET_JSON_pack_int64 ("key_index", - output->details.token.key_index) - ); - break; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_COIN: - /* Not implemented, how did we get here? */ - GNUNET_break (0); - reply_with_error (oc, - MHD_HTTP_NOT_IMPLEMENTED, - TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE, - "currency conversion not supported"); - json_decref (choices); - return NULL; - case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT: - // FIXME: generate JSON for DONAU here instead of killing - // the connection! - GNUNET_break (0); - finalize_order (oc, - MHD_NO); - json_decref (choices); - return NULL; - } - - GNUNET_assert (0 == - json_array_append_new (outputs, - joutput)); - } - - { - json_t *jchoice - = GNUNET_JSON_PACK ( - TALER_JSON_pack_amount ("amount", - &choice->amount), - TALER_JSON_pack_amount ("max_fee", - &oc->set_max_fee.details.v1.max_fees[i]), - GNUNET_JSON_pack_array_incref ("inputs", - inputs), - GNUNET_JSON_pack_array_incref ("outputs", - outputs) - ); + oc->parse_choices.choices[i].max_fee = + oc->set_max_fee.details.v1.max_fees[i]; + GNUNET_assert (0 == json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice ( + &oc->parse_choices.choices[i], + false))); + } - GNUNET_assert (0 == - json_array_append_new (choices, - jchoice)); - } - } /* for all choices */ return choices; } @@ -2434,6 +2312,41 @@ update_stefan (struct OrderContext *oc, /** + * Check our KYC status at all exchanges as our current limit is + * too low and we failed to create an order. + * + * @param oc order context + * @param exchange_url exchange to notify about + */ +static void +notify_kyc_required (const struct OrderContext *oc, + const char *exchange_url) +{ + struct GNUNET_DB_EventHeaderP es = { + .size = htons (sizeof (es)), + .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) + }; + char *hws; + char *extra; + + hws = GNUNET_STRINGS_data_to_string_alloc ( + &oc->add_payment_details.wm->h_wire, + sizeof (oc->add_payment_details.wm->h_wire)); + + GNUNET_asprintf (&extra, + "%s %s", + hws, + exchange_url); + TMH_db->event_notify (TMH_db->cls, + &es, + extra, + strlen (extra) + 1); + GNUNET_free (extra); + GNUNET_free (hws); +} + + +/** * Compute the set of exchanges that would be acceptable * for this order. * @@ -2467,6 +2380,15 @@ get_acceptable (void *cls, TALER_amount2s (&max_amount)); if (TALER_amount_is_zero (&max_amount)) { + if (! TALER_amount_is_zero (max_needed)) + { + /* Trigger re-checking the current deposit limit when + * paying non-zero amount with zero deposit limit */ + notify_kyc_required (oc, + url); + } + /* If deposit is impossible, we don't list the + * exchange in the contract terms. */ GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Exchange %s deposit limit is zero, skipping it\n", url); @@ -2675,47 +2597,6 @@ get_exchange_keys (void *cls, /** - * Check our KYC status at all exchanges as our current limit is - * too low and we failed to create an order. - * - * @param oc order context - */ -static void -notify_kyc_required (const struct OrderContext *oc) -{ - struct GNUNET_DB_EventHeaderP es = { - .size = htons (sizeof (es)), - .type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED) - }; - char *hws; - char *extra; - json_t *exchange; - size_t i; - - hws = GNUNET_STRINGS_data_to_string_alloc ( - &oc->add_payment_details.wm->h_wire, - sizeof (oc->add_payment_details.wm->h_wire)); - json_array_foreach (oc->set_exchanges.exchanges, i, exchange) - { - const char *exchange_url - = json_string_value (json_object_get (exchange, - "url")); - - GNUNET_asprintf (&extra, - "%s %s", - hws, - exchange_url); - TMH_db->event_notify (TMH_db->cls, - &es, - extra, - strlen (extra) + 1); - GNUNET_free (extra); - } - GNUNET_free (hws); -} - - -/** * Task run when we are timing out on /keys and will just * proceed with what we got. * @@ -2910,7 +2791,6 @@ set_exchanges (struct OrderContext *oc) if (! ok) { - notify_kyc_required (oc); reply_with_error ( oc, MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS, @@ -2944,7 +2824,7 @@ parse_order (struct OrderContext *oc) const char *merchant_base_url = NULL; uint64_t version = 0; const json_t *jmerchant = NULL; - const char *order_id; + const char *order_id = NULL; struct GNUNET_JSON_Specification spec[] = { GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint64 ("version", @@ -3041,6 +2921,37 @@ parse_order (struct OrderContext *oc) ret); return; } + if (NULL != order_id) + { + size_t len = strlen (order_id); + + for (size_t i = 0; i<len; i++) + { + char c = order_id[i]; + + if (! ( ( (c >= 'A') && + (c <= 'Z') ) || + ( (c >= 'a') && + (c <= 'z') ) || + ( (c >= '0') && + (c <= '9') ) || + (c == '-') || + (c == '_') || + (c == '.') || + (c == ':') ) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Invalid character `%c' in order ID `%s'\n", + c, + order_id); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_CURRENCY_MISMATCH, + "Invalid character in order_id"); + return; + } + } + } switch (version) { case 0: @@ -3554,7 +3465,9 @@ parse_order_outputs (struct OrderContext *oc, GNUNET_JSON_spec_string ("kind", &kind), GNUNET_JSON_spec_string ("token_family_slug", - &output.details.token.token_family_slug), + // FIXME... + (const char **) &output.details.token. + token_family_slug), GNUNET_JSON_spec_mark_optional ( GNUNET_JSON_spec_uint32 ("count", &output.details.token.count), @@ -3652,7 +3565,7 @@ parse_order_outputs (struct OrderContext *oc, static void parse_choices (struct OrderContext *oc) { - const json_t *choices; + const json_t *jchoices; switch (oc->parse_order.version) { @@ -3666,10 +3579,14 @@ parse_choices (struct OrderContext *oc) GNUNET_assert (0); } - choices = oc->parse_order.details.v1.choices; + jchoices = oc->parse_order.details.v1.choices; + + if (! json_is_array (jchoices)) + GNUNET_assert (0); + GNUNET_array_grow (oc->parse_choices.choices, oc->parse_choices.choices_len, - json_array_size (choices)); + json_array_size (jchoices)); for (unsigned int i = 0; i<oc->parse_choices.choices_len; i++) { struct TALER_MERCHANT_ContractChoice *choice @@ -3698,13 +3615,14 @@ parse_choices (struct OrderContext *oc) }; enum GNUNET_GenericReturnValue ret; - ret = GNUNET_JSON_parse (json_array_get (choices, + 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, @@ -3742,29 +3660,118 @@ parse_choices (struct OrderContext *oc) return; } + if (NULL != jinputs) + { + const json_t *jinput; + size_t idx; + json_array_foreach ((json_t *) jinputs, idx, jinput) + { + struct TALER_MERCHANT_ContractInput input = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_input ((json_t *) jinput, + &input, + idx, + true)) + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "input"); + return; + } + + switch (input.type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == input.details.token.count) + continue; - if ( (0 == json_array_size (jinputs)) && - (0 == json_array_size (joutputs)) ) + if (GNUNET_OK != + add_input_token_family (oc, + input.details.token.token_family_slug)) + + { + GNUNET_break_op (0); + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + input.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->inputs, + choice->inputs_len, + input); + continue; + } + GNUNET_assert (0); + } + } + + if (NULL != joutputs) { - GNUNET_log (GNUNET_ERROR_TYPE_WARNING, - "Choice #%u must have at least one input or output\n", - i); - reply_with_error (oc, - MHD_HTTP_BAD_REQUEST, - TALER_EC_GENERIC_PARAMETER_MALFORMED, - "choice"); - return; + const json_t *joutput; + size_t idx; + json_array_foreach ((json_t *) joutputs, idx, joutput) + { + struct TALER_MERCHANT_ContractOutput output = { + .details.token.count = 1 + }; + + if (GNUNET_OK != + TALER_MERCHANT_parse_choice_output ((json_t *) joutput, + &output, + idx, + true)) + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_GENERIC_PARAMETER_MALFORMED, + "output"); + return; + } + + switch (output.type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + GNUNET_assert (0); + break; + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN: + /* Ignore inputs tokens with 'count' field set to 0 */ + if (0 == output.details.token.count) + continue; + + // FIXME: no valid_at in choice->output, is NOW fine? + if (GNUNET_OK != + add_output_token_family (oc, + output.details.token.token_family_slug, + GNUNET_TIME_timestamp_get (), + &output.details.token.key_index)) + + { + reply_with_error (oc, + MHD_HTTP_BAD_REQUEST, + TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN, + output.details.token.token_family_slug); + return; + } + + GNUNET_array_append (choice->outputs, + choice->outputs_len, + output); + continue; + } + GNUNET_assert (0); + } } - if (GNUNET_OK != - parse_order_inputs (oc, - choice, - jinputs)) - return; - if (GNUNET_OK != - parse_order_outputs (oc, - choice, - joutputs)) - return; } oc->phase++; } @@ -4175,7 +4182,6 @@ TMH_private_post_orders ( parse_choices (oc); break; #ifdef HAVE_DONAU_DONAU_SERVICE_H - // TODO: PLACE HOLDER FOR THE PARSE DONAU INSTANCES, maybe it needs to be lower in the order case ORDER_PHASE_PARSE_DONAU: parse_donau_instances (oc); break; diff --git a/src/backend/taler-merchant-httpd_statics.c b/src/backend/taler-merchant-httpd_statics.c @@ -76,6 +76,7 @@ static const struct TVE * lookup_file (struct MHD_Connection *connection, const char *name) { + double best_q = 0.0; struct TVE *best = NULL; const char *lang; @@ -87,17 +88,19 @@ lookup_file (struct MHD_Connection *connection, /* find best match by language */ for (unsigned int i = 0; i<loaded_length; i++) { + double q; + if (0 != strcmp (loaded[i].name, name)) continue; /* does not match by name */ if (NULL == loaded[i].lang) /* no language == always best match */ return &loaded[i]; - if ( (NULL == best) || - (TALER_language_matches (lang, - loaded[i].lang) > - TALER_language_matches (lang, - best->lang) ) ) - best = &loaded[i]; + q = TALER_pattern_matches (lang, + loaded[i].lang); + if (q < best_q) + continue; + best_q = q; + best = &loaded[i]; } if (NULL == best) { diff --git a/src/backend/taler-merchant-kyccheck.c b/src/backend/taler-merchant-kyccheck.c @@ -715,30 +715,14 @@ is_eligible (const struct TALER_EXCHANGE_Keys *keys, const struct Account *a) { struct TALER_NormalizedPayto np; + bool ret; np = TALER_payto_normalize (a->merchant_account_uri); - /* For all accounts of the exchange */ - for (unsigned int i = 0; i<keys->accounts_len; i++) - { - /* FIXME: move into convenience function in libtalerexchange? See also taler-merchant-httpd_private-get-instances-ID-kyc.c, taler-merchant-httpd_exchanges (!)*/ - const struct TALER_EXCHANGE_WireAccount *account - = &keys->accounts[i]; - - /* KYC auth transfers are never supported with conversion */ - if (NULL != account->conversion_url) - continue; - /* filter by source account by credit_restrictions */ - if (GNUNET_YES != - TALER_EXCHANGE_test_account_allowed (account, - true, /* credit */ - np)) - continue; - /* exchange account is allowed, add it */ - GNUNET_free (np.normalized_payto); - return true; - } + ret = TALER_EXCHANGE_keys_test_account_allowed (keys, + true, + np); GNUNET_free (np.normalized_payto); - return false; + return ret; } diff --git a/src/backend/taler-merchant-reconciliation.c b/src/backend/taler-merchant-reconciliation.c @@ -662,7 +662,7 @@ check_transfer (void *cls, /* Build the `TrackTransferConflictDetails` */ ctc->ec = TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS; ctc->failure = true; - /* FIXME: this should be reported to the auditor (once the auditor has an API for this) */ + /* FIXME-#9426: this should be reported to the auditor (once the auditor has an API for this) */ return; } if ( (GNUNET_OK != @@ -684,7 +684,7 @@ check_transfer (void *cls, /* Build the `TrackTransferConflictDetails` */ ctc->ec = TALER_EC_MERCHANT_PRIVATE_POST_TRANSFERS_CONFLICTING_REPORTS; ctc->failure = true; - /* FIXME: this should be reported to the auditor (once the auditor has an API for this) */ + /* FIXME-#9426: this should be reported to the auditor (once the auditor has an API for this) */ return; } ctc->check_transfer_result = GNUNET_OK; diff --git a/src/backenddb/Makefile.am b/src/backenddb/Makefile.am @@ -75,131 +75,131 @@ libtalermerchantdb_la_LDFLAGS = \ -no-undefined libtaler_plugin_merchantdb_postgres_la_SOURCES = \ - pg_update_wirewatch_progress.h pg_update_wirewatch_progress.c \ - pg_select_wirewatch_accounts.h pg_select_wirewatch_accounts.c \ - pg_insert_account.h pg_insert_account.c \ - pg_update_account.h pg_update_account.c \ - pg_insert_deposit_to_transfer.h pg_insert_deposit_to_transfer.c \ + pg_account_kyc_get_status.h pg_account_kyc_get_status.c \ pg_account_kyc_set_failed.h pg_account_kyc_set_failed.c \ - pg_increase_refund.h pg_increase_refund.c \ - pg_insert_transfer.h pg_insert_transfer.c \ - pg_insert_transfer_details.h pg_insert_transfer_details.c \ - pg_store_wire_fee_by_exchange.h pg_store_wire_fee_by_exchange.c \ - pg_select_open_transfers.h pg_select_open_transfers.c \ - pg_lookup_categories.h pg_lookup_categories.c \ - pg_select_category_by_name.h pg_select_category_by_name.c \ - pg_select_category.h pg_select_category.c \ - pg_update_category.h pg_update_category.c \ - pg_insert_category.h pg_insert_category.c \ + pg_account_kyc_set_status.h pg_account_kyc_set_status.c \ + pg_activate_account.h pg_activate_account.c \ + pg_check_transfer_exists.h pg_check_transfer_exists.c \ pg_delete_category.h pg_delete_category.c \ - pg_lookup_instances.h pg_lookup_instances.c \ - pg_lookup_transfers.h pg_lookup_transfers.c \ - pg_update_transfer_status.h pg_update_transfer_status.c \ + pg_delete_contract_terms.h pg_delete_contract_terms.c \ pg_delete_exchange_accounts.h pg_delete_exchange_accounts.c \ - pg_select_accounts_by_exchange.h pg_select_accounts_by_exchange.c \ - pg_set_transfer_status_to_confirmed.h pg_set_transfer_status_to_confirmed.c \ - pg_insert_exchange_account.h pg_insert_exchange_account.c \ - pg_insert_login_token.h pg_insert_login_token.c \ - pg_delete_login_token.h pg_delete_login_token.c \ - pg_select_login_token.h pg_select_login_token.c \ - pg_select_account_by_uri.h pg_select_account_by_uri.c \ - pg_lookup_instance_auth.h pg_lookup_instance_auth.c \ - pg_lookup_pending_deposits.h pg_lookup_pending_deposits.c \ - pg_insert_instance.h pg_insert_instance.c \ - pg_account_kyc_set_status.h pg_account_kyc_set_status.c \ - pg_get_kyc_status.h pg_get_kyc_status.c \ - pg_get_kyc_limits.h pg_get_kyc_limits.c \ - pg_account_kyc_get_status.h pg_account_kyc_get_status.c \ pg_delete_instance_private_key.h pg_delete_instance_private_key.c \ - pg_purge_instance.h pg_purge_instance.c \ - pg_update_instance.h pg_update_instance.c \ - pg_update_deposit_confirmation_status.h pg_update_deposit_confirmation_status.c \ - pg_update_instance_auth.h pg_update_instance_auth.c \ - pg_inactivate_account.h pg_inactivate_account.c \ - pg_activate_account.h pg_activate_account.c \ - pg_insert_otp.h pg_insert_otp.c \ + pg_delete_login_token.h pg_delete_login_token.c \ + pg_delete_order.h pg_delete_order.c \ pg_delete_otp.h pg_delete_otp.c \ - pg_update_otp.h pg_update_otp.c \ - pg_select_otp.h pg_select_otp.c \ - pg_select_otp_serial.h pg_select_otp_serial.c \ - pg_lookup_otp_devices.h pg_lookup_otp_devices.c \ - pg_select_account.h pg_select_account.c \ - pg_select_accounts.h pg_select_accounts.c \ - pg_delete_template.h pg_delete_template.c \ - pg_insert_template.h pg_insert_template.c \ - pg_update_template.h pg_update_template.c \ - pg_lookup_token_family_keys.h pg_lookup_token_family_keys.c \ - pg_lookup_templates.h pg_lookup_templates.c \ - pg_lookup_template.h pg_lookup_template.c \ - pg_lookup_all_products.h pg_lookup_all_products.c \ - pg_lookup_products.h pg_lookup_products.c \ - pg_lookup_product.h pg_lookup_product.c \ + pg_delete_pending_webhook.h pg_delete_pending_webhook.c \ pg_delete_product.h pg_delete_product.c \ - pg_insert_product.h pg_insert_product.c \ - pg_update_product.h pg_update_product.c \ - pg_lock_product.h pg_lock_product.c \ - pg_insert_exchange_keys.h pg_insert_exchange_keys.c \ - pg_select_exchange_keys.h pg_select_exchange_keys.c \ + pg_delete_template.h pg_delete_template.c \ + pg_delete_token_family.h pg_delete_token_family.c \ + pg_delete_transfer.h pg_delete_transfer.c \ + pg_delete_webhook.h pg_delete_webhook.c \ pg_expire_locks.h pg_expire_locks.c \ - pg_delete_order.h pg_delete_order.c \ - pg_lookup_order.h pg_lookup_order.c \ - pg_lookup_order_summary.h pg_lookup_order_summary.c \ - pg_lookup_orders.h pg_lookup_orders.c \ + pg_get_kyc_limits.h pg_get_kyc_limits.c \ + pg_get_kyc_status.h pg_get_kyc_status.c \ + pg_helper.h pg_helper.c \ + pg_inactivate_account.h pg_inactivate_account.c \ + pg_increase_refund.h pg_increase_refund.c \ + pg_insert_account.h pg_insert_account.c \ + pg_insert_category.h pg_insert_category.c \ + pg_insert_contract_terms.h pg_insert_contract_terms.c \ + pg_insert_deposit.h pg_insert_deposit.c \ + pg_insert_deposit_confirmation.h pg_insert_deposit_confirmation.c \ + pg_insert_deposit_to_transfer.h pg_insert_deposit_to_transfer.c \ + pg_insert_exchange_account.h pg_insert_exchange_account.c \ + pg_insert_exchange_keys.h pg_insert_exchange_keys.c \ + pg_insert_exchange_signkey.h pg_insert_exchange_signkey.c \ + pg_insert_instance.h pg_insert_instance.c \ + pg_insert_issued_token.h pg_insert_issued_token.c \ + pg_insert_login_token.h pg_insert_login_token.c \ pg_insert_order.h pg_insert_order.c \ - pg_unlock_inventory.h pg_unlock_inventory.c \ pg_insert_order_lock.h pg_insert_order_lock.c \ - pg_lookup_contract_terms3.h pg_lookup_contract_terms3.c \ - pg_lookup_contract_terms2.h pg_lookup_contract_terms2.c \ + pg_insert_otp.h pg_insert_otp.c \ + pg_insert_pending_webhook.h pg_insert_pending_webhook.c \ + pg_insert_product.h pg_insert_product.c \ + pg_insert_refund_proof.h pg_insert_refund_proof.c \ + pg_insert_spent_token.h pg_insert_spent_token.c \ + pg_insert_template.h pg_insert_template.c \ + pg_insert_token_family.h pg_insert_token_family.c \ + pg_insert_token_family_key.h pg_insert_token_family_key.c \ + pg_insert_transfer.h pg_insert_transfer.c \ + pg_insert_transfer_details.h pg_insert_transfer_details.c \ + pg_insert_webhook.h pg_insert_webhook.c \ + pg_lock_product.h pg_lock_product.c \ + pg_lookup_account.h pg_lookup_account.c \ + pg_lookup_all_products.h pg_lookup_all_products.c \ + pg_lookup_categories.h pg_lookup_categories.c \ pg_lookup_contract_terms.h pg_lookup_contract_terms.c \ - pg_insert_contract_terms.h pg_insert_contract_terms.c \ - pg_update_contract_terms.h pg_update_contract_terms.c \ - pg_delete_contract_terms.h pg_delete_contract_terms.c \ + pg_lookup_contract_terms2.h pg_lookup_contract_terms2.c \ + pg_lookup_contract_terms3.h pg_lookup_contract_terms3.c \ pg_lookup_deposits.h pg_lookup_deposits.c \ - pg_insert_exchange_signkey.h pg_insert_exchange_signkey.c \ - pg_insert_deposit.h pg_insert_deposit.c \ - pg_insert_deposit_confirmation.h pg_insert_deposit_confirmation.c \ - pg_lookup_refunds.h pg_lookup_refunds.c \ - pg_mark_contract_paid.h pg_mark_contract_paid.c \ - pg_refund_coin.h pg_refund_coin.c \ + pg_lookup_deposits_by_contract_and_coin.h pg_lookup_deposits_by_contract_and_coin.c \ + pg_lookup_deposits_by_order.h pg_lookup_deposits_by_order.c \ + pg_lookup_instance_auth.h pg_lookup_instance_auth.c \ + pg_lookup_instances.h pg_lookup_instances.c \ + pg_lookup_order.h pg_lookup_order.c \ + pg_lookup_order_by_fulfillment.h pg_lookup_order_by_fulfillment.c \ pg_lookup_order_status.h pg_lookup_order_status.c \ pg_lookup_order_status_by_serial.h pg_lookup_order_status_by_serial.c \ - pg_lookup_deposits_by_order.h pg_lookup_deposits_by_order.c \ - pg_lookup_transfer_details_by_order.h pg_lookup_transfer_details_by_order.c \ - pg_mark_order_wired.h pg_mark_order_wired.c \ - pg_lookup_refunds_detailed.h pg_lookup_refunds_detailed.c \ - pg_insert_refund_proof.h pg_insert_refund_proof.c \ + pg_lookup_order_summary.h pg_lookup_order_summary.c \ + pg_lookup_orders.h pg_lookup_orders.c \ + pg_lookup_otp_devices.h pg_lookup_otp_devices.c \ + pg_lookup_pending_deposits.h pg_lookup_pending_deposits.c \ + pg_lookup_pending_webhooks.h pg_lookup_pending_webhooks.c \ + pg_lookup_product.h pg_lookup_product.c \ + pg_lookup_products.h pg_lookup_products.c \ pg_lookup_refund_proof.h pg_lookup_refund_proof.c \ - pg_lookup_order_by_fulfillment.h pg_lookup_order_by_fulfillment.c \ - pg_delete_transfer.h pg_delete_transfer.c \ - pg_check_transfer_exists.h pg_check_transfer_exists.c \ - pg_lookup_account.h pg_lookup_account.c \ - pg_lookup_wire_fee.h pg_lookup_wire_fee.c \ - pg_lookup_deposits_by_contract_and_coin.h pg_lookup_deposits_by_contract_and_coin.c \ + pg_lookup_refunds.h pg_lookup_refunds.c \ + pg_lookup_refunds_detailed.h pg_lookup_refunds_detailed.c \ + pg_lookup_spent_tokens_by_order.h pg_lookup_spent_tokens_by_order.c \ + pg_lookup_template.h pg_lookup_template.c \ + pg_lookup_templates.h pg_lookup_templates.c \ + pg_lookup_token_families.h pg_lookup_token_families.c \ + pg_lookup_token_family.h pg_lookup_token_family.c \ + pg_lookup_token_family_key.h pg_lookup_token_family_key.c \ + pg_lookup_token_family_keys.h pg_lookup_token_family_keys.c \ pg_lookup_transfer.h pg_lookup_transfer.c \ - pg_lookup_transfer_summary.h pg_lookup_transfer_summary.c \ pg_lookup_transfer_details.h pg_lookup_transfer_details.c \ - pg_lookup_webhooks.h pg_lookup_webhooks.c \ + pg_lookup_transfer_details_by_order.h pg_lookup_transfer_details_by_order.c \ + pg_lookup_transfer_summary.h pg_lookup_transfer_summary.c \ + pg_lookup_transfers.h pg_lookup_transfers.c \ pg_lookup_webhook.h pg_lookup_webhook.c \ - pg_delete_webhook.h pg_delete_webhook.c \ - pg_insert_webhook.h pg_insert_webhook.c \ - pg_update_webhook.h pg_update_webhook.c \ pg_lookup_webhook_by_event.h pg_lookup_webhook_by_event.c \ - pg_delete_pending_webhook.h pg_delete_pending_webhook.c \ - pg_insert_pending_webhook.h pg_insert_pending_webhook.c \ + pg_lookup_webhooks.h pg_lookup_webhooks.c \ + pg_lookup_wire_fee.h pg_lookup_wire_fee.c \ + pg_mark_contract_paid.h pg_mark_contract_paid.c \ + pg_mark_order_wired.h pg_mark_order_wired.c \ + pg_purge_instance.h pg_purge_instance.c \ + pg_refund_coin.h pg_refund_coin.c \ + pg_select_account.h pg_select_account.c \ + pg_select_account_by_uri.h pg_select_account_by_uri.c \ + pg_select_accounts.h pg_select_accounts.c \ + pg_select_accounts_by_exchange.h pg_select_accounts_by_exchange.c \ + pg_select_category.h pg_select_category.c \ + pg_select_category_by_name.h pg_select_category_by_name.c \ + pg_select_exchange_keys.h pg_select_exchange_keys.c \ + pg_select_login_token.h pg_select_login_token.c \ + pg_select_open_transfers.h pg_select_open_transfers.c \ + pg_select_otp.h pg_select_otp.c \ + pg_select_otp_serial.h pg_select_otp_serial.c \ + pg_select_wirewatch_accounts.h pg_select_wirewatch_accounts.c \ + pg_set_transfer_status_to_confirmed.h pg_set_transfer_status_to_confirmed.c \ + pg_store_wire_fee_by_exchange.h pg_store_wire_fee_by_exchange.c \ + pg_unlock_inventory.h pg_unlock_inventory.c \ + pg_update_account.h pg_update_account.c \ + pg_update_category.h pg_update_category.c \ + pg_update_contract_terms.h pg_update_contract_terms.c \ + pg_update_deposit_confirmation_status.h pg_update_deposit_confirmation_status.c \ + pg_update_instance.h pg_update_instance.c \ + pg_update_instance_auth.h pg_update_instance_auth.c \ + pg_update_otp.h pg_update_otp.c \ pg_update_pending_webhook.h pg_update_pending_webhook.c \ - pg_lookup_pending_webhooks.h pg_lookup_pending_webhooks.c \ - pg_insert_token_family.h pg_insert_token_family.c \ - pg_lookup_token_family.h pg_lookup_token_family.c \ - pg_lookup_token_families.h pg_lookup_token_families.c \ - pg_delete_token_family.h pg_delete_token_family.c \ + pg_update_product.h pg_update_product.c \ + pg_update_template.h pg_update_template.c \ pg_update_token_family.h pg_update_token_family.c \ - pg_insert_token_family_key.h pg_insert_token_family_key.c \ - pg_lookup_token_family_key.h pg_lookup_token_family_key.c \ - pg_insert_spent_token.h pg_insert_spent_token.c \ - pg_insert_issued_token.h pg_insert_issued_token.c \ - pg_lookup_spent_tokens_by_order.h pg_lookup_spent_tokens_by_order.c \ - plugin_merchantdb_postgres.c \ - pg_helper.h pg_helper.c + pg_update_transfer_status.h pg_update_transfer_status.c \ + pg_update_webhook.h pg_update_webhook.c \ + pg_update_wirewatch_progress.h pg_update_wirewatch_progress.c \ + plugin_merchantdb_postgres.c if HAVE_DONAU libtaler_plugin_merchantdb_postgres_la_SOURCES += \ diff --git a/src/backenddb/merchantdb_helper.c b/src/backenddb/merchantdb_helper.c @@ -88,19 +88,6 @@ TALER_MERCHANTDB_category_details_free ( { GNUNET_free (cd->category_name); json_decref (cd->category_name_i18n); - // FIXME: also return product details - // for (unsigned int i = 0; i<cd->num_products; i++) - // { - // const char* *ps - // = &cd->products[i]; - - // GNUNET_free (ps); - // // GNUNET_free (ps->description); - // // json_decref (ps->description_i18n); - // } - // GNUNET_array_grow (cd->products, - // cd->num_products, - // 0); } diff --git a/src/backenddb/pg_account_kyc_get_status.c b/src/backenddb/pg_account_kyc_get_status.c @@ -189,6 +189,7 @@ TMH_PG_account_kyc_get_status ( " JOIN merchant_kyc mk" " USING (account_serial)" " WHERE (mi.merchant_id=$1)" + " AND ma.active" " AND ( ($2::TEXT IS NULL)" " OR (mk.exchange_url=$2) )" " AND ( ($3::BYTEA IS NULL)" diff --git a/src/backenddb/pg_insert_issued_token.c b/src/backenddb/pg_insert_issued_token.c @@ -27,9 +27,12 @@ enum GNUNET_DB_QueryStatus TMH_PG_insert_issued_token (void *cls, - const struct TALER_PrivateContractHashP *h_contract_terms, - const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub, - const struct TALER_BlindedTokenIssueSignature *blind_sig) + const struct TALER_PrivateContractHashP * + h_contract_terms, + const struct TALER_TokenIssuePublicKeyHashP * + h_issue_pub, + const struct TALER_BlindedTokenIssueSignature * + blind_sig) { struct PostgresClosure *pg = cls; @@ -53,10 +56,8 @@ TMH_PG_insert_issued_token (void *cls, " USING (token_family_serial)" " WHERE h_pub = $1"); - /* TODO: Increase issued counter on merchant_token_family table. */ + /* FIXME-#9434: Increase issued counter on merchant_token_family table. */ return GNUNET_PQ_eval_prepared_non_select (pg->conn, "issued_token_insert", params); - - -} -\ No newline at end of file +} diff --git a/src/backenddb/pg_insert_spent_token.c b/src/backenddb/pg_insert_spent_token.c @@ -49,7 +49,7 @@ TMH_PG_insert_spent_token ( PREPARE (pg, "spent_token_insert", "INSERT INTO merchant_used_tokens" - "(merchant_serial" /* TODO: Remove merchant_serial field from the db, it's already given by token_family.merchant_serial. */ + "(merchant_serial" /* FIXME-#9434: Remove merchant_serial field from the db, it's already given by token_family.merchant_serial. */ ",token_family_key_serial" ",h_contract_terms" ",token_pub" @@ -61,7 +61,7 @@ TMH_PG_insert_spent_token ( " USING (token_family_serial)" " WHERE h_pub = $1"); - /* TODO: Increase used counter on merchant_token_family table. */ + /* FIXME-#9434: Increase used counter on merchant_token_family table. */ return GNUNET_PQ_eval_prepared_non_select (pg->conn, "spent_token_insert", params); diff --git a/src/backenddb/pg_lookup_contract_terms3.c b/src/backenddb/pg_lookup_contract_terms3.c @@ -45,7 +45,7 @@ TMH_PG_lookup_contract_terms3 ( struct PostgresClosure *pg = cls; enum GNUNET_DB_QueryStatus qs; struct TALER_ClaimTokenP ct; - uint16_t ci; + uint16_t ci = 0; bool choice_index_null = false; struct GNUNET_PQ_QueryParam params[] = { GNUNET_PQ_query_param_string (instance_id), @@ -105,7 +105,7 @@ TMH_PG_lookup_contract_terms3 ( if (NULL != claim_token) *claim_token = ct; if (! choice_index_null) - *choice_index = ci; + *choice_index = (int16_t) ci; else *choice_index = -1; return qs; diff --git a/src/backenddb/pg_select_category.c b/src/backenddb/pg_select_category.c @@ -27,12 +27,13 @@ enum GNUNET_DB_QueryStatus -TMH_PG_select_category (void *cls, - const char *instance_id, - uint64_t category_id, - struct TALER_MERCHANTDB_CategoryDetails *cd, - size_t *num_products, - char **products) +TMH_PG_select_category ( + void *cls, + const char *instance_id, + uint64_t category_id, + struct TALER_MERCHANTDB_CategoryDetails *cd, + size_t *num_products, + char **products) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { diff --git a/src/backenddb/pg_select_category_by_name.c b/src/backenddb/pg_select_category_by_name.c @@ -27,11 +27,12 @@ enum GNUNET_DB_QueryStatus -TMH_PG_select_category_by_name (void *cls, - const char *instance_id, - const char *category_name, - json_t **name_i18n, - uint64_t *category_id) +TMH_PG_select_category_by_name ( + void *cls, + const char *instance_id, + const char *category_name, + json_t **name_i18n, + uint64_t *category_id) { struct PostgresClosure *pg = cls; struct GNUNET_PQ_QueryParam params[] = { @@ -47,6 +48,7 @@ TMH_PG_select_category_by_name (void *cls, GNUNET_PQ_result_spec_end }; + check_connection (pg); PREPARE (pg, "select_category_by_name", "SELECT" @@ -57,9 +59,6 @@ TMH_PG_select_category_by_name (void *cls, " USING (merchant_serial)" " WHERE mi.merchant_id=$1" " AND mc.category_name=$2"); - - - check_connection (pg); return GNUNET_PQ_eval_prepared_singleton_select ( pg->conn, "select_category_by_name", diff --git a/src/backenddb/pg_select_exchange_keys.c b/src/backenddb/pg_select_exchange_keys.c @@ -61,8 +61,9 @@ TMH_PG_select_exchange_keys (void *cls, json_decref (jkeys); if (NULL == *keys) { + /* malformed /keys in cache, maybe format change. Just ignore */ GNUNET_break (0); - return GNUNET_DB_STATUS_HARD_ERROR; + return GNUNET_DB_STATUS_SUCCESS_NO_RESULTS; } return qs; } diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h @@ -3759,6 +3759,7 @@ struct TALER_MERCHANT_AbortCoin /** * Amount this coin contributes to (including fee). + * FIXME: no longer needed since **v18**. Remove eventually! */ struct TALER_Amount amount_with_fee; diff --git a/src/include/taler_merchant_util.h b/src/include/taler_merchant_util.h @@ -21,7 +21,10 @@ #ifndef TALER_MERCHANT_UTIL_H #define TALER_MERCHANT_UTIL_H +#include <gnunet/gnunet_common.h> #include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <stdint.h> #include <taler/taler_util.h> #include <jansson.h> @@ -31,7 +34,6 @@ const struct GNUNET_OS_ProjectData * TALER_MERCHANT_project_data (void); - /** * Possible versions of the contract terms. */ @@ -58,16 +60,20 @@ enum TALER_TALER_MERCHANT_ContractTokenKind enum TALER_MERCHANT_ContractTokenKind >>>>>>> master { + /** + * Token kind invalid + */ + TALER_MERCHANT_CONTRACT_TOKEN_KIND_INVALID = 0, /** * Subscription token kind */ - TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION = 0, + TALER_MERCHANT_CONTRACT_TOKEN_KIND_SUBSCRIPTION = 1, /** * Discount token kind */ - TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT = 1 + TALER_MERCHANT_CONTRACT_TOKEN_KIND_DISCOUNT = 2, }; /** @@ -81,11 +87,12 @@ enum TALER_MERCHANT_ContractInputType */ TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID = 0, +#if FUTURE /** * Input type coin */ TALER_MERCHANT_CONTRACT_INPUT_TYPE_COIN = 1, - +#endif /** * Input type token */ @@ -210,7 +217,7 @@ struct TALER_MERCHANT_ContractOutput struct TALER_Amount amount; /** - * TODO: Check if this block is possible to opperate without default placeholder... + * TODO: Check if this block is possible to operate without default placeholder... * Is the donation receipt required? */ bool receipt_required; @@ -218,7 +225,13 @@ struct TALER_MERCHANT_ContractOutput /** * Base URLs of the donation authorities that will issue the tax receipt. */ - // const char *donau_url; + const char **donau_urls; + + /* + * Length of the @e donau_urls array. + */ + unsigned int donau_urls_len; + } donation_receipt; /** @@ -229,7 +242,7 @@ struct TALER_MERCHANT_ContractOutput /** * Slug of the token family to be issued. */ - char *token_family_slug; + const char *token_family_slug; /** * Index of the public key in the @a token_family_slug's token family @@ -382,7 +395,7 @@ struct TALER_MERCHANT_ContractTokenFamily /** * Length of the @e trusted_domains array. */ - size_t trusted_domains_len; + unsigned int trusted_domains_len; } subscription; /** @@ -531,6 +544,30 @@ struct TALER_MERCHANT_Contract */ struct GNUNET_TIME_Timestamp delivery_date; + + /** + * Merchant public key. + */ + struct TALER_MerchantPublicKeyP merchant_pub; + + /** + * The hash of the merchant instance's wire details. + * TODO: appropriate type + */ + struct TALER_MerchantWireHashP h_wire; + + /** + * Wire transfer method identifier for the wire method associated with + h_wire. + */ + char *wire_method; + + /** + * Exchanges that the merchant accepts even if it does not accept any auditors that audit them. + * TODO: appropriate type + */ + json_t *exchanges; + /** * Delivery location. */ @@ -548,6 +585,11 @@ struct TALER_MERCHANT_Contract json_t *extra; /** + * Minimum age the buyer must have (in years). + */ + uint8_t minimum_age; + + /** * Specified version of the contract. */ enum TALER_MERCHANT_ContractVersion version; @@ -611,9 +653,119 @@ struct TALER_MERCHANT_Contract } details; - // FIXME: Add exchanges array? - }; +/** + * 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 TALER_MERCHANT_Contract * +TALER_MERCHANT_contract_parse (json_t *input, + bool nonce_optional); + + +/** + * 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 + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_choice_input ( + json_t *root, + struct TALER_MERCHANT_ContractInput *input, + size_t index, + bool order); + + +/** + * Parse JSON contract terms choice output. + * + * @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 + */ +enum GNUNET_GenericReturnValue +TALER_MERCHANT_parse_choice_output ( + json_t *root, + struct TALER_MERCHANT_ContractOutput *output, + size_t index, + bool order); + + +/** + * Serialize contract terms 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_contract_serialize ( + const struct TALER_MERCHANT_Contract *input, + bool nonce_optional); + + +/** + * Get JSON representation of contract 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); + + +/** + * Get JSON representation of contract token family. + * + * @param[in] family contract token family to serialize + * @return JSON representation of @a family; NULL on error + */ +json_t * +TALER_MERCHANT_json_from_token_family ( + const struct TALER_MERCHANT_ContractTokenFamily *family); + + +/** + * Find token family in contract terms from slug and validity date. + * + * @param slug slug of the token family + * @param valid_after validity start of the token family + * @param[in] families array of token families in the contract terms + * @param families_len length of @a families array + * @param[out] family matching token family; NULL if no result + * @param[out] key key of matching token family; NULL if no result + * @return #GNUNET_ERR if no matching family found; #GNUNET_OK otherwise + */ +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); + + +/** + * Free the @a contract and all fields in it. + * + * @param[in] contract contract to free + */ +void +TALER_MERCHANT_contract_free (struct TALER_MERCHANT_Contract *contract); + #endif diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h @@ -530,28 +530,6 @@ typedef void /** * Details about a product category. */ -struct TALER_MERCHANTDB_ProductSummary -{ - /** - * ID of the product. - */ - char *product_id; - - /** - * Description for the product. - */ - char *description; - - /** - * Translation of the @e description. - */ - json_t *description_i18n; - -}; - -/** - * Details about a product category. - */ struct TALER_MERCHANTDB_CategoryDetails { @@ -565,16 +543,6 @@ struct TALER_MERCHANTDB_CategoryDetails */ json_t *category_name_i18n; - /** - * Products in the category. - */ - struct TALER_MERCHANTDB_ProductSummary *products; - - /** - * Length of the @e products array. - */ - unsigned int num_products; - }; diff --git a/src/lib/merchant_api_get_config.c b/src/lib/merchant_api_get_config.c @@ -34,12 +34,12 @@ * Which version of the Taler protocol is implemented * by this library? Used to determine compatibility. */ -#define MERCHANT_PROTOCOL_CURRENT 17 +#define MERCHANT_PROTOCOL_CURRENT 18 /** * How many configs are we backwards-compatible with? */ -#define MERCHANT_PROTOCOL_AGE 5 +#define MERCHANT_PROTOCOL_AGE 6 /** * How many exchanges do we allow at most per merchant? diff --git a/src/lib/merchant_api_post_order_abort.c b/src/lib/merchant_api_post_order_abort.c @@ -341,6 +341,7 @@ TALER_MERCHANT_order_abort ( j_coin = GNUNET_JSON_PACK ( GNUNET_JSON_pack_data_auto ("coin_pub", &ac->coin_pub), + /* FIXME: no longer needed since **v18**, remove eventually! */ TALER_JSON_pack_amount ("contribution", &ac->amount_with_fee), GNUNET_JSON_pack_string ("exchange_url", @@ -417,7 +418,8 @@ TALER_MERCHANT_order_abort ( void -TALER_MERCHANT_order_abort_cancel (struct TALER_MERCHANT_OrderAbortHandle *oah) +TALER_MERCHANT_order_abort_cancel ( + struct TALER_MERCHANT_OrderAbortHandle *oah) { if (NULL != oah->job) { diff --git a/src/merchant-tools/benchmark-cs.conf b/src/merchant-tools/benchmark-cs.conf @@ -4,7 +4,7 @@ [exchange-account-test] # What is the bank account (with the "Taler Bank" demo system)? Must end with "/". -PAYTO_URI = "payto://x-taler-bank/localhost/Exchange" +PAYTO_URI = "payto://x-taler-bank/localhost/Exchange?receiver-name=Exchange" # Authentication information for basic authentication ENABLE_DEBIT = YES ENABLE_CREDIT = YES diff --git a/src/merchant-tools/benchmark-rsa.conf b/src/merchant-tools/benchmark-rsa.conf @@ -4,7 +4,7 @@ [exchange-account-test] # What is the bank account (with the "Taler Bank" demo system)? Must end with "/". -PAYTO_URI = "payto://x-taler-bank/localhost/Exchange" +PAYTO_URI = "payto://x-taler-bank/localhost/Exchange?receiver-name=Exchange" # Authentication information for basic authentication ENABLE_DEBIT = YES ENABLE_CREDIT = YES diff --git a/src/testing/test_key_rotation.conf b/src/testing/test_key_rotation.conf @@ -76,7 +76,7 @@ MAX_DEBT = TESTKUDOS:50.0 MAX_DEBT_BANK = TESTKUDOS:100000.0 HTTP_PORT = 8082 SUGGESTED_EXCHANGE = http://localhost:8081/ -SUGGESTED_EXCHANGE_PAYTO = payto://x-taler-bank/localhost/2 +SUGGESTED_EXCHANGE_PAYTO = payto://x-taler-bank/localhost/2?receiver-name=Exchange ALLOW_REGISTRATIONS = YES SERVE = tcp @@ -85,7 +85,7 @@ IDLE_RESERVE_EXPIRATION_TIME = 4 weeks LEGAL_RESERVE_EXPIRATION_TIME = 7 years [exchange-account-1] -PAYTO_URI = payto://x-taler-bank/localhost/Exchange +PAYTO_URI = payto://x-taler-bank/localhost/Exchange?receiver-name=Exchange enable_debit = yes enable_credit = yes diff --git a/src/testing/test_merchant_order_creation.sh b/src/testing/test_merchant_order_creation.sh @@ -194,7 +194,7 @@ echo "OK" echo -n "Checking created order without TOKEN..." STATUS=$(curl http://localhost:9966/orders/"$ORDER_ID" \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") + -w "%{http_code}" -s -o "$LAST_RESPONSE") PAY_URI=$(jq -r .taler_pay_uri < "$LAST_RESPONSE") if [ "$PAY_URI" == "null" ] then @@ -246,31 +246,45 @@ fi echo "OK" # -# CREATE TOKEN FAMILY AND V1 ORDER WITH CHOICES +# CREATE A DISCOUNT TOKEN FAMILY # -echo -n "Creating token family ..." -export NOW=$(date +%s) -export IN_A_YEAR=$((NOW + 31536000)) -export LAST_RESPONSE - +echo -n "Creating discount token family..." +VALID_AFTER="{\"t_s\": $(date +%s)}" # now +VALID_BEFORE="{\"t_s\": $(date +%s -d "+30 days")}" # 30 days from now +DURATION="{\"d_us\": $(echo '30 * 24 * 60 * 60 * 1000000' | bc)}" # 30 days STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ - -d '{"slug":"test-sub","kind":"subscription","description":"Test token family","name":"Test Subscription","valid_after":{"t_s":'${NOW}'},"valid_before":{"t_s":'${IN_A_YEAR}'},"duration": {"d_us": 2592000000000},"validity_granularity": {"d_us": 86400000000}}' \ - -w "%{http_code}" -s -o "$LAST_RESPONSE") - + -d "{\"kind\": \"discount\", \"slug\":\"test-discount\", \"name\": \"Test discount\", \"description\": \"Less money $$\", \"description_i18n\": {\"en\": \"Less money $$\", \"es\": \"Menos dinero $$\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $DURATION}" \ + -w "%{http_code}" -s -o /dev/null) if [ "$STATUS" != "204" ] then - cat "$LAST_RESPONSE" >&2 - exit_fail "Expected 204, token family created. got: $STATUS" + exit_fail "Expected '204 OK' response. Got instead $STATUS" fi +echo "Ok" -echo " OK" +# +# CREATE A SUBSCRIPTION TOKEN FAMILY +# +echo -n "Creating subscription token family..." +VALID_AFTER="{\"t_s\": $(date +%s)}" # now +VALID_BEFORE="{\"t_s\": $(date +%s -d "+30 days")}" # 30 days from now +DURATION="{\"d_us\": $(echo '30 * 24 * 60 * 60 * 1000000' | bc)}" # 30 days +STATUS=$(curl 'http://localhost:9966/private/tokenfamilies' \ + -d "{\"kind\": \"subscription\", \"slug\":\"test-subscription\", \"name\": \"Test subscription\", \"description\": \"Money per month\", \"description_i18n\": {\"en\": \"Money $$$ per month\", \"es\": \"Dinero $$$ al mes\"}, \"valid_after\": $VALID_AFTER, \"valid_before\": $VALID_BEFORE, \"duration\": $DURATION, \"validity_granularity\": $DURATION}" \ + -w "%{http_code}" -s -o /dev/null) +if [ "$STATUS" != "204" ] +then + exit_fail "Expected '204 OK' response. Got instead $STATUS" +fi +echo "Ok" -echo "curl 'http://localhost:9966/private/orders' \ - -d '{\"order\":{\"version\":1,\"summary\":\"with_subscription\",\"fulfillment_message\":\"Paid successfully\",\"choices\":[\{\"amount\":\"TESTKUDOS:7","inputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}],\"outputs\":[{\"kind\":\"token\",\"count\":1,\"token_family_slug\":\"test-sub\",\"valid_after\":{\"t_s\":'$NOW'}}]}]}}'" +# +# CREATE AN DISCOUNTABLE ORDER WITHOUT TOKEN +# +RANDOM_IMG='data:image/png;base64,abcdefg' -echo -n "Creating v1 order with token family ..." +echo -n "Creating discountable order..." STATUS=$(curl 'http://localhost:9966/private/orders' \ - -d '{"order":{"version":1,"summary":"with_subscription","fulfillment_message":"Paid successfully","choices":[{"amount":"TESTKUDOS:7","inputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}],"outputs":[{"kind":"token","count":1,"token_family_slug":"test-sub","valid_after":{"t_s":'$NOW'}}]}]}}' \ + -d '{"create_token":true,"order":{"version":1,"summary":"Expensive purchase","products":[{"description":"Expensive steak","quantity":2,"unit":"pieces","price":"TESTKUDOS:100"}],"choices":[{"amount":"TESTKUDOS:100"},{"amount":"TESTKUDOS:10","inputs":[{"type":"token","token_family_slug":"test-discount","count":1}],"outputs":[]}]}}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") if [ "$STATUS" != "200" ] @@ -278,14 +292,23 @@ then cat "$LAST_RESPONSE" >&2 exit_fail "Expected 200, order created. got: $STATUS" fi - -echo " OK" - -echo -n "Claming order with token family ..." +echo "OK" ORDER_ID=$(jq -r .order_id < "$LAST_RESPONSE") TOKEN=$(jq -r .token < "$LAST_RESPONSE") +echo -n "Checking created order..." +STATUS=$(curl http://localhost:9966/orders/"$ORDER_ID?token=$TOKEN" \ + -w "%{http_code}" -s -o "$LAST_RESPONSE") +PAY_URI=$(jq -r .taler_pay_uri < "$LAST_RESPONSE") +if [ "$PAY_URI" == "null" ] +then + cat "$LAST_RESPONSE" >&2 + exit_fail "Expected non-NULL payuri. got $PAY_URI" +fi +echo "OK" + +echo -n "Claming order with token family ..." STATUS=$(curl http://localhost:9966/orders/"$ORDER_ID"/claim \ -d '{"nonce":"","token":"'"$TOKEN"'"}' \ -w "%{http_code}" -s -o "$LAST_RESPONSE") diff --git a/src/testing/testing_api_cmd_pay_order.c b/src/testing/testing_api_cmd_pay_order.c @@ -153,6 +153,7 @@ find_token_public_key (const json_t *token_families, const char *slug, unsigned int key_index, struct TALER_TokenIssuePublicKey *pub) + { const json_t *tf = json_object_get (token_families, slug); const json_t *keys; @@ -165,7 +166,7 @@ find_token_public_key (const json_t *token_families, const char *error_name; unsigned int error_line; struct GNUNET_JSON_Specification ispec[] = { - TALER_JSON_spec_token_pub ("public_key", + TALER_JSON_spec_token_pub (NULL, pub), GNUNET_JSON_spec_end () }; @@ -261,7 +262,7 @@ build_coins (struct TALER_MERCHANT_PayCoin **pc, /* Token syntax is "LABEL[/NUMBER]" */ ctok = strchr (token, '/'); - // TODO: Check why ci variable is parsed but not used? + /* FIXME: Check why ci variable is parsed but not used? */ ci = 0; if (NULL != ctok) { @@ -755,7 +756,7 @@ pay_run (void *cls, unsigned int ierror_line = 0; struct GNUNET_JSON_Specification ispec[] = { - GNUNET_JSON_spec_string ("kind", + GNUNET_JSON_spec_string ("type", &kind), GNUNET_JSON_spec_string ("token_family_slug", &slug), @@ -813,7 +814,7 @@ pay_run (void *cls, TALER_token_blind_input_copy (&details->blinding_inputs, TALER_token_blind_input_rsa_singleton () ); - /* TODO: Where to get details->blinding_inputs from? */ + /* FIXME: Where to get details->blinding_inputs from? */ TALER_token_use_setup_random (&details->master); TALER_token_use_setup_priv (&details->master, &details->blinding_inputs, @@ -831,7 +832,7 @@ pay_run (void *cls, ( details->issue_pub.public_key, &details->blinding_secret, - NULL, /* TODO: Add session nonce to support CS tokens */ + NULL, /* FIXME: Add session nonce to support CS tokens */ &details->h_token_pub.hash, sizeof (details->h_token_pub.hash), details->blinding_inputs.blinding_inputs); diff --git a/src/testing/testing_api_cmd_post_orders.c b/src/testing/testing_api_cmd_post_orders.c @@ -916,7 +916,7 @@ TALER_TESTING_cmd_merchant_post_orders_choices ( json_array_append_new ( inputs, GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("kind", + GNUNET_JSON_pack_string ("type", "token"), GNUNET_JSON_pack_uint64 ("count", num_inputs), @@ -929,7 +929,7 @@ TALER_TESTING_cmd_merchant_post_orders_choices ( json_array_append_new ( outputs, GNUNET_JSON_PACK ( - GNUNET_JSON_pack_string ("kind", + GNUNET_JSON_pack_string ("type", "token"), GNUNET_JSON_pack_uint64 ("count", num_outputs), diff --git a/src/util/Makefile.am b/src/util/Makefile.am @@ -10,11 +10,20 @@ endif pkgcfgdir = $(prefix)/share/taler-merchant/config.d/ pkgcfg_DATA = \ + currencies.conf \ merchant-paths.conf bin_PROGRAMS = \ taler-merchant-config +AM_TESTS_ENVIRONMENT=export TALER_PREFIX=$${TALER_PREFIX:-@libdir@};export PATH=$${TALER_PREFIX:-@prefix@}/bin:$$PATH; + +check_PROGRAMS = \ + test_contract + +TESTS = \ + $(check_PROGRAMS) + taler_merchant_config_SOURCES = \ taler-merchant-config.c taler_merchant_config_LDADD = \ @@ -30,10 +39,21 @@ lib_LTLIBRARIES = \ libtalermerchantutil.la libtalermerchantutil_la_SOURCES = \ + contract_parse.c \ + contract_serialize.c \ os_installation.c libtalermerchantutil_la_LIBADD = \ + -lgnunetjson \ -lgnunetutil \ + -ltalerjson \ + -ltalerutil \ $(XLIB) libtalermerchantutil_la_LDFLAGS = \ -version-info 0:0:0 \ -export-dynamic -no-undefined + +test_contract_SOURCES = \ + test_contract.c +test_contract_LDADD = \ + -lgnunetutil \ + libtalermerchantutil.la diff --git a/src/util/contract_parse.c b/src/util/contract_parse.c @@ -0,0 +1,1313 @@ +/* + This file is part of TALER + (C) 2024 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 util/contract_parse.c + * @brief shared logic for contract terms parsing + * @author Iván Ávalos + */ +#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_merchant_util.h" + + +/** + * Parse merchant details 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_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 input type string. + * + * @param str contract input type string + * @return enum value of input type + */ +static enum TALER_MERCHANT_ContractInputType +contract_input_type_from_string (const char *str) +{ + /* For now, only 'token' is the only supported option. */ + if (0 == strcmp ("token", + str)) + return TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN; + return TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID; +} + + +/** + * Get enum value from contract output type string. + * + * @param str contract output type string + * @return enum value of output type + */ +static enum TALER_MERCHANT_ContractOutputType +contract_output_type_from_string (const char *str) +{ + if (0 == strcmp ("token", + str)) + return TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_TOKEN; + if (0 == strcmp ("tax-receipt", + str)) + return TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_DONATION_RECEIPT; + return TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID; +} + + +/** + * 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 *type; + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("type", + &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; + } + + input->type = contract_input_type_from_string (type); + + switch (input->type) + { + case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID: + 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 *type; + const char *ename; + unsigned int eline; + struct GNUNET_JSON_Specification ispec[] = { + GNUNET_JSON_spec_string ("type", + &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; + } + + output->type = contract_output_type_from_string (type); + + switch (output->type) + { + case TALER_MERCHANT_CONTRACT_OUTPUT_TYPE_INVALID: + 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), + (! 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; + 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] = + 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; + } + + 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; + const json_t *joutputs; + + struct GNUNET_JSON_Specification spec[] = { + TALER_JSON_spec_amount_any ("amount", + &choice->amount), + TALER_JSON_spec_amount_any ("max_fee", + &choice->max_fee), + GNUNET_JSON_spec_array_const ("inputs", + &jinputs), + GNUNET_JSON_spec_array_const ("outputs", + &joutputs), + 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; + } + + { + 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)) + 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); + } + } + + { + 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)) + 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; +} + + +/** + * Provide specification to parse given JSON array to contract terms + * choices. All fields from @a choices elements are copied. + * + * @param name name of the choices field in the JSON + * @param[out] choices where the contract choices array has to be written + * @param[out] choices_len length of the @a choices array + */ +static struct GNUNET_JSON_Specification +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; +} + + +/** + * 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 + */ +static void +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]; + + GNUNET_free (output->details.coin.exchange_url); + } + 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] 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; +} + + +struct GNUNET_JSON_Specification +TALER_MERCHANT_JSON_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 + * @param[out] contract where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +enum GNUNET_GenericReturnValue +parse_contract_v0 ( + json_t *input, + struct TALER_MERCHANT_Contract *contract) +{ + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount_any ("amount", + &contract->details.v0.brutto), + TALER_JSON_spec_amount_any ("max_fee", + &contract->details.v0.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 contract v0 at field %s\n", + ename); + return GNUNET_SYSERR; + } + + if (GNUNET_OK != + TALER_amount_cmp_currency (&contract->details.v0.max_fee, + &contract->details.v0.brutto)) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "'max_fee' in database does not match currency of contract price"); + return GNUNET_SYSERR; + } + + return res; +} + + +/** + * Parse v1-specific fields of @a input JSON into @a contract. + * + * @param[in] input the JSON contract terms + * @param[out] contract where to write the data + * @return #GNUNET_OK upon successful parsing; #GNUNET_SYSERR upon error + */ +enum GNUNET_GenericReturnValue +parse_contract_v1 ( + json_t *input, + struct TALER_MERCHANT_Contract *contract) +{ + struct GNUNET_JSON_Specification espec[] = { + spec_choices ("choices", + &contract->details.v1.choices, + &contract->details.v1.choices_len), + spec_token_families ("token_families", + &contract->details.v1.token_authorities, + &contract->details.v1. + token_authorities_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 contract v1 at field %s\n", + ename); + return GNUNET_SYSERR; + } + + return res; +} + + +struct TALER_MERCHANT_Contract * +TALER_MERCHANT_contract_parse (json_t *input, + bool nonce_optional) +{ + struct TALER_MERCHANT_Contract *contract; + contract = GNUNET_new (struct TALER_MERCHANT_Contract); + + struct GNUNET_JSON_Specification espec[] = { + TALER_MERCHANT_JSON_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_array_copy ("products", + &contract->products), + 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), + (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_end () + }; + + enum GNUNET_GenericReturnValue res; + const char *ename; + unsigned int eline; + + GNUNET_assert (NULL != input); + res = GNUNET_JSON_parse (input, + espec, + &ename, + &eline); + if (GNUNET_OK != res) + { + GNUNET_break (0); + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to parse contract at field %s\n", + ename); + 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: + TALER_MERCHANT_contract_free (contract); + return NULL; +} + + +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) + { + json_decref (contract->products); + contract->products = NULL; + } + GNUNET_free (contract->wire_method); + if (NULL != contract->exchanges) + { + json_decref (contract->exchanges); + contract->exchanges = NULL; + } + if (NULL != contract->delivery_location) + { + json_decref (contract->delivery_location); + contract->delivery_location = 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++) + 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 @@ -0,0 +1,474 @@ +/* + This file is part of GNU Taler + Copyright (C) 2024 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 util/contract_serialize.c + * @brief shared logic for contract terms serialization + * @author Iván Ávalos + */ + +#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_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 (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), + (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_steal ("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. + * + * @param[in] input contract terms v0 + * @return JSON object with @a input v0 fields; NULL on error + */ +static json_t * +json_from_contract_v0 ( + const struct TALER_MERCHANT_Contract *input) +{ + return GNUNET_JSON_PACK ( + TALER_JSON_pack_amount ("amount", + &input->details.v0.brutto), + TALER_JSON_pack_amount ("max_fee", + &input->details.v0.max_fee)); +} + + +/** + * Get JSON object with contract terms v1-specific fields. + * + * @param[in] input contract terms v1 + * @return JSON object with @a input v1 fields; NULL on error + */ +static json_t * +json_from_contract_v1 ( + const struct TALER_MERCHANT_Contract *input) +{ + json_t *choices; + json_t *families; + + choices = json_array (); + GNUNET_assert (0 != choices); + for (unsigned i = 0; i < input->details.v1.choices_len; i++) + GNUNET_assert (0 == json_array_append_new ( + choices, + TALER_MERCHANT_json_from_contract_choice ( + &input->details.v1.choices[i], + false))); + + families = json_object (); + GNUNET_assert (0 != families); + for (unsigned i = 0; i < input->details.v1.token_authorities_len; i++) + GNUNET_assert (0 == json_object_set_new ( + families, + input->details.v1.token_authorities[i].slug, + TALER_MERCHANT_json_from_token_family ( + &input->details.v1.token_authorities[i]))); + + return GNUNET_JSON_PACK ( + GNUNET_JSON_pack_array_steal ("choices", + choices), + GNUNET_JSON_pack_object_steal ("token_families", + families)); +} + + +json_t * +TALER_MERCHANT_contract_serialize ( + const struct TALER_MERCHANT_Contract *input, + bool nonce_optional) +{ + json_t *details; + + switch (input->version) + { + case TALER_MERCHANT_CONTRACT_VERSION_0: + details = json_from_contract_v0 (input); + goto success; + case TALER_MERCHANT_CONTRACT_VERSION_1: + details = json_from_contract_v1 (input); + goto success; + } + + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "unknown contract type version %d", + input->version); + GNUNET_assert (0); + return NULL; + +success: + 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", + input->products), + GNUNET_JSON_pack_timestamp ("timestamp", + input->timestamp), + GNUNET_JSON_pack_timestamp ("refund_deadline", + input->refund_deadline), + GNUNET_JSON_pack_timestamp ("pay_deadline", + input->pay_deadline), + GNUNET_JSON_pack_timestamp ("wire_transfer_deadline", + input->wire_deadline), + GNUNET_JSON_pack_data_auto ("merchant_pub", + &input->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)), + GNUNET_JSON_pack_data_auto ("h_wire", + &input->h_wire), + GNUNET_JSON_pack_string ("wire_method", + input->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)), + (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)), + GNUNET_JSON_pack_object_steal (NULL, + details)); +} diff --git a/src/util/currencies.conf b/src/util/currencies.conf @@ -0,0 +1,89 @@ +[currency-euro] +ENABLED = YES +name = "Euro" +code = "EUR" +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"€"} + +[currency-swiss-francs] +ENABLED = YES +name = "Swiss Francs" +code = "CHF" +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"Fr.","-2":"Rp."} + +[currency-forint] +ENABLED = NO +name = "Hungarian Forint" +code = "HUF" +fractional_input_digits = 0 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 0 +alt_unit_names = {"0":"Ft"} + +[currency-us-dollar] +ENABLED = NO +name = "US Dollar" +code = "USD" +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"$"} + +[currency-kudos] +ENABLED = YES +name = "Kudos (Taler Demonstrator)" +code = "KUDOS" +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"ク"} + +[currency-testkudos] +ENABLED = YES +name = "Test-kudos (Taler Demonstrator)" +code = "TESTKUDOS" +fractional_input_digits = 2 +fractional_normal_digits = 2 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"テ","3":"kテ","-3":"mテ"} + +[currency-japanese-yen] +ENABLED = NO +name = "Japanese Yen" +code = "JPY" +fractional_input_digits = 2 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 2 +alt_unit_names = {"0":"¥"} + +[currency-bitcoin-mainnet] +ENABLED = NO +name = "Bitcoin (Mainnet)" +code = "BITCOINBTC" +fractional_input_digits = 8 +fractional_normal_digits = 3 +fractional_trailing_zero_digits = 0 +alt_unit_names = {"0":"BTC","-3":"mBTC"} + +[currency-ethereum] +ENABLED = NO +name = "WAI-ETHER (Ethereum)" +code = "EthereumWAI" +fractional_input_digits = 0 +fractional_normal_digits = 0 +fractional_trailing_zero_digits = 0 +alt_unit_names = {"0":"WAI","3":"KWAI","6":"MWAI","9":"GWAI","12":"Szabo","15":"Finney","18":"Ether","21":"KEther","24":"MEther"} + +[currency-netzbon] +ENABLED=YES +name=NetzBon +code=NETZBON +fractional_input_digits=2 +fractional_normal_digits=2 +fractional_trailing_zero_digits=2 +alt_unit_names = {"0":"NETZBON"} diff --git a/src/util/test_contract.c b/src/util/test_contract.c @@ -0,0 +1,243 @@ +/* + This file is part of TALER + (C) 2015, 2021 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 util/test_contract.c + * @brief Tests for contract parsing/serializing + * @author Iván Ávalos (ivan@avalos.me) + */ + +#include "platform.h" +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_common.h> +#include <gnunet/gnunet_json_lib.h> +#include "taler_merchant_util.h" +#include <jansson.h> + +const char *contract_common = "{\n" + " \"summary\": \"Test order\",\n" + " \"summary_i18n\": {\n" + " \"en\": \"Test order\",\n" + " \"es\": \"Orden de prueba\"\n" + " },\n" + " \"order_id\": \"test\",\n" + " \"public_reorder_url\": \"https://test.org/reorder\",\n" + " \"fulfillment_url\": \"https://test.org/fulfillment\",\n" + " \"fulfillment_message\": \"Thank you for your purchase!\",\n" + " \"fulfillment_message_i18n\": {\n" + " \"en\": \"Thank you for your purchase!\",\n" + " \"es\": \"¡Gracias por su compra!\"\n" + " },\n" + " \"products\": [],\n" + " \"timestamp\": {\"t_s\": 1736174497},\n" + " \"refund_deadline\": {\"t_s\": 1736174497},\n" + " \"pay_deadline\": {\"t_s\": 1736174497},\n" + " \"wire_transfer_deadline\": {\"t_s\": 1736174497},\n" + " \"merchant_pub\": \"F80MFRG8HVH6R9CQ47KRFQSJP3T6DBJ4K1D9B703RJY3Z39TBMJ0\",\n" + " \"merchant_base_url\": \"https://test.org/merchant\",\n" + " \"merchant\": {\n" + " \"name\": \"Test merchant\",\n" + " \"email\": \"test@test.org\",\n" + " \"website\": \"https://test.org/merchant\",\n" + " \"logo\": \"data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAADRUlEQVRYhe2WXUgUURiG39l1Vt021C2piJAQyhXEXVNKysoSd8nywgLNn1CIMCLposCLxOii8KasC2+CFB1WRVASMpMUKiyIaLXyBwmhoh9Kzc1W929murAZ9+znZl2EXey5OvOc93zfO+ec+eZwZmuLjFVsmtVMHjYQNvBfGIhYSSAD6LcXYJ1RrzLO1AhzAg9oNHD0FGPBLSJ2bwuSjRE4U2rCydIdmJpxIae4C9wK8blQdUCWgaH7pepzZXUf3rx34eqFXcgwbwIANLUP40bjKzzuLIRBzwMATFY7ojgJL3pL1eRmqwAuhJNlDSiTTQebEcUv7pKjd9GMxSaouj9hHp+M0f4yeH0Sdh6xEwPkDOTu2QzuV4CVkltsAkYnptTxQI3CInkOFpsAHa9Byjbj7w2Ikoy6i9lMIh+nDfmWAFBS1UuYonX6OIY13zwEOWi9GQMv+8pwS3Awgtf3jjPJg5ME9wPZZH8Jw7r7JphzRQwAQIMwovZHZ/wkcF11Jj5/cRHumvfjVFES4Z/mRLVfe+0ZGVcNLHglMjg3eALff3gZlrs/ETnFnUSbdtiO0+XpDHN7RIz1FBKtKC3tg2rAtDUG35xuRqjjNdhd2EUC8BH0m9LraE2rrO5DXEwU4ZJ2qfyos6adXqw1RBKx0bBirQrZJj/MEyaKQKxeu4yBWTcitOybuT0i7t7OJ0G8flq7PD7KepryMb/AniOtFph1eqgB3TLLGpPVAsManmF3escx0HqUaB92HENH9wjDDHoeG7PpFxK4hWTjqspT1H6ykS7/pfrn2BCvJzzeGI0rDQ7CE2OXYtScTSfjjIFUq4CKolRGkJrXxhQZpQWyUOPbc9nSW5CXBLOVXRHGgIYDzl8eYAJqRD9JohSe+toswgK1eo3EsPJzPeSnRLag/8lHdYJy2ILru8L2ZSao/4Tg5ApzeaTFWD4Jw+MzwemWvw9YbAJkGRh5UAZg8ddssQmIitapCeZcXlhsAmqq0lX26Ok7bDlgR17Gesas2SaEvBeEvA8oTZaBgbYCGOP0aG534HrjCIam/XAPliFSp0VaXitkUcTQWx/ksQp13tdpF3JLaBH7awP/uq36nTBsIGxg1Q38BGj1Qe2GKmj3AAAAAElFTkSuQmCC\",\n" + " \"address\": {},\n" // TODO + " \"jurisdiction\": {}\n" // TODO + " },\n" + " \"h_wire\": \"WYEMPXPRA87Y1QBJAJSR5SMB8V3QN2MMHAKDAEN04XC8TQ6TBEJF5KAKT3Y8KKP9W0TW3A2PH1YB22EZJ9TA7HX1P5BFTJQ660GS1TG\",\n" + " \"wire_method\": \"iban\",\n" + " \"exchanges\": [],\n" + " \"delivery_location\": {},\n" // TODO + " \"delivery_date\": {\"t_s\": 1736174497},\n" + " \"nonce\": \"\",\n" // TODO + " \"auto_refund\": {\"d_us\": \"forever\"},\n" + " \"extra\": {\n" + " \"key0\": \"value0\",\n" + " \"key1\": \"value1\",\n" + " \"key2\": 2\n" + " },\n" + " \"minimum_age\": 100\n" + "}"; + +const char *contract_v0 = "{\n" + " \"version\": 0,\n" + " \"amount\": \"KUDOS:10\",\n" + " \"max_fee\": \"KUDOS:0.2\"\n" + "}"; + +const char *contract_v1 = "{\n" + " \"version\": 1,\n" + " \"choices\": [\n" + " {\n" + " \"amount\": \"KUDOS:1\",\n" + " \"max_fee\": \"KUDOS:0.5\",\n" + " \"inputs\": [\n" + " {\n" + " \"type\": \"token\",\n" + " \"token_family_slug\": \"test-subscription\",\n" + " \"count\": 1\n" + " }\n" + " ],\n" + " \"outputs\": [\n" + " {\n" + " \"type\": \"token\",\n" + " \"token_family_slug\": \"test-discount\",\n" + " \"count\": 2,\n" + " \"key_index\": 0\n" // TODO + " },\n" + " {\n" + " \"type\": \"tax-receipt\",\n" + " \"donau_urls\": [\"a\", \"b\", \"c\"],\n" + " \"amount\": \"KUDOS:10\"\n" + " }\n" + " ]\n" + " }\n" + " ],\n" + " \"token_families\": {\n" + " \"test-subscription\": {\n" + " \"name\": \"Test subscription\",\n" + " \"description\": \"This is a test subscription\",\n" + " \"description_i18n\": {\n" + " \"en\": \"This is a test subscription\",\n" + " \"es\": \"Esta es una subscripción de prueba\"\n" + " },\n" + " \"keys\": [\n" + " {\n" + " \"cipher\": \"CS\",\n" + " \"cs_pub\": \"AMGKHAWCF3Y32E64G6JV7TPP7KHE2C3QFMNZ8N66Q744FV3TH1D0\",\n" + " \"signature_validity_start\": {\"t_s\": 1736174497},\n" + " \"signature_validity_end\": {\"t_s\": 1736174497}\n" + " }\n" + " ],\n" + " \"details\": {\n" + " \"class\": \"subscription\",\n" + " \"trusted_domains\": [\"a\", \"b\", \"c\"]\n" + " },\n" + " \"critical\": true\n" + " },\n" + " \"test-discount\": {\n" + " \"name\": \"Test discount\",\n" + " \"description\": \"This is a test discount\",\n" + " \"description_i18n\": {\n" + " \"en\": \"This is a test discount\",\n" + " \"es\": \"Este es un descuento de prueba\"\n" + " },\n" + " \"keys\": [\n" + " {\n" + " \"cipher\": \"RSA\",\n" + " \"rsa_pub\": \"040000YGF5DK0PKCN99J0V814C20Q54C82S3RE3GBVC2T4QXEP7N05ABAN5DG8BC3FTN33BSG15VFX2N9X95HE7GBDAHSYHG4G00VHDCV4E0W4HVYTZGN6SGPBMTAE1XMYBH5DFWT4TXPSEQB96AG3G65X6BPQ0WXSARD5NP2YR1CQB6GB0W2BSKZK1AXZN67GHB3HHAPFV8V584QF1DGDXEWN875RN4HYNH3AW4XZ9SP5A7J5MED56P0TXX5D8C1HPWHFD89GE6Q7J0Q3QKM18WAVAZJTF6PR3Q5T2C71ST0VTP42F16ZZRWS4CHSXHM5RW0BGH383VX4100AD61X6QQ99K12Q17EQZK5MSE6AGNK24SCAH06XTXA7WFC78V0ARJKFDX1M483GE9SX20XXFKSTQ6B8104002\",\n" + " \"signature_validity_start\": {\"t_s\": 1736174497},\n" + " \"signature_validity_end\": {\"t_s\": 1736174497}\n" + " }\n" + " ],\n" + " \"details\": {\n" + " \"class\": \"discount\",\n" + " \"expected_domains\": [\"a\", \"b\", \"c\"]\n" + " },\n" + " \"critical\": true\n" + " }\n" + " }\n" + "}"; + + +int +main (int argc, + const char *const argv[]) +{ + (void) argc; + (void) argv; + GNUNET_log_setup ("test-contract", + "WARNING", + NULL); + + { // Contract v0 + json_t *common; + json_t *v0; + struct TALER_MERCHANT_Contract *v0_parsed; + json_t *v0_serialized; + + common = json_loads (contract_common, 0, NULL); + GNUNET_assert (NULL != common); + + v0 = json_loads (contract_v0, 0, NULL); + GNUNET_assert (NULL != v0); + + GNUNET_assert (0 == json_object_update_new (v0, common)); + + v0_parsed = TALER_MERCHANT_contract_parse (v0, false); + GNUNET_assert (NULL != v0_parsed); + + v0_serialized = TALER_MERCHANT_contract_serialize (v0_parsed, false); + GNUNET_assert (NULL != v0_serialized); + + GNUNET_assert (1 == json_equal (v0, v0_serialized)); + + json_decref (v0_serialized); + TALER_MERCHANT_contract_free (v0_parsed); + json_decref (v0); + } + + { // Contract v1 + json_t *common; + json_t *v1; + struct TALER_MERCHANT_Contract *v1_parsed; + json_t *v1_serialized; + + common = json_loads (contract_common, 0, NULL); + GNUNET_assert (NULL != common); + + v1 = json_loads (contract_v1, 0, NULL); + GNUNET_assert (NULL != v1); + + GNUNET_assert (0 == json_object_update_new (v1, common)); + + v1_parsed = TALER_MERCHANT_contract_parse (v1, false); + GNUNET_assert (NULL != v1_parsed); + + v1_serialized = TALER_MERCHANT_contract_serialize (v1_parsed, false); + 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++) + { + struct TALER_MERCHANT_ContractTokenFamily *in = + &v1_parsed->details.v1.token_authorities[i]; + for (unsigned int j = 0; j < in->keys_len; j++) + { + struct TALER_MERCHANT_ContractTokenFamilyKey *inkey = &in->keys[j]; + struct TALER_MERCHANT_ContractTokenFamily out; + struct TALER_MERCHANT_ContractTokenFamilyKey outkey; + 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, + &out, + &outkey)); + GNUNET_assert (0 == GNUNET_memcmp (in, &out)); + GNUNET_assert (0 == GNUNET_memcmp (inkey, &outkey)); + } + } + + json_decref (v1_serialized); + TALER_MERCHANT_contract_free (v1_parsed); + json_decref (v1); + } + + return 0; +}