commit 1d371b1c68d96fe132d5e6300aace9e219d313ac
parent 9b4df2bedb986919e145f6804d209a7217489a8e
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 29 Jan 2025 17:59:47 +0100
fix #9417 (refactor contract parse/serialize and fix v1 support)
Diffstat:
12 files changed, 2576 insertions(+), 565 deletions(-)
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";
}
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)
@@ -585,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
@@ -619,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);
@@ -642,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);
@@ -682,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)
{
@@ -728,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,
@@ -771,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 = {
@@ -786,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 (
@@ -846,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))
@@ -890,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);
@@ -906,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,
@@ -934,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)
@@ -1014,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);
@@ -1030,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,
@@ -1098,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)));
@@ -1155,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
@@ -1171,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);
@@ -1320,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,
@@ -1469,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",
@@ -1486,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)
{
@@ -1550,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_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"
@@ -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,
@@ -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)
);
@@ -2714,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))
@@ -2744,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;
@@ -2850,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)
{
@@ -2859,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 (
@@ -2901,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,
@@ -3148,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,
@@ -3188,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);
@@ -3217,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);
@@ -3227,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)
+ switch (pc->contract_terms->version)
{
- 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)
- {
- 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);
@@ -3328,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,
@@ -3368,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,
@@ -3386,7 +3211,6 @@ phase_check_contract (struct PayContext *pc)
return;
}
}
- pc->version = TALER_MERCHANT_CONTRACT_VERSION_1;
break;
default:
GNUNET_break (0);
@@ -3402,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);
@@ -3416,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,
@@ -3433,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)
@@ -3871,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++)
{
@@ -3893,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_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"
@@ -1900,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)
- );
-
- GNUNET_assert (0 ==
- json_array_append_new (keys,
- jkey));
- }
+ jfamily = TALER_MERCHANT_json_from_token_family (family);
- /* 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,
@@ -1962,109 +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;
-#if FUTURE
- 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;
-#endif
- 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;
}
@@ -3688,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)
{
@@ -3702,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
@@ -3734,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,
@@ -3778,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;
+ }
- if ( (0 == json_array_size (jinputs)) &&
- (0 == json_array_size (joutputs)) )
+ switch (input.type)
+ {
+ case TALER_MERCHANT_CONTRACT_INPUT_TYPE_INVALID:
+ GNUNET_assert (0);
+ break;
+ case TALER_MERCHANT_CONTRACT_INPUT_TYPE_TOKEN:
+ /* Ignore inputs tokens with 'count' field set to 0 */
+ if (0 == input.details.token.count)
+ continue;
+
+ if (GNUNET_OK !=
+ add_input_token_family (oc,
+ input.details.token.token_family_slug))
+
+ {
+ GNUNET_break_op (0);
+ reply_with_error (oc,
+ MHD_HTTP_BAD_REQUEST,
+ TALER_EC_MERCHANT_PRIVATE_POST_ORDERS_TOKEN_FAMILY_SLUG_UNKNOWN,
+ input.details.token.token_family_slug);
+ return;
+ }
+
+ GNUNET_array_append (choice->inputs,
+ choice->inputs_len,
+ input);
+ continue;
+ }
+ GNUNET_assert (0);
+ }
+ }
+
+ if (NULL != joutputs)
{
- 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++;
}
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.
*/
@@ -54,16 +56,20 @@ enum TALER_MERCHANT_ContractVersion
*/
enum TALER_MERCHANT_ContractTokenKind
{
+ /**
+ * 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,
};
/**
@@ -77,11 +83,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
*/
@@ -214,7 +221,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;
/**
@@ -225,7 +238,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
@@ -378,7 +391,7 @@ struct TALER_MERCHANT_ContractTokenFamily
/**
* Length of the @e trusted_domains array.
*/
- size_t trusted_domains_len;
+ unsigned int trusted_domains_len;
} subscription;
/**
@@ -527,6 +540,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.
*/
@@ -544,6 +581,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;
@@ -607,9 +649,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/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 ()
};
@@ -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),
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
@@ -16,6 +16,14 @@ pkgcfg_DATA = \
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 = \
@@ -31,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/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;
+}