commit a0238028dd8ff2f0098465c510460c13b542861d
parent 48a0a077162d7e42c249929eda536fded1b7b940
Author: Christian Grothoff <christian@grothoff.org>
Date: Thu, 19 May 2016 18:26:00 +0200
Merge branch 'master' of git.taler.net:/var/git/merchant
Diffstat:
18 files changed, 898 insertions(+), 186 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -27,5 +27,6 @@ GTAGS
*.swp
src/backend/taler-merchant-httpd
src/lib/test_merchant_api
+src/lib/test_merchant_api_home/.local/share/taler/exchange/live-keys/
taler_merchant_config.h
taler_merchant_config.h.in
diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf
@@ -21,7 +21,8 @@ DB = postgres
# Which wireformat does this merchant use? (test/sepa/etc.)
# WIREFORMAT = "test"
-# Must match the specification given in [merchant-wireformat]
+# Determines which wire plugin will be used. We currently only
+# support one wire plugin at a time!
# Configuration for postgres database.
@@ -31,14 +32,7 @@ CONFIG = postgres:///talermerchant
# Configuration of our bank account details
[merchant-wireformat]
+# Default location for the 'test' wire plugin
TEST_RESPONSE_FILE = ${TALER_CONFIG_HOME}/merchant/wire/test.json
-# The values in this section must match the "WIREFORMAT" given in [merchant]:
-# * for SEPA:
-# IBAN = DE67830654080004822650
-# NAME = GNUNET E.V
-# BIC = GENODEF1SRL
-#
-# * for TEST:
-# ACCOUNT_NUMBER = 123456
-# BANK_URI = http://
-#
+# Default location for the 'sepa' wire plugin
+SEPA_RESPONSE_FILE = ${TALER_CONFIG_HOME}/merchant/wire/sepa.json
diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c
@@ -553,6 +553,10 @@ run (void *cls,
GNUNET_SCHEDULER_shutdown ();
return;
}
+ if (GNUNET_YES != GNUNET_DISK_file_test (keyfile))
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Merchant private key `%s' does not exist yet, creating it!\n",
+ keyfile);
if (NULL ==
(privkey =
GNUNET_CRYPTO_eddsa_key_create_from_file (keyfile)))
diff --git a/src/backend/taler-merchant-httpd_auditors.c b/src/backend/taler-merchant-httpd_auditors.c
@@ -128,8 +128,8 @@ parse_auditors (void *cls,
struct Auditor auditor;
if (0 != strncasecmp (section,
- "auditor-",
- strlen ("auditor-")))
+ "merchant-auditor-",
+ strlen ("merchant-auditor-")))
return;
if (GNUNET_OK !=
GNUNET_CONFIGURATION_get_value_string (cfg,
diff --git a/src/backend/taler-merchant-httpd_contract.c b/src/backend/taler-merchant-httpd_contract.c
@@ -31,6 +31,58 @@
extern char *TMH_merchant_currency_string;
+
+/**
+ * Check that the given JSON array of products is well-formed.
+ *
+ * @param products JSON array to check
+ * @return #GNUNET_OK if all is fine
+ */
+static int
+check_products (json_t *products)
+{
+ size_t index;
+ json_t *value;
+ int res;
+
+ if (! json_is_array (products))
+ {
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ json_array_foreach (products, index, value) {
+ const char *description;
+ const char *error_name;
+ unsigned int error_line;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_string ("description", &description),
+ /* FIXME: there are other fields in the product specification
+ that rre currently not labeled as optional. Maybe check
+ those as well, or make them truly optional. */
+ GNUNET_JSON_spec_end()
+ };
+
+ /* extract fields we need to sign separately */
+ res = GNUNET_JSON_parse (value,
+ spec,
+ &error_name,
+ &error_line);
+ if (GNUNET_OK != res)
+ {
+ GNUNET_break (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Product description parsing failed at #%u: %s:%u\n",
+ (unsigned int) index,
+ error_name,
+ error_line);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_JSON_parse_free (spec);
+ }
+ return GNUNET_OK;
+}
+
+
/**
* Manage a contract request. In practical terms, it adds the fields
* 'exchanges', 'merchant_pub', and 'H_wire' to the contract 'proposition'
@@ -64,10 +116,20 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
struct TALER_Amount total;
struct TALER_Amount max_fee;
uint64_t transaction_id;
+ json_t *products;
+ struct GNUNET_TIME_Absolute timestamp;
+ struct GNUNET_TIME_Absolute refund_deadline;
+ struct GNUNET_TIME_Absolute expiry;
struct GNUNET_JSON_Specification spec[] = {
TALER_JSON_spec_amount ("amount", &total),
TALER_JSON_spec_amount ("max_fee", &max_fee),
GNUNET_JSON_spec_uint64 ("transaction_id", &transaction_id),
+ /* The following entries we don't actually need, except to check that
+ the contract is well-formed */
+ GNUNET_JSON_spec_json ("products", &products),
+ GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp),
+ GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline),
+ GNUNET_JSON_spec_absolute_time ("expiry", &expiry),
GNUNET_JSON_spec_end()
};
@@ -97,6 +159,7 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
if (NULL == jcontract)
{
+ json_decref (root);
return TMH_RESPONSE_reply_external_error (connection,
"contract request malformed");
}
@@ -105,10 +168,49 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
jcontract,
spec);
if (GNUNET_NO == res)
+ {
+ json_decref (root);
return MHD_YES;
+ }
if (GNUNET_SYSERR == res)
+ {
+ json_decref (root);
return TMH_RESPONSE_reply_external_error (connection,
"contract request malformed");
+ }
+ /* check contract is well-formed */
+ if (GNUNET_OK != check_products (products))
+ {
+ GNUNET_JSON_parse_free (spec);
+ json_decref (root);
+ return TMH_RESPONSE_reply_external_error (connection,
+ "products in contract request malformed");
+ }
+
+ /* Check if this transaction ID erroneously corresponds to a
+ contract that already paid, in which case we should refuse
+ to sign it again (frontend buggy, it should use a fresh
+ transaction ID each time)! */
+ if (GNUNET_OK ==
+ db->check_payment (db->cls,
+ transaction_id))
+ {
+ struct MHD_Response *resp;
+ int ret;
+
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Transaction %llu already paid in the past, refusing to sign!\n",
+ (unsigned long long) transaction_id);
+ resp = MHD_create_response_from_buffer (strlen ("Duplicate transaction ID!"),
+ "Duplicate transaction ID!",
+ MHD_RESPMEM_PERSISTENT);
+ ret = MHD_queue_response (connection,
+ MHD_HTTP_FORBIDDEN,
+ resp);
+ MHD_destroy_response (resp);
+ return ret;
+ }
+
/* add fields to the contract that the backend should provide */
json_object_set (jcontract,
"exchanges",
@@ -126,9 +228,6 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
sizeof (pubkey)));
/* create contract signature */
- GNUNET_assert (GNUNET_OK ==
- TALER_JSON_hash (jcontract,
- &contract.h_contract));
contract.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_CONTRACT);
contract.purpose.size = htonl (sizeof (contract));
contract.transaction_id = GNUNET_htonll (transaction_id);
@@ -136,6 +235,9 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
&total);
TALER_amount_hton (&contract.max_fee,
&max_fee);
+ GNUNET_assert (GNUNET_OK ==
+ TALER_JSON_hash (jcontract,
+ &contract.h_contract));
GNUNET_CRYPTO_eddsa_sign (privkey,
&contract.purpose,
&contract_sig);
@@ -146,9 +248,10 @@ MH_handler_contract (struct TMH_RequestHandler *rh,
"{s:O, s:O, s:O}",
"contract", jcontract,
"merchant_sig", GNUNET_JSON_from_data (&contract_sig,
- sizeof (contract_sig)),
+ sizeof (contract_sig)),
"H_contract", GNUNET_JSON_from_data (&contract.h_contract,
- sizeof (contract.h_contract)));
+ sizeof (contract.h_contract)));
+ GNUNET_JSON_parse_free (spec);
json_decref (root);
return res;
}
diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c
@@ -312,7 +312,7 @@ return_result (void *cls)
*/
struct TMH_EXCHANGES_FindOperation *
TMH_EXCHANGES_find_exchange (const char *chosen_exchange,
- TMH_EXCHANGES_FindContinuation fc,
+ TMH_EXCHANGES_FindContinuation fc, // process payment
void *fc_cls)
{
struct Exchange *exchange;
@@ -390,7 +390,7 @@ TMH_EXCHANGES_find_exchange (const char *chosen_exchange,
exchange->fo_tail,
fo);
- if (GNUNET_YES != exchange->pending)
+ if (GNUNET_YES != exchange->pending) // can post coins
{
/* We are not currently waiting for a reply, immediately
return result */
diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c
@@ -232,6 +232,24 @@ resume_pay_with_response (struct PayContext *pc,
TMH_trigger_daemon (); /* we resumed, kick MHD */
}
+/**
+ * Convert denomination key to its base32 representation
+ *
+ * @param dk denomination key to convert
+ * @return 0-terminated base32 encoding of @a dk, to be deallocated
+ */
+static char *
+denomination_to_string_alloc (struct TALER_DenominationPublicKey *dk)
+{
+ char *buf;
+ char *buf2;
+ size_t buf_size;
+ buf_size = GNUNET_CRYPTO_rsa_public_key_encode (dk->rsa_public_key, &buf);
+ buf2 = GNUNET_STRINGS_data_to_string_alloc (buf, buf_size);
+ GNUNET_free (buf);
+ return buf2;
+}
+
/**
* Abort all pending /deposit operations.
@@ -300,10 +318,19 @@ deposit_cb (void *cls,
}
else
{
- /* Forward error including 'proof' for the body */
+ /* Forward error, adding the "coin_pub" for which the
+ error was being generated */
+ json_t *eproof;
+
+ eproof = json_copy ((json_t *) proof);
+ json_object_set (eproof,
+ "coin_pub",
+ GNUNET_JSON_from_data (&dc->coin_pub,
+ sizeof (struct TALER_CoinSpendPublicKeyP)));
resume_pay_with_response (pc,
http_status,
- TMH_RESPONSE_make_json (proof));
+ TMH_RESPONSE_make_json (eproof));
+ json_decref (eproof);
}
return;
}
@@ -436,12 +463,16 @@ process_pay_with_exchange (void *cls,
&dc->denom);
if (NULL == denom_details)
{
+ char *denom_enc;
GNUNET_break_op (0);
resume_pay_with_response (pc,
MHD_HTTP_BAD_REQUEST,
TMH_RESPONSE_make_json_pack ("{s:s, s:o}",
"hint", "unknown denom to exchange",
"denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key)));
+ denom_enc = denomination_to_string_alloc (&dc->denom);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "unknown denom to exchange: %s\n", denom_enc);
+ GNUNET_free (denom_enc);
return;
}
if (GNUNET_OK !=
@@ -449,12 +480,16 @@ process_pay_with_exchange (void *cls,
denom_details,
exchange_trusted))
{
+ char *denom_enc;
GNUNET_break_op (0);
resume_pay_with_response (pc,
MHD_HTTP_BAD_REQUEST,
TMH_RESPONSE_make_json_pack ("{s:s, s:o}",
"hint", "no acceptable auditor for denomination",
"denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key)));
+ denom_enc = denomination_to_string_alloc (&dc->denom);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "no acceptable auditor for denomination: %s\n", denom_enc);
+ GNUNET_free (denom_enc);
return;
}
if (0 == i)
@@ -633,8 +668,6 @@ MH_handler_pay (struct TMH_RequestHandler *rh,
GNUNET_break (0);
return MHD_NO; /* hard error */
}
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Queueing response for /pay.\n");
res = MHD_queue_response (connection,
pc->response_code,
pc->response);
@@ -643,6 +676,10 @@ MH_handler_pay (struct TMH_RequestHandler *rh,
MHD_destroy_response (pc->response);
pc->response = NULL;
}
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Queueing response (%u) for /pay (%s).\n",
+ (unsigned int) pc->response_code,
+ res ? "OK" : "FAILED");
return res;
}
@@ -797,6 +834,9 @@ MH_handler_pay (struct TMH_RequestHandler *rh,
/* Payment succeeded in the past; take short cut
and accept immediately */
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Transaction %llu already paid in the past, taking short cut.\n",
+ (unsigned long long) pc->transaction_id);
resp = MHD_create_response_from_buffer (0,
NULL,
MHD_RESPMEM_PERSISTENT);
diff --git a/src/backend/taler-merchant-httpd_pay.h b/src/backend/taler-merchant-httpd_pay.h
@@ -23,7 +23,6 @@
#include <microhttpd.h>
#include "taler-merchant-httpd.h"
-
/**
* Manage a payment
*
diff --git a/src/backend/taler-merchant-httpd_util.c b/src/backend/taler-merchant-httpd_util.c
@@ -29,7 +29,6 @@
#include "taler-merchant-httpd_responses.h"
-
/**
* Hashes a plain JSON contract sending the result to the other end of
* HTTP communication
@@ -48,7 +47,6 @@ MH_handler_hash_contract (struct TMH_RequestHandler *rh,
const char *upload_data,
size_t *upload_data_size)
{
-
json_t *root;
json_t *jcontract;
int res;
@@ -102,8 +100,7 @@ MH_handler_hash_contract (struct TMH_RequestHandler *rh,
MHD_HTTP_OK,
"{s:O}",
"hash", GNUNET_JSON_from_data (&hc,
- sizeof (hc)));
+ sizeof (hc)));
json_decref (root);
return res;
-
}
diff --git a/src/backend/test-merchant.conf b/src/backend/test-merchant.conf
diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c
@@ -48,6 +48,26 @@ struct PostgresClosure
/**
+ * Drop merchant tables
+ *
+ * @param cls closure our `struct Plugin`
+ * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure
+ */
+static int
+postgres_drop_tables (void *cls)
+{
+ struct PostgresClosure *pg = cls;
+ int ret;
+
+ ret = GNUNET_POSTGRES_exec (pg->conn,
+ "DROP TABLE payments;");
+ if (GNUNET_OK != ret)
+ return ret;
+ return GNUNET_OK;
+}
+
+
+/**
* Initialize merchant tables
*
* @param cls closure our `struct Plugin`
@@ -290,6 +310,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls)
pg->conn = GNUNET_POSTGRES_connect (cfg, "merchantdb-postgres");
plugin = GNUNET_new (struct TALER_MERCHANTDB_Plugin);
plugin->cls = pg;
+ plugin->drop_tables = &postgres_drop_tables;
plugin->initialize = &postgres_initialize;
plugin->store_payment = &postgres_store_payment;
plugin->check_payment = &postgres_check_payment;
diff --git a/src/include/taler_merchant_service.h b/src/include/taler_merchant_service.h
@@ -25,6 +25,65 @@
#include <gnunet/gnunet_curl_lib.h>
#include <jansson.h>
+/* ********************* /contract *********************** */
+
+
+/**
+ * @brief Handle to a /contract operation at a merchant's backend.
+ */
+struct TALER_MERCHANT_ContractOperation;
+
+
+/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * /contract request to a merchant.
+ *
+ * @param cls closure
+ * @param http_status HTTP response code, 200 indicates success;
+ * 0 if the backend's reply is bogus (fails to follow the protocol)
+ * @param obj the full received JSON reply, or
+ * error details if the request failed
+ * @param contract completed contract, NULL on error
+ * @param sig merchant's signature over the contract, NULL on error
+ * @param h_contract hash of the contract, NULL on error
+ */
+typedef void
+(*TALER_MERCHANT_ContractCallback) (void *cls,
+ unsigned int http_status,
+ const json_t *obj,
+ const json_t *contract,
+ const struct TALER_MerchantSignatureP *sig,
+ const struct GNUNET_HashCode *h_contract);
+
+
+/**
+ * Request backend to sign a contract (and add fields like wire transfer
+ * details).
+ *
+ * @param ctx execution context
+ * @param backend_uri URI of the backend
+ * @param contract prototype of the contract
+ * @param contract_cb the callback to call when a reply for this request is available
+ * @param contract_cb_cls closure for @a contract_cb
+ * @return a handle for this request
+ */
+struct TALER_MERCHANT_ContractOperation *
+TALER_MERCHANT_contract_sign (struct GNUNET_CURL_Context *ctx,
+ const char *backend_uri,
+ const json_t *contract,
+ TALER_MERCHANT_ContractCallback contract_cb,
+ void *contract_cb_cls);
+
+
+/**
+ * Cancel a /contract request.
+ *
+ * @param co the contract operation handle
+ */
+void
+TALER_MERCHANT_contract_sign_cancel (struct TALER_MERCHANT_ContractOperation *co);
+
+
/* ********************* /pay *********************** */
@@ -49,14 +108,11 @@ struct TALER_MERCHANT_Pay;
* can indicate success, depending on whether the interaction
* was with a merchant frontend or backend;
* 0 if the merchant's reply is bogus (fails to follow the protocol)
- * @param redirect_uri URI for the redirect, if the request was successful and we were talking to a frontend;
- * NULL if the request failed or if were were talking to a backend
* @param obj the received JSON reply, with error details if the request failed
*/
typedef void
(*TALER_MERCHANT_PayCallback) (void *cls,
unsigned int http_status,
- const char *redirect_uri,
const json_t *obj);
@@ -77,6 +133,11 @@ struct TALER_MERCHANT_PayCoin
struct TALER_DenominationSignature denom_sig;
/**
+ * Overall value that coins of this @e denom_pub have.
+ */
+ struct TALER_Amount denom_value;
+
+ /**
* Coin's private key.
*/
struct TALER_CoinSpendPrivateKeyP coin_priv;
@@ -120,7 +181,7 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx,
const char *merchant_uri,
const struct GNUNET_HashCode *h_contract,
uint64_t transaction_id,
- const struct TALER_Amount *amount,
+ const struct TALER_Amount *amount,
const struct TALER_Amount *max_fee,
const struct TALER_MerchantPublicKeyP *merchant_pub,
const struct TALER_MerchantSignatureP *merchant_sig,
@@ -151,6 +212,11 @@ struct TALER_MERCHANT_PaidCoin
struct TALER_DenominationSignature denom_sig;
/**
+ * Overall value that coins of this @e denom_pub have.
+ */
+ struct TALER_Amount denom_value;
+
+ /**
* Coin's public key.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
@@ -185,12 +251,10 @@ struct TALER_MERCHANT_PaidCoin
* @param amount total value of the contract to be paid to the merchant
* @param max_fee maximum fee covered by the merchant (according to the contract)
* @param transaction_id transaction id for the transaction between merchant and customer
- * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests)
* @param merchant_sig the signature of the merchant over the original contract
* @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed)
* @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant
- * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend)
- * @param h_wire hash of the merchant’s account details
+ * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend). If non-zero, must be larger than @a refund_deadline.
* @param exchange_uri URI of the exchange that the coins belong to
* @param num_coins number of coins used to pay
* @param coins array of coins we use to pay
@@ -206,12 +270,10 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
const struct TALER_Amount *amount,
const struct TALER_Amount *max_fee,
uint64_t transaction_id,
- const struct TALER_MerchantPublicKeyP *merchant_pub,
const struct TALER_MerchantSignatureP *merchant_sig,
struct GNUNET_TIME_Absolute refund_deadline,
struct GNUNET_TIME_Absolute timestamp,
struct GNUNET_TIME_Absolute execution_deadline,
- const struct GNUNET_HashCode *h_wire,
const char *exchange_uri,
unsigned int num_coins,
const struct TALER_MERCHANT_PaidCoin *coins,
diff --git a/src/include/taler_merchantdb_plugin.h b/src/include/taler_merchantdb_plugin.h
@@ -48,6 +48,15 @@ struct TALER_MERCHANTDB_Plugin
char *library_name;
/**
+ * Drop merchant tables. Used for testcases.
+ *
+ * @param cls closure
+ * @return #GNUNET_OK upon success; #GNUNET_SYSERR upon failure
+ */
+ int
+ (*drop_tables) (void *cls);
+
+ /**
* Initialize merchant tables
*
* @param cls closure
diff --git a/src/lib/Makefile.am b/src/lib/Makefile.am
@@ -14,9 +14,11 @@ libtalermerchant_la_LDFLAGS = \
-no-undefined
libtalermerchant_la_SOURCES = \
+ merchant_api_contract.c \
merchant_api_pay.c
libtalermerchant_la_LIBADD = \
+ -ltalerexchange \
-ltalerjson \
-ltalerutil \
-lgnunetcurl \
@@ -42,11 +44,13 @@ TESTS = \
test_merchant_api_SOURCES = \
test_merchant_api.c
test_merchant_api_LDADD = \
+ $(top_srcdir)/src/backenddb/libtalermerchantdb.la \
libtalermerchant.la \
$(LIBGCRYPT_LIBS) \
-ltalerexchange \
-ltalerjson \
-ltalerutil \
+ -lgnunetjson \
-lgnunetcurl \
-lgnunetutil \
-ljansson
diff --git a/src/lib/merchant_api_contract.c b/src/lib/merchant_api_contract.c
@@ -0,0 +1,238 @@
+/*
+ This file is part of TALER
+ Copyright (C) 2014, 2015, 2016 GNUnet e.V. and INRIA
+
+ 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 2.1, 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 Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public License along with
+ TALER; see the file COPYING.LGPL. If not, If not, see
+ <http://www.gnu.org/licenses/>
+*/
+/**
+ * @file lib/merchant_api_contract.c
+ * @brief Implementation of the /contract request of the merchant's HTTP API
+ * @author Christian Grothoff
+ */
+#include "platform.h"
+#include <curl/curl.h>
+#include <jansson.h>
+#include <microhttpd.h> /* just for HTTP status codes */
+#include <gnunet/gnunet_util_lib.h>
+#include <gnunet/gnunet_curl_lib.h>
+#include "taler_merchant_service.h"
+#include <taler/taler_json_lib.h>
+#include <taler/taler_signatures.h>
+
+
+/**
+ * @brief A Contract Operation Handle
+ */
+struct TALER_MERCHANT_ContractOperation
+{
+
+ /**
+ * The url for this request.
+ */
+ char *url;
+
+ /**
+ * JSON encoding of the request to POST.
+ */
+ char *json_enc;
+
+ /**
+ * Handle for the request.
+ */
+ struct GNUNET_CURL_Job *job;
+
+ /**
+ * Function to call with the result.
+ */
+ TALER_MERCHANT_ContractCallback cb;
+
+ /**
+ * Closure for @a cb.
+ */
+ void *cb_cls;
+
+ /**
+ * Reference to the execution context.
+ */
+ struct GNUNET_CURL_Context *ctx;
+};
+
+
+/**
+ * Function called when we're done processing the
+ * HTTP /contract request.
+ *
+ * @param cls the `struct TALER_MERCHANT_Pay`
+ * @param response_code HTTP response code, 0 on error
+ * @param json response body, NULL if not in JSON
+ */
+static void
+handle_contract_finished (void *cls,
+ long response_code,
+ const json_t *json)
+{
+ struct TALER_MERCHANT_ContractOperation *co = cls;
+ json_t *contract;
+ const struct TALER_MerchantSignatureP *sigp;
+ const struct GNUNET_HashCode *h_contractp;
+ struct TALER_MerchantSignatureP sig;
+ struct GNUNET_HashCode h_contract;
+
+ co->job = NULL;
+ contract = NULL;
+ sigp = NULL;
+ h_contractp = NULL;
+ switch (response_code)
+ {
+ case 0:
+ break;
+ case MHD_HTTP_OK:
+ {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_json ("contract", &contract),
+ GNUNET_JSON_spec_fixed_auto ("merchant_sig", &sig),
+ GNUNET_JSON_spec_fixed_auto ("H_contract", &h_contract),
+ GNUNET_JSON_spec_end()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json,
+ spec,
+ NULL, NULL))
+ {
+ GNUNET_break_op (0);
+ response_code = 0;
+ break;
+ }
+ h_contractp = &h_contract;
+ sigp = &sig;
+ }
+ break;
+ case MHD_HTTP_BAD_REQUEST:
+ /* This should never happen, either us or the merchant is buggy
+ (or API version conflict); just pass JSON reply to the application */
+ break;
+ case MHD_HTTP_FORBIDDEN:
+ /* Duplicate transaction ID, frontend is buggy! */
+ break;
+ case MHD_HTTP_UNAUTHORIZED:
+ /* Nothing really to verify, merchant says one of the signatures is
+ invalid; as we checked them, this should never happen, we
+ should pass the JSON reply to the application */
+ break;
+ case MHD_HTTP_NOT_FOUND:
+ /* Nothing really to verify, this should never
+ happen, we should pass the JSON reply to the application */
+ break;
+ case MHD_HTTP_INTERNAL_SERVER_ERROR:
+ /* Server had an internal issue; we should retry, but this API
+ leaves this to the application */
+ break;
+ default:
+ /* unexpected response code */
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected response code %u\n",
+ (unsigned int) response_code);
+ GNUNET_break (0);
+ response_code = 0;
+ break;
+ }
+ co->cb (co->cb_cls,
+ response_code,
+ json,
+ contract,
+ sigp,
+ h_contractp);
+ if (NULL != contract)
+ json_decref (contract);
+ TALER_MERCHANT_contract_sign_cancel (co);
+}
+
+
+/**
+ * Request backend to sign a contract (and add fields like wire transfer
+ * details).
+ *
+ * @param ctx execution context
+ * @param backend_uri URI of the backend
+ * @param contract prototype of the contract
+ * @param contract_cb the callback to call when a reply for this request is available
+ * @param contract_cb_cls closure for @a contract_cb
+ * @return a handle for this request
+ */
+struct TALER_MERCHANT_ContractOperation *
+TALER_MERCHANT_contract_sign (struct GNUNET_CURL_Context *ctx,
+ const char *backend_uri,
+ const json_t *contract,
+ TALER_MERCHANT_ContractCallback contract_cb,
+ void *contract_cb_cls)
+{
+ struct TALER_MERCHANT_ContractOperation *co;
+ json_t *req;
+ CURL *eh;
+
+ co = GNUNET_new (struct TALER_MERCHANT_ContractOperation);
+ co->ctx = ctx;
+ co->cb = contract_cb;
+ co->cb_cls = contract_cb_cls;
+ co->url = GNUNET_strdup (backend_uri);
+
+ req = json_pack ("{s:O}",
+ "contract", (json_t *) contract);
+ eh = curl_easy_init ();
+ GNUNET_assert (NULL != (co->json_enc =
+ json_dumps (req,
+ JSON_COMPACT)));
+ json_decref (req);
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_URL,
+ co->url));
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_POSTFIELDS,
+ co->json_enc));
+ GNUNET_assert (CURLE_OK ==
+ curl_easy_setopt (eh,
+ CURLOPT_POSTFIELDSIZE,
+ strlen (co->json_enc)));
+ co->job = GNUNET_CURL_job_add (ctx,
+ eh,
+ GNUNET_YES,
+ &handle_contract_finished,
+ co);
+ return co;
+}
+
+
+/**
+ * Cancel a /contract request. This function cannot be used
+ * on a request handle if a response is already served for it.
+ *
+ * @param co the contract operation request handle
+ */
+void
+TALER_MERCHANT_contract_sign_cancel (struct TALER_MERCHANT_ContractOperation *co)
+{
+ if (NULL != co->job)
+ {
+ GNUNET_CURL_job_cancel (co->job);
+ co->job = NULL;
+ }
+ GNUNET_free (co->url);
+ GNUNET_free (co->json_enc);
+ GNUNET_free (co);
+}
+
+
+/* end of merchant_api_contract.c */
diff --git a/src/lib/merchant_api_pay.c b/src/lib/merchant_api_pay.c
@@ -28,6 +28,7 @@
#include "taler_merchant_service.h"
#include <taler/taler_json_lib.h>
#include <taler/taler_signatures.h>
+#include <taler/taler_exchange_service.h>
/**
@@ -65,9 +66,113 @@ struct TALER_MERCHANT_Pay
* Reference to the execution context.
*/
struct GNUNET_CURL_Context *ctx;
+
+ /**
+ * Number of @e coins we are paying with.
+ */
+ unsigned int num_coins;
+
+ /**
+ * The coins we are paying with.
+ */
+ struct TALER_MERCHANT_PaidCoin *coins;
+
};
+/**
+ * We got a 403 response back from the exchange (or the merchant).
+ * Now we need to check the provided cryptographic proof that the
+ * coin was actually already spent!
+ *
+ * @param pc handle of the original coin we paid with
+ * @param json cryptographic proof of coin's transaction history as
+ * was returned by the exchange/merchant
+ * @return #GNUNET_OK if proof checks out
+ */
+static int
+check_coin_history (const struct TALER_MERCHANT_PaidCoin *pc,
+ json_t *json)
+{
+ struct TALER_Amount spent;
+ struct TALER_Amount spent_plus_contrib;
+
+ if (GNUNET_OK !=
+ TALER_EXCHANGE_verify_coin_history (pc->amount_with_fee.currency,
+ &pc->coin_pub,
+ json,
+ &spent))
+ {
+ /* Exchange's history fails to verify */
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ if (GNUNET_OK !=
+ TALER_amount_add (&spent_plus_contrib,
+ &spent,
+ &pc->amount_with_fee))
+ {
+ /* We got an integer overflow? Bad application! */
+ GNUNET_break (0);
+ return GNUNET_SYSERR;
+ }
+ if (-1 != TALER_amount_cmp (&pc->denom_value,
+ &spent_plus_contrib))
+ {
+ /* according to our calculations, the transaction should
+ have still worked, exchange error! */
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "Accepting proof of double-spending\n");
+ return GNUNET_OK;
+}
+
+
+/**
+ * We got a 403 response back from the exchange (or the merchant).
+ * Now we need to check the provided cryptographic proof that the
+ * coin was actually already spent!
+ *
+ * @param ph handle of the original pay operation
+ * @param json cryptographic proof returned by the exchange/merchant
+ * @return #GNUNET_OK if proof checks out
+ */
+static int
+check_forbidden (struct TALER_MERCHANT_Pay *ph,
+ const json_t *json)
+{
+ json_t *history;
+ struct TALER_CoinSpendPublicKeyP coin_pub;
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_json ("history", &history),
+ GNUNET_JSON_spec_fixed_auto ("coin_pub", &coin_pub),
+ GNUNET_JSON_spec_end()
+ };
+ unsigned int i;
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (json,
+ spec,
+ NULL, NULL))
+ {
+ GNUNET_break_op (0);
+ return GNUNET_SYSERR;
+ }
+ for (i=0;i<ph->num_coins;i++)
+ {
+ if (0 == memcmp (&ph->coins[i].coin_pub,
+ &coin_pub,
+ sizeof (struct TALER_CoinSpendPublicKeyP)))
+ return check_coin_history (&ph->coins[i],
+ history);
+ }
+ GNUNET_break_op (0); /* complaint is not about any of the coins
+ that we actually paid with... */
+ return GNUNET_SYSERR;
+}
+
/**
* Function called when we're done processing the
@@ -96,6 +201,12 @@ handle_pay_finished (void *cls,
(or API version conflict); just pass JSON reply to the application */
break;
case MHD_HTTP_FORBIDDEN:
+ if (GNUNET_OK != check_forbidden (ph,
+ json))
+ {
+ GNUNET_break_op (0);
+ response_code = 0;
+ }
break;
case MHD_HTTP_UNAUTHORIZED:
/* Nothing really to verify, merchant says one of the signatures is
@@ -119,9 +230,11 @@ handle_pay_finished (void *cls,
response_code = 0;
break;
}
+ GNUNET_log (GNUNET_ERROR_TYPE_INFO,
+ "/pay completed with response code %u\n",
+ (unsigned int) response_code);
ph->cb (ph->cb_cls,
response_code,
- "FIXME-redirect-URI",
json);
TALER_MERCHANT_pay_cancel (ph);
}
@@ -134,6 +247,11 @@ handle_pay_finished (void *cls,
* @param exchange_uri URI of the exchange that the coins belong to
* @param h_wire hash of the merchant’s account details
* @param h_contract hash of the contact of the merchant with the customer
+ * @param transaction_id transaction id for the transaction between merchant and customer
+ * @param amount total value of the contract to be paid to the merchant
+ * @param max_fee maximum fee covered by the merchant (according to the contract)
+ * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests)
+ * @param merchant_sig signature from the merchant over the original contract
* @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant
* @param transaction_id transaction id for the transaction between merchant and customer
* @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests)
@@ -205,6 +323,7 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx,
&p->coin_sig.eddsa_signature);
p->denom_pub = coin->denom_pub;
p->denom_sig = coin->denom_sig;
+ p->denom_value = coin->denom_value;
p->coin_pub = dr.coin_pub;
p->amount_with_fee = coin->amount_with_fee;
p->amount_without_fee = coin->amount_without_fee;
@@ -215,12 +334,10 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx,
amount,
max_fee,
transaction_id,
- merchant_pub,
merchant_sig,
refund_deadline,
timestamp,
GNUNET_TIME_UNIT_ZERO_ABS,
- h_wire,
exchange_uri,
num_coins,
pc,
@@ -237,13 +354,11 @@ TALER_MERCHANT_pay_wallet (struct GNUNET_CURL_Context *ctx,
*
* @param ctx the execution loop context
* @param exchange_uri URI of the exchange that the coins belong to
- * @param h_wire hash of the merchant’s account details
* @param h_contract hash of the contact of the merchant with the customer
* @param timestamp timestamp when the contract was finalized, must match approximately the current time of the merchant
* @param transaction_id transaction id for the transaction between merchant and customer
- * @param merchant_pub the public key of the merchant (used to identify the merchant for refund requests)
* @param refund_deadline date until which the merchant can issue a refund to the customer via the merchant (can be zero if refunds are not allowed)
- * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend)
+ * @param execution_deadline date by which the merchant would like the exchange to execute the transaction (can be zero if there is no specific date desired by the frontend). If non-zero, must be larger than @a refund_deadline.
* @param num_coins number of coins used to pay
* @param coins array of coins we use to pay
* @param coin_sig the signature made with purpose #TALER_SIGNATURE_WALLET_COIN_DEPOSIT made by the customer with the coin’s private key.
@@ -260,12 +375,10 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
const struct TALER_Amount *amount,
const struct TALER_Amount *max_fee,
uint64_t transaction_id,
- const struct TALER_MerchantPublicKeyP *merchant_pub,
const struct TALER_MerchantSignatureP *merchant_sig,
struct GNUNET_TIME_Absolute refund_deadline,
struct GNUNET_TIME_Absolute timestamp,
struct GNUNET_TIME_Absolute execution_deadline,
- const struct GNUNET_HashCode *h_wire,
const char *exchange_uri,
unsigned int num_coins,
const struct TALER_MERCHANT_PaidCoin *coins,
@@ -280,6 +393,19 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
struct TALER_Amount total_amount;
unsigned int i;
+ if (GNUNET_YES !=
+ TALER_amount_cmp_currency (amount,
+ max_fee))
+ {
+ GNUNET_break (0);
+ return NULL;
+ }
+ if ( (0 != execution_deadline.abs_value_us) &&
+ (execution_deadline.abs_value_us < refund_deadline.abs_value_us) )
+ {
+ GNUNET_break (0);
+ return NULL;
+ }
if (0 == num_coins)
{
GNUNET_break (0);
@@ -364,6 +490,14 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
json_decref (j_coins);
return NULL;
}
+ if (GNUNET_YES !=
+ TALER_amount_cmp_currency (&new_amount,
+ &total_amount))
+ {
+ GNUNET_break (0);
+ json_decref (j_coins);
+ return NULL;
+ }
if (1 ==
TALER_amount_cmp (&new_amount,
&total_amount))
@@ -380,6 +514,14 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
{
/* Full fee covered by merchant, but our total
must at least cover the total contract amount */
+ if (GNUNET_YES !=
+ TALER_amount_cmp_currency (amount,
+ &total_amount))
+ {
+ GNUNET_break (0);
+ json_decref (j_coins);
+ return NULL;
+ }
if (1 ==
TALER_amount_cmp (amount,
&total_amount))
@@ -424,6 +566,12 @@ TALER_MERCHANT_pay_frontend (struct GNUNET_CURL_Context *ctx,
ph->cb = pay_cb;
ph->cb_cls = pay_cb_cls;
ph->url = GNUNET_strdup (merchant_uri);
+ ph->num_coins = num_coins;
+ ph->coins = GNUNET_new_array (num_coins,
+ struct TALER_MERCHANT_PaidCoin);
+ memcpy (ph->coins,
+ coins,
+ num_coins * sizeof (struct TALER_MERCHANT_PaidCoin));
eh = curl_easy_init ();
GNUNET_assert (NULL != (ph->json_enc =
diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c
@@ -24,6 +24,7 @@
#include <taler/taler_exchange_service.h>
#include <taler/taler_json_lib.h>
#include "taler_merchant_service.h"
+#include "taler_merchantdb_lib.h"
#include <gnunet/gnunet_util_lib.h>
#include <gnunet/gnunet_curl_lib.h>
#include <microhttpd.h>
@@ -96,6 +97,11 @@ enum OpCode
OC_WITHDRAW_SIGN,
/**
+ * Get backend to sign a contract.
+ */
+ OC_CONTRACT,
+
+ /**
* Pay with coins.
*/
OC_PAY
@@ -287,21 +293,49 @@ struct Command
} reserve_withdraw;
/**
- * Information for a #OC_PAY command.
- * FIXME: support tests where we pay with multiple coins at once.
+ * Information for an #OC_CONTRACT command.
*/
struct
{
/**
- * Amount to pay (total for the entire contract).
+ * Contract proposal (without merchant_pub, exchanges or H_wire).
+ */
+ const char *proposal;
+
+ /**
+ * Handle to the active /contract operation, or NULL.
+ */
+ struct TALER_MERCHANT_ContractOperation *co;
+
+ /**
+ * Full contract in JSON, set by the /contract operation.
*/
- const char *total_amount;
+ json_t *contract;
/**
- * Maximum fee covered by merchant.
+ * Signature, set by the /contract operation.
*/
- const char *max_fee;
+ struct TALER_MerchantSignatureP merchant_sig;
+
+ /**
+ * Hash of the full contract, set by the /contract operation.
+ */
+ struct GNUNET_HashCode h_contract;
+
+ } contract;
+
+ /**
+ * Information for a #OC_PAY command.
+ * FIXME: support tests where we pay with multiple coins at once.
+ */
+ struct
+ {
+
+ /**
+ * Reference to the contract.
+ */
+ const char *contract_ref;
/**
* Reference to a reserve_withdraw operation for a coin to
@@ -330,27 +364,6 @@ struct Command
const char *amount_without_fee;
/**
- * JSON string describing the merchant's "wire details".
- */
- const char *wire_details;
-
- /**
- * JSON string describing the contract between the two parties.
- */
- const char *contract;
-
- /**
- * Transaction ID to use.
- */
- uint64_t transaction_id;
-
- /**
- * Relative time (to add to 'now') to compute the refund deadline.
- * Zero for no refunds.
- */
- struct GNUNET_TIME_Relative refund_deadline;
-
- /**
* Deposit handle while operation is running.
*/
struct TALER_MERCHANT_Pay *ph;
@@ -399,6 +412,10 @@ struct InterpreterState
static void
fail (struct InterpreterState *is)
{
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Interpreter failed at step %s (#%u)\n",
+ is->commands[is->ip].label,
+ is->ip);
result = GNUNET_SYSERR;
GNUNET_SCHEDULER_shutdown ();
}
@@ -716,6 +733,54 @@ reserve_withdraw_cb (void *cls,
/**
+ * Callbacks of this type are used to serve the result of submitting a
+ * /contract request to a merchant.
+ *
+ * @param cls closure
+ * @param http_status HTTP response code, 200 indicates success;
+ * 0 if the backend's reply is bogus (fails to follow the protocol)
+ * @param obj the full received JSON reply, or
+ * error details if the request failed
+ * @param contract completed contract, NULL on error
+ * @param sig merchant's signature over the contract, NULL on error
+ * @param h_contract hash of the contract, NULL on error
+ */
+static void
+contract_cb (void *cls,
+ unsigned int http_status,
+ const json_t *obj,
+ const json_t *contract,
+ const struct TALER_MerchantSignatureP *sig,
+ const struct GNUNET_HashCode *h_contract)
+{
+ struct InterpreterState *is = cls;
+ struct Command *cmd = &is->commands[is->ip];
+
+ cmd->details.contract.co = NULL;
+ switch (http_status)
+ {
+ case MHD_HTTP_OK:
+ cmd->details.contract.contract = json_incref ((json_t *) contract);
+ cmd->details.contract.merchant_sig = *sig;
+ cmd->details.contract.h_contract = *h_contract;
+ break;
+ default:
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "/contract responded with unexpected status code %u in step %u\n",
+ http_status,
+ is->ip);
+ json_dumpf (obj, stderr, 0);
+ GNUNET_break (0);
+ fail (is);
+ return;
+ }
+ is->ip++;
+ is->task = GNUNET_SCHEDULER_add_now (&interpreter_run,
+ is);
+}
+
+
+/**
* Function called with the result of a /pay operation.
*
* @param cls closure with the interpreter state
@@ -727,7 +792,6 @@ reserve_withdraw_cb (void *cls,
static void
pay_cb (void *cls,
unsigned int http_status,
- const char *redirect_uri,
const json_t *obj)
{
struct InterpreterState *is = cls;
@@ -977,83 +1041,93 @@ interpreter_run (void *cls)
return;
}
return;
- case OC_PAY:
+ case OC_CONTRACT:
{
- struct TALER_MERCHANT_PayCoin pc;
- struct TALER_Amount amount;
- struct TALER_Amount max_fee;
- json_t *wire;
- json_t *contract;
- struct GNUNET_HashCode h_wire;
- struct GNUNET_HashCode h_contract;
- struct GNUNET_TIME_Absolute refund_deadline;
- struct GNUNET_TIME_Absolute timestamp;
- struct TALER_MerchantSignatureP merchant_sig;
-
- /* get amount */
- if (GNUNET_OK !=
- TALER_string_to_amount (cmd->details.pay.total_amount,
- &amount))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to parse total amount `%s' at %u\n",
- cmd->details.pay.total_amount,
- is->ip);
- fail (is);
- return;
- }
-
- /* get max_fee */
- if (GNUNET_OK !=
- TALER_string_to_amount (cmd->details.pay.max_fee,
- &max_fee))
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to parse max_fee `%s' at %u\n",
- cmd->details.pay.max_fee,
- is->ip);
- fail (is);
- return;
- }
+ json_t *proposal;
+ json_error_t error;
/* parse wire details */
- wire = json_loads (cmd->details.pay.wire_details,
- JSON_REJECT_DUPLICATES,
- NULL);
- if (NULL == wire)
+ proposal = json_loads (cmd->details.contract.proposal,
+ JSON_REJECT_DUPLICATES,
+ &error);
+ if (NULL == proposal)
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to parse wire details `%s' at %u\n",
- cmd->details.pay.wire_details,
- is->ip);
+ "Failed to parse proposal `%s' at command #%u: %s at %u\n",
+ cmd->details.contract.proposal,
+ is->ip,
+ error.text,
+ (unsigned int) error.column);
fail (is);
return;
}
- TALER_JSON_hash (wire,
- &h_wire);
- json_decref (wire);
-
- /* parse contract */
- contract = json_loads (cmd->details.pay.contract,
- JSON_REJECT_DUPLICATES,
- NULL);
- if (NULL == contract)
+ cmd->details.contract.co
+ = TALER_MERCHANT_contract_sign (ctx,
+ MERCHANT_URI "contract",
+ proposal,
+ &contract_cb,
+ is);
+ if (NULL == cmd->details.contract.co)
{
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Failed to parse contract details `%s' at instruction %u\n",
- cmd->details.pay.contract,
- is->ip);
+ GNUNET_break (0);
fail (is);
return;
}
- TALER_JSON_hash (contract,
+ return;
+ }
+ case OC_PAY:
+ {
+ struct TALER_MERCHANT_PayCoin pc;
+ uint64_t transaction_id;
+ struct GNUNET_TIME_Absolute refund_deadline;
+ struct GNUNET_TIME_Absolute timestamp;
+ struct GNUNET_HashCode h_wire;
+ struct TALER_MerchantPublicKeyP merchant_pub;
+ struct TALER_MerchantSignatureP merchant_sig;
+ struct GNUNET_HashCode h_contract;
+ struct TALER_Amount total_amount;
+ struct TALER_Amount max_fee;
+ const char *error_name;
+ unsigned int error_line;
+
+ /* get amount */
+ ref = find_command (is,
+ cmd->details.pay.contract_ref);
+ merchant_sig = ref->details.contract.merchant_sig;
+ GNUNET_assert (NULL != ref->details.contract.contract);
+ {
+ struct GNUNET_JSON_Specification spec[] = {
+ GNUNET_JSON_spec_uint64 ("transaction_id", &transaction_id),
+ GNUNET_JSON_spec_absolute_time ("refund_deadline", &refund_deadline),
+ GNUNET_JSON_spec_absolute_time ("timestamp", ×tamp),
+ GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub),
+ GNUNET_JSON_spec_fixed_auto ("H_wire", &h_wire),
+ TALER_JSON_spec_amount ("amount", &total_amount),
+ TALER_JSON_spec_amount ("max_fee", &max_fee),
+ GNUNET_JSON_spec_end()
+ };
+
+ if (GNUNET_OK !=
+ GNUNET_JSON_parse (ref->details.contract.contract,
+ spec,
+ &error_name,
+ &error_line))
+ {
+ GNUNET_break_op (0);
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Parser failed on %s:%u\n",
+ error_name,
+ error_line);
+ fail (is);
+ return;
+ }
+ }
+
+ TALER_JSON_hash (ref->details.contract.contract,
&h_contract);
- json_decref (contract);
/* initialize 'pc' (FIXME: to do in a loop later...) */
{
- const struct Command *ref;
-
memset (&pc, 0, sizeof (pc));
ref = find_command (is,
cmd->details.pay.coin_ref);
@@ -1064,6 +1138,7 @@ interpreter_run (void *cls)
pc.coin_priv = ref->details.reserve_withdraw.coin_priv;
pc.denom_pub = ref->details.reserve_withdraw.pk->key;
pc.denom_sig = ref->details.reserve_withdraw.sig;
+ pc.denom_value = ref->details.reserve_withdraw.pk->value;
break;
default:
GNUNET_assert (0);
@@ -1094,21 +1169,12 @@ interpreter_run (void *cls)
}
}
- if (0 == cmd->details.pay.refund_deadline.rel_value_us)
- refund_deadline = GNUNET_TIME_UNIT_ZERO_ABS; /* no refunds */
- else
- refund_deadline = GNUNET_TIME_relative_to_absolute (cmd->details.pay.refund_deadline);
- GNUNET_TIME_round_abs (&refund_deadline);
- timestamp = GNUNET_TIME_absolute_get ();
- GNUNET_TIME_round_abs (×tamp);
- memset (&merchant_sig, 0, sizeof (merchant_sig)); // FIXME: init properly!
- GNUNET_break (0);
cmd->details.pay.ph
= TALER_MERCHANT_pay_wallet (ctx,
MERCHANT_URI "pay",
&h_contract,
- cmd->details.pay.transaction_id,
- &amount,
+ transaction_id,
+ &total_amount,
&max_fee,
&merchant_pub,
&merchant_sig,
@@ -1225,6 +1291,22 @@ do_shutdown (void *cls)
cmd->details.reserve_withdraw.blinding_key.rsa_blinding_key = NULL;
}
break;
+ case OC_CONTRACT:
+ if (NULL != cmd->details.contract.co)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Command %u (%s) did not complete\n",
+ i,
+ cmd->label);
+ TALER_MERCHANT_contract_sign_cancel (cmd->details.contract.co);
+ cmd->details.contract.co = NULL;
+ }
+ if (NULL != cmd->details.contract.contract)
+ {
+ json_decref (cmd->details.contract.contract);
+ cmd->details.contract.contract = NULL;
+ }
+ break;
case OC_PAY:
if (NULL != cmd->details.pay.ph)
{
@@ -1335,57 +1417,33 @@ run (void *cls)
.expected_response_code = MHD_HTTP_OK,
.details.reserve_status.reserve_reference = "create-reserve-1",
.details.reserve_status.expected_balance = "EUR:0" },
- /* Try to pay with the 5 EUR coin (in full) */
+ /* Create contract */
+ { .oc = OC_CONTRACT,
+ .label = "create-contract-1",
+ .expected_response_code = MHD_HTTP_OK,
+ .details.contract.proposal = "{ \"max_fee\":{\"currency\":\"EUR\", \"value\":0, \"fraction\":500000}, \"transaction_id\":1, \"timestamp\":\"\\/Date(42)\\/\", \"refund_deadline\":\"\\/Date(0)\\/\", \"expiry\":\"\\/Date(999999999)\\/\", \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0}, \"products\":[ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }" },
{ .oc = OC_PAY,
.label = "deposit-simple",
.expected_response_code = MHD_HTTP_OK,
- .details.pay.total_amount = "EUR:5",
- .details.pay.max_fee = "EUR:0.5",
+ .details.pay.contract_ref = "create-contract-1",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.amount_with_fee = "EUR:5",
- .details.pay.amount_without_fee = "EUR:4.99",
- .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }",
- .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }",
- .details.pay.transaction_id = 1 },
+ .details.pay.amount_without_fee = "EUR:4.99" },
+ /* Create another contract */
+ { .oc = OC_CONTRACT,
+ .label = "create-contract-2",
+ .expected_response_code = MHD_HTTP_OK,
+ .details.contract.proposal = "{ \"max_fee\":{\"currency\":\"EUR\", \"value\":0, \"fraction\":500000}, \"transaction_id\":2, \"timestamp\":\"\\/Date(42)\\/\", \"refund_deadline\":\"\\/Date(0)\\/\", \"expiry\":\"\\/Date(999999999)\\/\", \"amount\":{\"currency\":\"EUR\", \"value\":5, \"fraction\":0}, \"products\":[ {\"description\":\"ice cream\", \"value\":\"{EUR:5}\"} ] }" },
- /* Try to double-spend the 5 EUR coin with different wire details */
- { .oc = OC_PAY,
- .label = "deposit-double-1",
- .expected_response_code = MHD_HTTP_FORBIDDEN,
- .details.pay.total_amount = "EUR:5",
- .details.pay.max_fee = "EUR:0.5",
- .details.pay.coin_ref = "withdraw-coin-1",
- .details.pay.amount_with_fee = "EUR:5",
- .details.pay.amount_without_fee = "EUR:4.99",
- .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }",
- .details.pay.contract = "{ \"items\":[{ \"name\":\"ice cream\", \"value\":1} ] }",
- .details.pay.transaction_id = 1 },
/* Try to double-spend the 5 EUR coin at the same merchant (but different
transaction ID) */
{ .oc = OC_PAY,
.label = "deposit-double-2",
.expected_response_code = MHD_HTTP_FORBIDDEN,
- .details.pay.total_amount = "EUR:5",
- .details.pay.max_fee = "EUR:0.5",
+ .details.pay.contract_ref = "create-contract-2",
.details.pay.coin_ref = "withdraw-coin-1",
.details.pay.amount_with_fee = "EUR:5",
- .details.pay.amount_without_fee = "EUR:4.99",
- .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }",
- .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":1} ] }",
- .details.pay.transaction_id = 2 },
- /* Try to double-spend the 5 EUR coin at the same merchant (but different
- contract) */
- { .oc = OC_PAY,
- .label = "deposit-double-3",
- .expected_response_code = MHD_HTTP_FORBIDDEN,
- .details.pay.total_amount = "EUR:5",
- .details.pay.max_fee = "EUR:0.5",
- .details.pay.coin_ref = "withdraw-coin-1",
- .details.pay.amount_with_fee = "EUR:5",
- .details.pay.amount_without_fee = "EUR:4.99",
- .details.pay.wire_details = "{ \"type\":\"test\", \"bank_uri\":\"http://localhost/\", \"account_number\":62 }",
- .details.pay.contract = "{ \"items\":[ {\"name\":\"ice cream\", \"value\":2} ] }",
- .details.pay.transaction_id = 1 },
+ .details.pay.amount_without_fee = "EUR:4.99" },
{ .oc = OC_END }
};
@@ -1428,12 +1486,35 @@ main (int argc,
struct GNUNET_OS_Process *proc;
struct GNUNET_OS_Process *exchanged;
struct GNUNET_OS_Process *merchantd;
+ struct TALER_MERCHANTDB_Plugin *db;
+ struct GNUNET_CONFIGURATION_Handle *cfg;
unsetenv ("XDG_DATA_HOME");
unsetenv ("XDG_CONFIG_HOME");
GNUNET_log_setup ("test-merchant-api",
"WARNING",
NULL);
+ cfg = GNUNET_CONFIGURATION_create ();
+ GNUNET_assert (GNUNET_OK ==
+ GNUNET_CONFIGURATION_load (cfg,
+ "test_merchant_api.conf"));
+ db = TALER_MERCHANTDB_plugin_load (cfg);
+ if (NULL == db)
+ {
+ GNUNET_CONFIGURATION_destroy (cfg);
+ return 77;
+ }
+ (void) db->drop_tables (db->cls);
+ if (GNUNET_OK != db->initialize (db->cls))
+ {
+ TALER_MERCHANTDB_plugin_unload (db);
+ GNUNET_CONFIGURATION_destroy (cfg);
+ return 77;
+ }
+ TALER_MERCHANTDB_plugin_unload (db);
+ GNUNET_CONFIGURATION_destroy (cfg);
+
+
GNUNET_assert (GNUNET_OK ==
GNUNET_STRINGS_string_to_data (merchant_pub_str,
strlen (merchant_pub_str),
diff --git a/src/lib/test_merchant_api.conf b/src/lib/test_merchant_api.conf
@@ -4,6 +4,10 @@
# Persistant data storage for the testcase
TALER_TEST_HOME = test_merchant_api_home/
+##########################################
+# Configuration for the merchant backend #
+##########################################
+
[merchant]
# Which port do we run the backend on? (HTTP server)
@@ -29,7 +33,7 @@ DB = postgres
# Which wireformat do we use?
WIREFORMAT = test
-[exchange-taler]
+[merchant-exchange-test]
URI = http://localhost:8081/
MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG
@@ -37,7 +41,7 @@ MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG
# Auditors must be in sections "auditor-", the rest of the section
# name could be anything.
-[auditor-ezb]
+[merchant-auditor-ezb]
# Informal name of the auditor. Just for the user.
NAME = European Central Bank
@@ -49,6 +53,8 @@ URI = http://taler.ezb.eu/
# This is the important bit: the signing key of the auditor.
PUBLIC_KEY = 9QXF7XY7E9VPV47B5Z806NDFSX2VJ79SVHHD29QEQ3BG31ANHZ60
+
+
# This specifies which database we use.
[merchantdb-postgres]
CONFIG = postgres:///talercheck
@@ -62,6 +68,10 @@ SALT = 17919252168512238964
ADDRESS = "Garching"
+###################################################
+# Configuration for the exchange for the testcase #
+###################################################
+
[exchange]
# How to access our database
DB = postgres
@@ -86,6 +96,7 @@ BANK_URI = "http://localhost/"
# Into which account at the 'bank' should (incoming) wire transfers be made?
BANK_ACCOUNT_NUMBER = 2
+
[coin_eur_ct_1]
value = EUR:0.01
duration_overlap = 5 minutes