merchant

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

commit a5f4a65c223f396a8b18caa492105793e46df909
parent e94d256e09a9ba6c0283843ffb02ddb2c02e104d
Author: Marcello Stanisci <marcello.stanisci@inria.fr>
Date:   Tue,  7 Mar 2017 15:57:34 +0100

Merge branch 'master' of taler.net:merchant

Diffstat:
M.gitignore | 3+++
MChangeLog | 9+++++++++
Mdoc/manual.texi | 4++--
Msrc/backend/merchant.conf | 23++++++++++++++++++++++-
Msrc/backend/taler-merchant-httpd.c | 260++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
Msrc/backend/taler-merchant-httpd.h | 25+++++++++++++++++++++++--
Msrc/backend/taler-merchant-httpd_exchanges.c | 428++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/backend/taler-merchant-httpd_exchanges.h | 4++++
Msrc/backend/taler-merchant-httpd_history.c | 72++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Msrc/backend/taler-merchant-httpd_pay.c | 402+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
Msrc/backend/taler-merchant-httpd_proposal.c | 70++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/backend/taler-merchant-httpd_track-transaction.c | 21+++++++++++++--------
Msrc/backend/taler-merchant-httpd_track-transfer.c | 5+++++
Msrc/backenddb/plugin_merchantdb_postgres.c | 2+-
Msrc/lib/test_merchant_api.c | 57++++++++++++++++++++++++++++++++++++---------------------
Msrc/lib/test_merchant_api.conf | 28++++++++++++++++++++++------
16 files changed, 1103 insertions(+), 310 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -26,8 +26,10 @@ GRTAGS GTAGS *.swp src/backend/taler-merchant-httpd +src/merchant-tools/taler-merchant-dbinit src/lib/test_merchant_api src/lib/test_merchant_api_home/.local/share/taler/exchange/live-keys/ +src/lib/test_merchant_api_home/.local/share/taler/wirefees/ taler_merchant_config.h taler_merchant_config.h.in doc/* @@ -35,3 +37,4 @@ doc/* !doc/*.am !doc/*.sh !doc/examples/ +src/lib/test_merchant_api_home/.local/share/taler/exchange/wirefees/ diff --git a/ChangeLog b/ChangeLog @@ -1,3 +1,12 @@ +Mon Mar 6 17:57:51 CET 2017 + Add support for wire fee calculations to /pay handling (#4935), + and adding setting "max_wire_fee" and "wire_fee_amortization" + in contract from defaults. -CG + +Mon Mar 6 00:59:25 CET 2017 + Implement BIND_TO option to allow binding backend to a particular + IP address (#4752). Enabling use of dual-stack by default. -CG + Thu Dec 15 10:37:08 CET 2016 Implementing: - /map/in, /map/out API, to allow frontends to store diff --git a/doc/manual.texi b/doc/manual.texi @@ -507,14 +507,14 @@ Which currency the Web shop deals in, i.e. ``EUR'' or ``USD'', is specified usin @cindex currency @cindex KUDOS @example -[merchant]/currency +[taler]/currency @end example For testing purposes, the currency MUST match ``KUDOS'' so that tests will work with the Taler demonstration exchange at @url{https://exchange.demo.taler.net/}: @example -$ taler-config -s merchant -o currency -V KUDOS +$ taler-config -s taler -o currency -V KUDOS @end example @item Database diff --git a/src/backend/merchant.conf b/src/backend/merchant.conf @@ -5,14 +5,35 @@ # General settings for the backend. [merchant] + +# Use TCP or UNIX domain sockets? SERVE = tcp -# Which HTTP port does the backend listen on? +# Which HTTP port does the backend listen on? Only used if "SERVE" is 'tcp'. PORT = 9966 +# Which IP address should we bind to? i.e. 127.0.0.1 or ::1 for loopback. +# Can also be given as a hostname. We will bind to the wildcard (dual-stack) +# if left empty. Only used if "SERVE" is 'tcp'. +# BIND_TO = + + +# Which unix domain path should we bind to? Only used if "SERVE" is 'unix'. UNIXPATH = ${TALER_RUNTIME_DIR}/merchant.http +# What should be the file access permissions (see chmod) for "UNIXPATH"? UNIXPATH_MODE = 660 + +# Maximum wire fee to permit by default. You most certainly want to +# adjust at least the currency. +# DEFAULT_MAX_WIRE_FEE = "KUDOS:0.10" + +# Which fraction of an exessivly high wire fee is the customer expected +# to cover? Must be a positive integer representing the expected +# average number of transactions aggregated by exchanges. 1 is +# always safe (financially speaking). +DEFAULT_WIRE_FEE_AMORTIZATION = 1 + # Where does the backend store the merchant's private key? KEYFILE = ${TALER_DATA_HOME}/merchant/merchant.priv diff --git a/src/backend/taler-merchant-httpd.c b/src/backend/taler-merchant-httpd.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014, 2015, 2016 INRIA + (C) 2014-2017 INRIA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -73,6 +73,17 @@ static long long unsigned port; struct GNUNET_TIME_Relative wire_transfer_delay; /** + * Default maximum wire fee to assume, unless stated differently in the proposal + * already. + */ +struct TALER_Amount default_max_wire_fee; + +/** + * Default factor for wire fee amortization. + */ +unsigned long long default_wire_fee_amortization; + +/** * Should a "Connection: close" header be added to each HTTP response? */ int TMH_merchant_connection_close; @@ -242,19 +253,25 @@ url_handler (void *cls, /** * Callback that frees all the elements in the hashmap * - * @param cls closure + * @param cls closure, NULL * @param key current key - * @param value current value + * @param value a `struct MerchantInstance` */ -int +static int hashmap_free (void *cls, const struct GNUNET_HashCode *key, void *value) { - GNUNET_free (value); + struct MerchantInstance *mi = value; + + json_decref (mi->j_wire); + GNUNET_free (mi->id); + GNUNET_free (mi->keyfile); + GNUNET_free (mi); return GNUNET_YES; } + /** * Shutdown task (magically invoked when the application is being * quit) @@ -269,6 +286,10 @@ do_shutdown (void *cls) GNUNET_SCHEDULER_cancel (mhd_task); mhd_task = NULL; } + /* FIXME: MHD API requires us to resume all suspended + connections before we do this, but /pay currently + suspends connections without giving us a way to + enumerate / resume them... */ if (NULL != mhd) { MHD_stop_daemon (mhd); @@ -286,9 +307,15 @@ do_shutdown (void *cls) &hashmap_free, NULL); if (NULL != by_id_map) + { GNUNET_CONTAINER_multihashmap_destroy (by_id_map); + by_id_map = NULL; + } if (NULL != by_kpub_map) + { GNUNET_CONTAINER_multihashmap_destroy (by_kpub_map); + by_kpub_map = NULL; + } } @@ -434,13 +461,15 @@ instances_iterator_cb (void *cls, /* used as hashmap keys */ struct GNUNET_HashCode h_pk; struct GNUNET_HashCode h_id; + json_t *type; char *emsg; iic = cls; substr = strstr (section, "merchant-instance-"); - if ((NULL == substr) - || (NULL != strstr (section, "merchant-instance-wireformat-"))) + if ( (NULL == substr) || + (NULL != strstr (section, + "merchant-instance-wireformat-")) ) return; if (substr != section) @@ -467,6 +496,7 @@ instances_iterator_cb (void *cls, GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, section, "KEYFILE"); + GNUNET_free (mi); GNUNET_SCHEDULER_shutdown (); return; } @@ -479,6 +509,8 @@ instances_iterator_cb (void *cls, (pk = GNUNET_CRYPTO_eddsa_key_create_from_file (mi->keyfile))) { GNUNET_break (0); + GNUNET_free (mi->keyfile); + GNUNET_free (mi); GNUNET_SCHEDULER_shutdown (); return; } @@ -487,11 +519,6 @@ instances_iterator_cb (void *cls, &mi->pubkey.eddsa_pub); GNUNET_free (pk); - /** - * FIXME: 'token' must NOT be freed, as it is handled by the - * gnunet_configuration facility. OTOH mi->id does need to be freed, - * because it is a duplicate. - */ mi->id = GNUNET_strdup (token + 1); if (0 == strcmp ("default", mi->id)) iic->default_instance = GNUNET_YES; @@ -499,11 +526,19 @@ instances_iterator_cb (void *cls, GNUNET_asprintf (&instance_wiresection, "merchant-instance-wireformat-%s", mi->id); - mi->j_wire = iic->plugin->get_wire_details (iic->plugin->cls, iic->config, instance_wiresection); GNUNET_free (instance_wiresection); + if ( (NULL == (type = json_object_get (mi->j_wire, + "type"))) || + (! json_is_string (type)) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Malformed wireformat: lacks type\n"); + iic->ret |= GNUNET_SYSERR; + } + mi->wire_method = json_string_value (type); if (TALER_EC_NONE != iic->plugin->wire_validate (iic->plugin->cls, @@ -511,7 +546,6 @@ instances_iterator_cb (void *cls, NULL, &emsg)) { - GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Malformed wireformat: %s\n", emsg); @@ -535,7 +569,7 @@ instances_iterator_cb (void *cls, #endif GNUNET_CRYPTO_hash (mi->id, - strlen(mi->id), + strlen (mi->id), &h_id); GNUNET_CRYPTO_hash (&mi->pubkey.eddsa_pub, sizeof (struct GNUNET_CRYPTO_EddsaPublicKey), @@ -550,18 +584,6 @@ instances_iterator_cb (void *cls, "Failed to put an entry into the 'by_id' hashmap\n"); iic->ret |= GNUNET_SYSERR; } - #ifdef EXTRADEBUG - else { - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Added element at %p, by by-id key %s of '%s' in hashmap\n", - mi, - GNUNET_h2s (&h_id), - mi->id); - GNUNET_assert (NULL != GNUNET_CONTAINER_multihashmap_get (by_id_map, - &h_id)); - } - #endif - if (GNUNET_OK != GNUNET_CONTAINER_multihashmap_put (by_kpub_map, &h_pk, @@ -633,8 +655,8 @@ get_instance (struct json_t *json) * * @param config configuration handle * @param allowed which wire format is allowed/expected? - * @return GNUNET_OK if successful, GNUNET_SYSERR upon errors - * (for example, if no "defaul" instance is defined) + * @return #GNUNET_OK if successful, #GNUNET_SYSERR upon errors + * (for example, if no "default" instance is defined) */ static unsigned int iterate_instances (const struct GNUNET_CONFIGURATION_Handle *config, @@ -688,7 +710,8 @@ iterate_instances (const struct GNUNET_CONFIGURATION_Handle *config, GNUNET_free (iic); return GNUNET_OK; - fail: do { + fail: + do { GNUNET_PLUGIN_unload (lib_name, iic->plugin); GNUNET_free (lib_name); @@ -717,7 +740,6 @@ run (void *cls, char *wireformat; int fh; - wireformat = NULL; result = GNUNET_SYSERR; GNUNET_SCHEDULER_add_shutdown (&do_shutdown, NULL); @@ -764,6 +786,56 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } + + if (GNUNET_OK != + TALER_config_get_denom (config, + "merchant", + "DEFAULT_MAX_WIRE_FEE", + &default_max_wire_fee)) + { + char *currency; + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (config, + "taler", + "CURRENCY", + &currency)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler", + "CURRENCY"); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + TALER_amount_get_zero (currency, + &default_max_wire_fee)) + { + GNUNET_log_config_invalid (GNUNET_ERROR_TYPE_ERROR, + "taler", + "CURRENCY", + "Specified value not legal for a Taler currency"); + GNUNET_SCHEDULER_shutdown (); + GNUNET_free (currency); + return; + } + GNUNET_free (currency); + } + + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (config, + "merchant", + "DEFAULT_WIRE_FEE_AMORTIZATION", + &default_wire_fee_amortization)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "merchant", + "DEFAULT_WIRE_FEE_AMORTIZATION"); + GNUNET_SCHEDULER_shutdown (); + return; + } + + wireformat = NULL; if (GNUNET_OK != GNUNET_CONFIGURATION_get_value_string (config, "merchant", @@ -776,10 +848,10 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } - - iterate_instances (config, wireformat); - + iterate_instances (config, + wireformat); GNUNET_free (wireformat); + if (NULL == (db = TALER_MERCHANTDB_plugin_load (config))) { @@ -882,41 +954,57 @@ run (void *cls, un = GNUNET_new (struct sockaddr_un); un->sun_family = AF_UNIX; - strncpy (un->sun_path, serve_unixpath, sizeof (un->sun_path) - 1); + strncpy (un->sun_path, + serve_unixpath, + sizeof (un->sun_path) - 1); GNUNET_NETWORK_unix_precheck (un); - if (NULL == (nh = GNUNET_NETWORK_socket_create (AF_UNIX, SOCK_STREAM, 0))) + if (NULL == (nh = GNUNET_NETWORK_socket_create (AF_UNIX, + SOCK_STREAM, + 0))) { - GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "create(for AF_UNIX)"); + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "socket(AF_UNIX)"); GNUNET_SCHEDULER_shutdown (); return; } - if (GNUNET_OK != GNUNET_NETWORK_socket_bind (nh, (void *) un, sizeof (struct sockaddr_un))) + if (GNUNET_OK != + GNUNET_NETWORK_socket_bind (nh, + (void *) un, + sizeof (struct sockaddr_un))) { - GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "bind(for AF_UNIX)"); + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "bind(AF_UNIX)"); GNUNET_SCHEDULER_shutdown (); return; } - if (GNUNET_OK != GNUNET_NETWORK_socket_listen (nh, UNIX_BACKLOG)) + if (GNUNET_OK != + GNUNET_NETWORK_socket_listen (nh, + UNIX_BACKLOG)) { - GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, "listen(for AF_UNIX)"); + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "listen(AF_UNIX)"); GNUNET_SCHEDULER_shutdown (); return; } fh = GNUNET_NETWORK_get_fd (nh); - if (0 != chmod (serve_unixpath, unixpath_mode)) + GNUNET_NETWORK_socket_free_memory_only_ (nh); + if (0 != chmod (serve_unixpath, + unixpath_mode)) { - fprintf (stderr, "chmod failed: %s\n", strerror (errno)); + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "chmod"); GNUNET_SCHEDULER_shutdown (); return; } - GNUNET_NETWORK_socket_free_memory_only_ (nh); port = 0; } else if (0 == strcmp (serve_type, "tcp")) { + char *bind_to; + if (GNUNET_SYSERR == GNUNET_CONFIGURATION_get_value_number (config, "merchant", @@ -929,7 +1017,81 @@ run (void *cls, GNUNET_SCHEDULER_shutdown (); return; } - fh = -1; + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_string (config, + "merchant", + "BIND_TO", + &bind_to)) + { + char port_str[6]; + struct addrinfo hints; + struct addrinfo *res; + int ec; + struct GNUNET_NETWORK_Handle *nh; + + GNUNET_snprintf (port_str, + sizeof (port_str), + "%u", + (uint16_t) port); + memset (&hints, 0, sizeof (hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE | AI_IDN; + if (0 != + (ec = getaddrinfo (bind_to, + port_str, + &hints, + &res))) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to resolve BIND_TO address `%s': %s\n", + bind_to, + gai_strerror (ec)); + GNUNET_free (bind_to); + GNUNET_SCHEDULER_shutdown (); + return; + } + GNUNET_free (bind_to); + + if (NULL == (nh = GNUNET_NETWORK_socket_create (res->ai_family, + res->ai_socktype, + res->ai_protocol))) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "socket"); + freeaddrinfo (res); + GNUNET_SCHEDULER_shutdown (); + return; + } + if (GNUNET_OK != + GNUNET_NETWORK_socket_bind (nh, + res->ai_addr, + res->ai_addrlen)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "bind"); + freeaddrinfo (res); + GNUNET_SCHEDULER_shutdown (); + return; + } + freeaddrinfo (res); + if (GNUNET_OK != + GNUNET_NETWORK_socket_listen (nh, + UNIX_BACKLOG)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "listen"); + GNUNET_SCHEDULER_shutdown (); + return; + } + fh = GNUNET_NETWORK_get_fd (nh); + GNUNET_NETWORK_socket_free_memory_only_ (nh); + } + else + { + fh = -1; + } } else { @@ -937,15 +1099,13 @@ run (void *cls, GNUNET_assert (0); } } - mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME, + mhd = MHD_start_daemon (MHD_USE_SUSPEND_RESUME | MHD_USE_DUAL_STACK, port, NULL, NULL, &url_handler, NULL, MHD_OPTION_LISTEN_SOCKET, fh, - MHD_OPTION_NOTIFY_COMPLETED, - &handle_mhd_completion_callback, NULL, - MHD_OPTION_CONNECTION_TIMEOUT, - (unsigned int) 10 /* 10s */, + MHD_OPTION_NOTIFY_COMPLETED, &handle_mhd_completion_callback, NULL, + MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int) 10 /* 10s */, MHD_OPTION_END); if (NULL == mhd) { diff --git a/src/backend/taler-merchant-httpd.h b/src/backend/taler-merchant-httpd.h @@ -38,7 +38,8 @@ * Used by the iterator of the various merchant's instances given * in configuration */ -struct IterateInstancesCls { +struct IterateInstancesCls +{ /** * Handle for the configuration beig parsed @@ -75,7 +76,8 @@ struct IterateInstancesCls { * backend can account for several merchants, as used to do in donation * shops */ -struct MerchantInstance { +struct MerchantInstance +{ /** * Instance's mnemonic identifier. This value lives as long as @@ -89,6 +91,14 @@ struct MerchantInstance { */ char *keyfile; + /* NOTE: the *_wire-fields should eventually be moved into a DLL + once we implement #4939 */ + + /** + * Which wire method is @e j_wire using? + */ + const char *wire_method; + /** * Wire details for this instance */ @@ -215,6 +225,17 @@ struct TM_HandlerContext extern json_t *j_wire; /** + * Default maximum wire fee to assume, unless stated differently in the proposal + * already. + */ +extern struct TALER_Amount default_max_wire_fee; + +/** + * Default factor for wire fee amortization. + */ +extern unsigned long long default_wire_fee_amortization; + +/** * Hash of our wire format details as given in #j_wire. */ extern struct GNUNET_HashCode h_wire; diff --git a/src/backend/taler-merchant-httpd_exchanges.c b/src/backend/taler-merchant-httpd_exchanges.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014, 2015, 2016 INRIA + (C) 2014-2017 INRIA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -84,6 +84,11 @@ struct TMH_EXCHANGES_FindOperation struct Exchange *my_exchange; /** + * Wire method we care about for fees. + */ + char *wire_method; + + /** * Task scheduled to asynchronously return the result to * the find continuation. */ @@ -93,6 +98,35 @@ struct TMH_EXCHANGES_FindOperation /** + * Information about wire transfer fees of an exchange, by wire method. + */ +struct FeesByWireMethod +{ + + /** + * Kept in a DLL. + */ + struct FeesByWireMethod *next; + + /** + * Kept in a DLL. + */ + struct FeesByWireMethod *prev; + + /** + * Wire method these fees are for. + */ + char *wire_method; + + /** + * Applicable fees, NULL if unknown/error. + */ + struct TALER_EXCHANGE_WireAggregateFees *af; + +}; + + +/** * Exchange */ struct Exchange @@ -129,6 +163,26 @@ struct Exchange struct TALER_EXCHANGE_Handle *conn; /** + * Active /wire request to the exchange, or NULL. + */ + struct TALER_EXCHANGE_WireHandle *wire_request; + + /** + * Task to re-run /wire after some delay. + */ + struct GNUNET_SCHEDULER_Task *wire_task; + + /** + * Head of wire fees from /wire request. + */ + struct FeesByWireMethod *wire_fees_head; + + /** + * Tail of wire fees from /wire request. + */ + struct FeesByWireMethod *wire_fees_tail; + + /** * Master public key, guaranteed to be set ONLY for * trusted exchanges. */ @@ -239,6 +293,279 @@ retry_exchange (void *cls) /** + * Function called with information about the wire fees + * for each wire method. Stores the wire fees with the + * exchange for laster use. + * + * @param cls closure + * @param wire_method name of the wire method (i.e. "sepa") + * @param fees fee structure for this method + */ +static void +process_wire_fees (void *cls, + const char *wire_method, + const struct TALER_EXCHANGE_WireAggregateFees *fees) +{ + struct Exchange *exchange = cls; + struct FeesByWireMethod *f; + struct TALER_EXCHANGE_WireAggregateFees *endp; + struct TALER_EXCHANGE_WireAggregateFees *af; + + for (f = exchange->wire_fees_head; NULL != f; f = f->next) + if (0 == strcasecmp (wire_method, + f->wire_method)) + break; + if (NULL == f) + { + f = GNUNET_new (struct FeesByWireMethod); + f->wire_method = GNUNET_strdup (wire_method); + GNUNET_CONTAINER_DLL_insert (exchange->wire_fees_head, + exchange->wire_fees_tail, + f); + } + endp = f->af; + while ( (NULL != endp) && + (NULL != endp->next) ) + endp = endp->next; + while ( (NULL != endp) && + (fees->start_date.abs_value_us < endp->end_date.abs_value_us) ) + fees = fees->next; + if ( (NULL != endp) && + (fees->start_date.abs_value_us != endp->end_date.abs_value_us) ) + { + /* Hole in the fee structure, not allowed! */ + GNUNET_break_op (0); + return; + } + while (NULL != fees) + { + af = GNUNET_new (struct TALER_EXCHANGE_WireAggregateFees); + *af = *fees; + af->next = NULL; + if (NULL == endp) + f->af = af; + else + endp->next = af; + endp = af; + // FIXME: also preserve `fees` in backend DB (under wire method + exchange master pub!) + fees = fees->next; + } +} + + +/** + * Obtain applicable fees for @a exchange and @a wire_method. + * + * @param exchange the exchange to query + * @param now current time + * @param wire_method the wire method we want the fees for + * @return NULL if we do not have fees for this method yet + */ +static struct TALER_EXCHANGE_WireAggregateFees * +get_wire_fees (struct Exchange *exchange, + struct GNUNET_TIME_Absolute now, + const char *wire_method) +{ + for (struct FeesByWireMethod *fbw = exchange->wire_fees_head; + NULL != fbw; + fbw = fbw->next) + if (0 == strcasecmp (fbw->wire_method, + wire_method) ) + { + struct TALER_EXCHANGE_WireAggregateFees *af; + + /* Advance through list up to current time */ + while ( (NULL != (af = fbw->af)) && + (now.abs_value_us >= af->end_date.abs_value_us) ) + { + fbw->af = af->next; + GNUNET_free (af); + } + return af; + } + return NULL; +} + + +/** + * Check if we have any remaining pending requests for the + * given @a exchange, and if we have the required data, call + * the callback. + * + * @param exchange the exchange to check for pending find operations + * @return #GNUNET_YES if we need /wire data from @a exchange + */ +static int +process_find_operations (struct Exchange *exchange) +{ + struct TMH_EXCHANGES_FindOperation *fo; + struct TMH_EXCHANGES_FindOperation *fn; + struct GNUNET_TIME_Absolute now; + int need_wire; + + now = GNUNET_TIME_absolute_get (); + need_wire = GNUNET_NO; + for (fo = exchange->fo_head; NULL != fo; fo = fn) + { + const struct TALER_Amount *wire_fee; + + fn = fo->next; + if (NULL != fo->wire_method) + { + struct TALER_EXCHANGE_WireAggregateFees *af; + + /* Find fee structure for our wire method */ + af = get_wire_fees (exchange, + now, + fo->wire_method); + if (NULL == af) + { + need_wire = GNUNET_YES; + continue; + } + if (af->start_date.abs_value_us > now.abs_value_us) + { + /* Disagreement on the current time */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Exchange's earliest fee is %s adhead of our time. Clock skew issue?\n", + GNUNET_STRINGS_relative_time_to_string (GNUNET_TIME_absolute_get_remaining (af->start_date), + GNUNET_YES)); + continue; + } + /* found fee, great! */ + wire_fee = &af->wire_fee; + } + else + { + /* no wire transfer method given, so we yield no fee */ + wire_fee = NULL; + } + GNUNET_CONTAINER_DLL_remove (exchange->fo_head, + exchange->fo_tail, + fo); + fo->fc (fo->fc_cls, + exchange->conn, + wire_fee, + exchange->trusted); + GNUNET_free_non_null (fo->wire_method); + GNUNET_free (fo); + } + return need_wire; +} + + +/** + * Check if we have any remaining pending requests for the + * given @a exchange, and if we have the required data, call + * the callback. If requests without /wire data remain, + * retry the /wire request after some delay. + * + * @param cls a `struct Exchange` to check + */ +static void +wire_task_cb (void *cls); + + +/** + * Callbacks of this type are used to serve the result of submitting a + * wire format inquiry request to a exchange. + * + * If the request fails to generate a valid response from the + * exchange, @a http_status will also be zero. + * + * @param cls closure, a `struct Exchange` + * @param http_status HTTP response code, #MHD_HTTP_OK (200) for successful request; + * 0 if the exchange's reply is bogus (fails to follow the protocol) + * @param ec taler-specific error code, #TALER_EC_NONE on success + * @param obj the received JSON reply, if successful this should be the wire + * format details as provided by /wire, or NULL if the + * reply was not in JSON format. + */ +static void +handle_wire_data (void *cls, + unsigned int http_status, + enum TALER_ErrorCode ec, + const json_t *obj) +{ + struct Exchange *exchange = cls; + + exchange->wire_request = NULL; + if (MHD_HTTP_OK != http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to obtain /wire details from `%s': %d\n", + exchange->uri, + ec); + return; + } + if (GNUNET_OK != + TALER_EXCHANGE_wire_get_fees (&TALER_EXCHANGE_get_keys (exchange->conn)->master_pub, + obj, + &process_wire_fees, + exchange)) + { + /* Report hard failure to all callbacks! */ + struct TMH_EXCHANGES_FindOperation *fo; + + GNUNET_break_op (0); + while (NULL != (fo = exchange->fo_head)) + { + GNUNET_CONTAINER_DLL_remove (exchange->fo_head, + exchange->fo_tail, + fo); + /* TODO: report more precise error, this ultimately generates + "exchange not supported" instead of "exchange violated + protocol"; we should ideally generate a reply with + a specific TALER_EC-code, boxing 'obj' within it. */ + fo->fc (fo->fc_cls, + NULL, + NULL, + GNUNET_NO); + GNUNET_free_non_null (fo->wire_method); + GNUNET_free (fo); + } + return; + } + if (GNUNET_YES == + process_find_operations (exchange)) + { + /* need to run /wire again, with some delay */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Do not have sufficient wire data. Will re-request /wire in 1 minute\n"); + + GNUNET_assert (NULL == exchange->wire_task); + exchange->wire_task = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_MINUTES, + &wire_task_cb, + exchange); + } +} + + +/** + * Check if we have any remaining pending requests for the + * given @a exchange, and if we have the required data, call + * the callback. If requests without /wire data remain, + * retry the /wire request after some delay. + * + * @param cls a `struct Exchange` to check + */ +static void +wire_task_cb (void *cls) +{ + struct Exchange *exchange = cls; + + exchange->wire_task = NULL; + if (GNUNET_YES != + process_find_operations (exchange)) + return; /* no more need */ + GNUNET_assert (NULL == exchange->wire_request); + exchange->wire_request = TALER_EXCHANGE_wire (exchange->conn, + &handle_wire_data, + exchange); +} + + +/** * Function called with information about who is auditing * a particular exchange and what key the exchange is using. * @@ -257,13 +584,22 @@ keys_mgmt_cb (void *cls, const struct TALER_EXCHANGE_Keys *keys) { struct Exchange *exchange = cls; - struct TMH_EXCHANGES_FindOperation *fo; struct GNUNET_TIME_Absolute expire; struct GNUNET_TIME_Relative delay; if (NULL == keys) { exchange->pending = GNUNET_YES; + if (NULL != exchange->wire_request) + { + TALER_EXCHANGE_wire_cancel (exchange->wire_request); + exchange->wire_request = NULL; + } + if (NULL != exchange->wire_task) + { + GNUNET_SCHEDULER_cancel (exchange->wire_task); + exchange->wire_task = NULL; + } exchange->retry_delay = RETRY_BACKOFF (exchange->retry_delay); GNUNET_log (GNUNET_ERROR_TYPE_WARNING, "Failed to fetch /keys from `%s', retrying in %s\n", @@ -280,21 +616,23 @@ keys_mgmt_cb (void *cls, delay = RELOAD_DELAY; else delay = GNUNET_TIME_absolute_get_remaining (expire); - exchange->retry_delay = GNUNET_TIME_UNIT_ZERO; + exchange->retry_delay + = GNUNET_TIME_UNIT_ZERO; exchange->retry_task = GNUNET_SCHEDULER_add_delayed (delay, &retry_exchange, exchange); exchange->pending = GNUNET_NO; - while (NULL != (fo = exchange->fo_head)) + if (GNUNET_YES == + process_find_operations (exchange)) { - GNUNET_CONTAINER_DLL_remove (exchange->fo_head, - exchange->fo_tail, - fo); - fo->fc (fo->fc_cls, - exchange->conn, - exchange->trusted); - GNUNET_free (fo); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Got key data, but do not have current wire data. Will request /wire now\n"); + GNUNET_assert (NULL == exchange->wire_request); + GNUNET_assert (NULL == exchange->wire_task); + exchange->wire_request = TALER_EXCHANGE_wire (exchange->conn, + &handle_wire_data, + exchange); } } @@ -310,17 +648,19 @@ return_result (void *cls) struct TMH_EXCHANGES_FindOperation *fo = cls; struct Exchange *exchange = fo->my_exchange; - fo->at = NULL; - GNUNET_CONTAINER_DLL_remove (exchange->fo_head, - exchange->fo_tail, - fo); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Returning result for exchange %s, trusted=%d\n", - exchange->uri, exchange->trusted); - fo->fc (fo->fc_cls, - (GNUNET_SYSERR == exchange->pending) ? NULL : exchange->conn, - exchange->trusted); - GNUNET_free (fo); + if ( (GNUNET_YES == + process_find_operations (exchange)) && + (NULL == exchange->wire_request) && + (GNUNET_NO == exchange->pending) && + (NULL != exchange->wire_task) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Do not have current wire data. Will re-request /wire in 1 minute\n"); + exchange->wire_task + = GNUNET_SCHEDULER_add_delayed (GNUNET_TIME_UNIT_MINUTES, + &wire_task_cb, + exchange); + } } @@ -330,12 +670,14 @@ return_result (void *cls) * NULL for the exchange. * * @param chosen_exchange URI of the exchange we would like to talk to + * @param wire_method the wire method we will use with @a chosen_exchange, NULL for none * @param fc function to call with the handles for the exchange * @param fc_cls closure for @a fc * @return NULL on error */ struct TMH_EXCHANGES_FindOperation * TMH_EXCHANGES_find_exchange (const char *chosen_exchange, + const char *wire_method, TMH_EXCHANGES_FindContinuation fc, void *fc_cls) { @@ -387,11 +729,17 @@ TMH_EXCHANGES_find_exchange (const char *chosen_exchange, fo->fc = fc; fo->fc_cls = fc_cls; fo->my_exchange = exchange; + if (NULL != wire_method) + fo->wire_method = GNUNET_strdup (wire_method); GNUNET_CONTAINER_DLL_insert (exchange->fo_head, exchange->fo_tail, fo); - if (GNUNET_YES != exchange->pending) + if ( (GNUNET_YES != exchange->pending) && + ( (NULL == fo->wire_method) || + (NULL != get_wire_fees (exchange, + GNUNET_TIME_absolute_get (), + fo->wire_method)) ) ) { /* We are not currently waiting for a reply, immediately return result */ @@ -404,9 +752,22 @@ TMH_EXCHANGES_find_exchange (const char *chosen_exchange, if ( (NULL == exchange->conn) && (GNUNET_YES == exchange->pending) ) { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Do not have current key data. Will request /keys now\n"); exchange->retry_task = GNUNET_SCHEDULER_add_now (&retry_exchange, exchange); } + else if ( (GNUNET_NO == exchange->pending) && + (NULL == exchange->wire_task) && + (NULL == exchange->wire_request) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Do not have current wire data. Will re-request /wire now\n"); + exchange->wire_task = GNUNET_SCHEDULER_add_now (&wire_task_cb, + exchange); + } + + return fo; } @@ -425,10 +786,11 @@ TMH_EXCHANGES_find_exchange_cancel (struct TMH_EXCHANGES_FindOperation *fo) { GNUNET_SCHEDULER_cancel (fo->at); fo->at = NULL; - } + } GNUNET_CONTAINER_DLL_remove (exchange->fo_head, exchange->fo_tail, fo); + GNUNET_free_non_null (fo->wire_method); GNUNET_free (fo); } @@ -443,7 +805,7 @@ TMH_EXCHANGES_find_exchange_cancel (struct TMH_EXCHANGES_FindOperation *fo) */ static void accept_exchanges (void *cls, - const char *section) + const char *section) { const struct GNUNET_CONFIGURATION_Handle *cfg = cls; char *uri; @@ -561,10 +923,26 @@ TMH_EXCHANGES_done () GNUNET_CONTAINER_DLL_remove (exchange_head, exchange_tail, exchange); + if (NULL != exchange->wire_request) + { + TALER_EXCHANGE_wire_cancel (exchange->wire_request); + exchange->wire_request = NULL; + } + if (NULL != exchange->wire_task) + { + GNUNET_SCHEDULER_cancel (exchange->wire_task); + exchange->wire_task = NULL; + } if (NULL != exchange->conn) + { TALER_EXCHANGE_disconnect (exchange->conn); + exchange->conn = NULL; + } if (NULL != exchange->retry_task) + { GNUNET_SCHEDULER_cancel (exchange->retry_task); + exchange->retry_task = NULL; + } GNUNET_free (exchange->uri); GNUNET_free (exchange); } diff --git a/src/backend/taler-merchant-httpd_exchanges.h b/src/backend/taler-merchant-httpd_exchanges.h @@ -60,11 +60,13 @@ TMH_EXCHANGES_done (void); * * @param cls closure * @param eh handle to the exchange context + * @param wire_fee current applicable wire fee for dealing with @a eh, NULL if not available * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config */ typedef void (*TMH_EXCHANGES_FindContinuation)(void *cls, struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, int exchange_trusted); @@ -80,11 +82,13 @@ struct TMH_EXCHANGES_FindOperation; * NULL for the exchange. * * @param chosen_exchange URI of the exchange we would like to talk to + * @param wire_method the wire method we will use with @a chosen_exchange, NULL for none * @param fc function to call with the handles for the exchange * @param fc_cls closure for @a fc */ struct TMH_EXCHANGES_FindOperation * TMH_EXCHANGES_find_exchange (const char *chosen_exchange, + const char *wire_method, TMH_EXCHANGES_FindContinuation fc, void *fc_cls); diff --git a/src/backend/taler-merchant-httpd_history.c b/src/backend/taler-merchant-httpd_history.c @@ -53,7 +53,6 @@ unsigned int current = 0; * @param refund refund deadline * @param total_amount total amount we receive for the contract after fees */ - static void pd_cb (void *cls, const char *order_id, @@ -63,26 +62,37 @@ pd_cb (void *cls, json_t *entry; json_t *amount; json_t *timestamp; + json_t *instance; + GNUNET_assert (-1 != json_unpack ((json_t *) proposal_data, + "{s:o, s:o, s:{s:o}}", + "amount", &amount, + "timestamp", &timestamp, + "merchant", "instance", &instance)); - GNUNET_assert (NULL != (amount = json_copy (json_object_get (proposal_data, "amount")))); - GNUNET_assert (NULL != (timestamp = json_object_get (proposal_data, "timestamp"))); - - if (current >= start && current < start + delta) + if ( (current >= start) && + (current < start + delta) ) { - GNUNET_break (NULL != (entry = json_pack ("{s:s, s:o, s:s}", + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Adding history element. Current: %u, start: %u, delta: %u\n", + current, + start, + delta); + GNUNET_break (NULL != (entry = json_pack ("{s:s, s:O, s:O, s:O}", "order_id", order_id, "amount", amount, - "timestamp", json_string_value (timestamp)))); + "timestamp", timestamp, + "instance", instance))); - GNUNET_break (0 == json_array_append_new (response, entry)); - + GNUNET_break (0 == json_array_append_new (response, + entry)); } // FIXME to zero after returned. current++; } + /** * Manage a /history request. Query the db and returns transactions * younger than the date given as parameter @@ -108,7 +118,7 @@ MH_handler_history (struct TMH_RequestHandler *rh, unsigned int ret; unsigned long long seconds; struct MerchantInstance *mi; - + response = json_array (); /*FIXME who decrefs this?*/ str = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, @@ -118,17 +128,22 @@ MH_handler_history (struct TMH_RequestHandler *rh, if (NULL != str) { if (1 != sscanf (str, "%llu", &seconds)) - return TMH_RESPONSE_reply_arg_invalid (connection, - TALER_EC_PARAMETER_MALFORMED, - "date"); + { + json_decref (response); + return TMH_RESPONSE_reply_arg_invalid (connection, + TALER_EC_PARAMETER_MALFORMED, + "date"); + } } date.abs_value_us = seconds * 1000LL * 1000LL; if (date.abs_value_us / 1000LL / 1000LL != seconds) + { + json_decref (response); return TMH_RESPONSE_reply_bad_request (connection, TALER_EC_HISTORY_TIMESTAMP_OVERFLOW, "Timestamp overflowed"); - + } mi = TMH_lookup_instance ("default"); @@ -139,13 +154,15 @@ MH_handler_history (struct TMH_RequestHandler *rh, mi = TMH_lookup_instance (str); if (NULL == mi) + { + json_decref (response); return TMH_RESPONSE_reply_not_found (connection, TALER_EC_HISTORY_INSTANCE_UNKNOWN, "instance"); - + } start = 0; delta = 20; - + str = MHD_lookup_connection_value (connection, MHD_GET_ARGUMENT_KIND, "start"); @@ -153,9 +170,12 @@ MH_handler_history (struct TMH_RequestHandler *rh, { if ((1 != sscanf (str, "%d", &start)) || start < 0) + { + json_decref (response); return TMH_RESPONSE_reply_arg_invalid (connection, TALER_EC_PARAMETER_MALFORMED, - "start"); + "start"); + } } str = MHD_lookup_connection_value (connection, @@ -168,11 +188,11 @@ MH_handler_history (struct TMH_RequestHandler *rh, delta < 0) return TMH_RESPONSE_reply_arg_invalid (connection, TALER_EC_PARAMETER_MALFORMED, - "delta"); + "delta"); } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Querying history back to %llu\n", - date.abs_value_us); + "Querying history back to %s\n", + GNUNET_STRINGS_absolute_time_to_string (date)); ret = db->find_proposal_data_by_date (db->cls, date, @@ -181,17 +201,21 @@ MH_handler_history (struct TMH_RequestHandler *rh, response); current = 0; if (GNUNET_SYSERR == ret) + { + json_decref (response); return TMH_RESPONSE_reply_internal_error (connection, TALER_EC_HISTORY_DB_FETCH_ERROR, "db error to get history"); - + } GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "history data: %s\n", json_dumps (response, JSON_INDENT (1))); - return TMH_RESPONSE_reply_json (connection, - response, - MHD_HTTP_OK); + ret = TMH_RESPONSE_reply_json (connection, + response, + MHD_HTTP_OK); + json_decref (response); + return ret; } /* end of taler-merchant-httpd_history.c */ diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c @@ -1,6 +1,6 @@ /* This file is part of TALER - (C) 2014, 2015, 2016 GNUnet e.V. and INRIA + (C) 2014-2017 GNUnet e.V. and INRIA TALER is free software; you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software @@ -165,18 +165,46 @@ struct PayContext struct TALER_Amount max_fee; /** + * Maximum wire 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 amorized difference. Wire fees are charged over an + * aggregate of several translations, hence unlike the deposit + * fees, they are amortized over several customer's transactions. + * The contract specifies under @e wire_fee_amortization how many + * customer's transactions he expects the wire fees to be amortized + * over on average. Thus, if the wire fees are larger than + * @e max_wire_fee, each customer is expected to contribute + * $\frac{actual-wire-fee - max_wire_fee}{wire_fee_amortization}$. + * The customer's contribution may be further reduced by the + * difference between @e max_fee and the sum of the deposit fees. + * + * Default is that the merchant is unwilling to pay any wire fees. + */ + struct TALER_Amount max_wire_fee; + + /** + * Number of transactions that the wire fees are expected to be + * amortized over. Never zero, defaults (conservateively) to 1. + * May be higher if merchants expect many small transactions to + * be aggregated and thus wire fees to be reasonably amortized + * due to aggregation. + */ + uint32_t wire_fee_amortization; + + /** * Amount from @e root. This is the amount the merchant expects * to make, minus @e max_fee. */ struct TALER_Amount amount; /** - * Timestamp from @e root. + * Timestamp from @e proposal_data. */ struct GNUNET_TIME_Absolute timestamp; /** - * Refund deadline from @e root. + * Refund deadline from @e proposal_data. */ struct GNUNET_TIME_Absolute refund_deadline; @@ -186,11 +214,17 @@ struct PayContext struct GNUNET_TIME_Absolute pay_deadline; /** - * "H_contract" from @e root. + * "H_contract" from @e proposal_data. */ struct GNUNET_HashCode h_proposal_data; /** + * "H_wire" from @e proposal_data. Used to identify the instance's + * wire transfer method. + */ + struct GNUNET_HashCode h_wire; + + /** * Wire transfer deadline. How soon would the merchant like the * wire transfer to be executed? (Can be given by the frontend * or be determined by our configuration via #wire_transfer_delay.) @@ -281,28 +315,6 @@ resume_pay_with_response (struct PayContext *pc, /** - * 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. * * @param pc pay context to abort @@ -347,10 +359,11 @@ sign_success_response (struct PayContext *pc) &mr.purpose, &sig); - return TMH_RESPONSE_make_json_pack ("{s:O, s:s, s:o}", - "proposal_data", pc->proposal_data, + return TMH_RESPONSE_make_json_pack ("{s:O, s:o, s:o}", + "proposal_data", + pc->proposal_data, "sig", - json_string_value (GNUNET_JSON_from_data_auto (&sig)), + GNUNET_JSON_from_data_auto (&sig), "h_proposal_data", GNUNET_JSON_from_data (&pc->h_proposal_data, sizeof (struct GNUNET_HashCode))); @@ -451,9 +464,9 @@ deposit_cb (void *cls, if (0 != pc->pending) return; /* still more to do */ - - - resume_pay_with_response (pc, MHD_HTTP_OK, sign_success_response (pc)); + resume_pay_with_response (pc, + MHD_HTTP_OK, + sign_success_response (pc)); } @@ -525,16 +538,20 @@ pay_context_cleanup (struct TM_HandlerContext *hc) * * @param cls the `struct PayContext` * @param mh NULL if exchange was not found to be acceptable + * @param wire_fee current applicable fee for dealing with @a mh, NULL if not available * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config */ static void process_pay_with_exchange (void *cls, struct TALER_EXCHANGE_Handle *mh, + const struct TALER_Amount *wire_fee, int exchange_trusted) { struct PayContext *pc = cls; struct TALER_Amount acc_fee; struct TALER_Amount acc_amount; + struct TALER_Amount wire_fee_delta; + struct TALER_Amount wire_fee_customer_contribution; const struct TALER_EXCHANGE_Keys *keys; unsigned int i; @@ -551,7 +568,6 @@ process_pay_with_exchange (void *cls, return; } pc->mh = mh; - keys = TALER_EXCHANGE_get_keys (mh); if (NULL == keys) { @@ -573,8 +589,6 @@ 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, @@ -583,11 +597,6 @@ process_pay_with_exchange (void *cls, "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND, "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key), "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh))); - 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 != @@ -595,8 +604,6 @@ 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, @@ -604,11 +611,6 @@ process_pay_with_exchange (void *cls, "error", "invalid denomination", "code", (json_int_t) TALER_EC_PAY_DENOMINATION_KEY_AUDITOR_FAILURE, "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, - "Client offered invalid denomination: %s\n", - denom_enc); - GNUNET_free (denom_enc); return; } dc->deposit_fee = denom_details->fee_deposit; @@ -654,6 +656,38 @@ process_pay_with_exchange (void *cls, } } + /* Now compare exchange wire fee compared to what we are willing to pay */ + if (GNUNET_YES != + TALER_amount_cmp_currency (wire_fee, + &pc->max_wire_fee)) + { + GNUNET_break (0); + resume_pay_with_response (pc, + MHD_HTTP_INTERNAL_SERVER_ERROR, + TMH_RESPONSE_make_internal_error (TALER_EC_PAY_WIRE_FEE_CURRENCY_MISSMATCH, + "wire_fee")); + return; + } + + if (GNUNET_OK == + TALER_amount_subtract (&wire_fee_delta, + wire_fee, + &pc->max_wire_fee)) + { + /* Actual wire fee is indeed higher than our maximum, compute + how much the customer is expected to cover! */ + TALER_amount_divide (&wire_fee_customer_contribution, + &wire_fee_delta, + pc->wire_fee_amortization); + } + else + { + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (wire_fee->currency, + &wire_fee_customer_contribution)); + + } + /* Now check that the customer paid enough for the full contract */ if (-1 == TALER_amount_cmp (&pc->max_fee, &acc_fee)) @@ -680,6 +714,12 @@ process_pay_with_exchange (void *cls, "overflow")); return; } + /* add wire fee contribution to the total */ + if (GNUNET_OK == + TALER_amount_add (&total_needed, + &total_needed, + &wire_fee_customer_contribution)) + /* check if total payment sufficies */ if (-1 == TALER_amount_cmp (&acc_amount, &total_needed)) @@ -694,7 +734,40 @@ process_pay_with_exchange (void *cls, } else { - /* fees are acceptable, we cover them all; let's check the amount */ + struct TALER_Amount deposit_fee_savings; + + /* Compute how much the customer saved by not going to the + limit on the deposit fees, as this amount is counted against + what we expect him to cover for the wire fees */ + GNUNET_assert (GNUNET_SYSERR != + TALER_amount_subtract (&deposit_fee_savings, + &pc->max_fee, + &acc_fee)); + /* See how much of wire fee contribution is covered by fee_savings */ + if (-1 == TALER_amount_cmp (&deposit_fee_savings, + &wire_fee_customer_contribution)) + { + /* wire_fee_customer_contribution > deposit_fee_savings */ + GNUNET_assert (GNUNET_SYSERR != + TALER_amount_subtract (&wire_fee_customer_contribution, + &wire_fee_customer_contribution, + &deposit_fee_savings)); + /* subtract remaining wire fees from total contribution */ + if (GNUNET_SYSERR == + TALER_amount_subtract (&acc_amount, + &acc_amount, + &wire_fee_customer_contribution)) + { + GNUNET_break_op (0); + resume_pay_with_response (pc, + MHD_HTTP_METHOD_NOT_ACCEPTABLE, + TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES, + "insufficient funds (including excessive exchange fees to be covered by customer)")); + return; + } + } + + /* fees are acceptable, merchant covers them all; let's check the amount */ if (-1 == TALER_amount_cmp (&acc_amount, &pc->amount)) { @@ -710,8 +783,6 @@ process_pay_with_exchange (void *cls, GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Exchange and fee structure OK. Initiating deposit operation for coins\n"); - - /* Initiate /deposit operation for all coins */ for (i=0;i<pc->coins_cnt;i++) { @@ -759,24 +830,21 @@ process_pay_with_exchange (void *cls, /** * Handle a timeout for the processing of the pay request. * - * @param cls closure + * @param cls our `struct PayContext` */ static void handle_pay_timeout (void *cls) { struct PayContext *pc = cls; + pc->timeout_task = NULL; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, "Resuming /pay with error after timeout\n"); - - pc->timeout_task = NULL; - if (NULL != pc->fo) { TMH_EXCHANGES_find_exchange_cancel (pc->fo); pc->fo = NULL; } - resume_pay_with_response (pc, MHD_HTTP_SERVICE_UNAVAILABLE, TMH_RESPONSE_make_internal_error (TALER_EC_PAY_EXCHANGE_TIMEOUT, @@ -873,48 +941,29 @@ check_transaction_exists (void *cls, } } + +// FIXME: declare in proper header! extern struct MerchantInstance * get_instance (struct json_t *json); /** - * Just a stub used to double-check if a transaction - * has been correctly inserted into db. - * - * @param cls closure - * @param transaction_id of the contract - * @param merchant's public key - * @param exchange_uri URI of the exchange - * @param h_contract hash of the contract - * @param h_wire hash of our wire details - * @param timestamp time of the confirmation - * @param refund refund deadline - * @param total_amount total amount we receive for the contract after fees - */ -static void -transaction_double_check (void *cls, - const struct TALER_MerchantPublicKeyP *merchant_pub, - const char *exchange_uri, - const struct GNUNET_HashCode *h_proposal_data, - const struct GNUNET_HashCode *h_wire, - struct GNUNET_TIME_Absolute timestamp, - struct GNUNET_TIME_Absolute refund, - const struct TALER_Amount *total_amount) -{ - return; -} - - - -/** * Try to parse the pay request into the given pay context. * * Schedules an error response in the connection on failure. * - * @return #GNUNET_YES on success + * + * @param connection HTTP connection we are receiving payment on + * @param root JSON upload with payment data + * @param pc context we use to handle the payment + * @return #GNUNET_OK on success, + * #GNUNET_NO on failure (response was queued with MHD) + * #GNUNET_SYSERR on hard error (MHD connection must be dropped) */ static int -parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *pc) +parse_pay (struct MHD_Connection *connection, + const json_t *root, + struct PayContext *pc) { json_t *coins; json_t *coin; @@ -940,19 +989,17 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p GNUNET_break (0); return res; } - res = db->find_proposal_data (db->cls, &pc->proposal_data, order_id, &merchant_pub); - - if (GNUNET_OK != res) { - - if (MHD_YES != TMH_RESPONSE_reply_not_found (connection, - TALER_EC_PAY_DB_STORE_PAY_ERROR, - "Proposal not found")) + GNUNET_JSON_parse_free (spec); + if (MHD_YES != + TMH_RESPONSE_reply_not_found (connection, + TALER_EC_PAY_DB_STORE_PAY_ERROR, + "Proposal not found")) { GNUNET_break (0); return GNUNET_SYSERR; @@ -960,12 +1007,15 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p return GNUNET_NO; } - - if (GNUNET_OK != TALER_JSON_hash (pc->proposal_data, &pc->h_proposal_data)) + if (GNUNET_OK != + TALER_JSON_hash (pc->proposal_data, + &pc->h_proposal_data)) { - if (MHD_YES != TMH_RESPONSE_reply_internal_error (connection, - TALER_EC_NONE, - "Can not hash proposal")) + GNUNET_JSON_parse_free (spec); + if (MHD_YES != + TMH_RESPONSE_reply_internal_error (connection, + TALER_EC_PAY_FAILED_COMPUTE_PROPOSAL_HASH, + "Failed to hash proposal")) { GNUNET_break (0); return GNUNET_SYSERR; @@ -973,15 +1023,17 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p return GNUNET_NO; } - - merchant = json_object_get (pc->proposal_data, "merchant"); + merchant = json_object_get (pc->proposal_data, + "merchant"); if (NULL == merchant) { - // invalid contract: + /* invalid contract */ GNUNET_break (0); - if (MHD_YES != TMH_RESPONSE_reply_internal_error (connection, - TALER_EC_NONE, - "No merchant field in contract")) + GNUNET_JSON_parse_free (spec); + if (MHD_YES != + TMH_RESPONSE_reply_internal_error (connection, + TALER_EC_PAY_MERCHANT_FIELD_MISSING, + "No merchant field in contract")) { GNUNET_break (0); return GNUNET_SYSERR; @@ -989,11 +1041,11 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p return GNUNET_NO; } pc->mi = get_instance (merchant); - if (NULL == pc->mi) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Not able to find the specified instance\n"); + "Unable to find the specified instance\n"); + GNUNET_JSON_parse_free (spec); if (MHD_NO == TMH_RESPONSE_reply_not_found (connection, TALER_EC_PAY_INSTANCE_UNKNOWN, "Unknown instance given")) @@ -1007,14 +1059,10 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p GNUNET_log (GNUNET_ERROR_TYPE_INFO, "/pay: picked instance %s with key %s\n", pc->mi->id, - GNUNET_STRINGS_data_to_string_alloc (&pc->mi->pubkey, sizeof (pc->mi->pubkey))); + GNUNET_STRINGS_data_to_string_alloc (&pc->mi->pubkey, + sizeof (pc->mi->pubkey))); pc->chosen_exchange = GNUNET_strdup (chosen_exchange); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Parsed JSON for /pay.\n"); - - - { struct GNUNET_JSON_Specification espec[] = { GNUNET_JSON_spec_absolute_time ("refund_deadline", @@ -1027,6 +1075,8 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p &pc->max_fee), TALER_JSON_spec_amount ("amount", &pc->amount), + GNUNET_JSON_spec_fixed_auto ("H_wire", + &pc->h_wire), GNUNET_JSON_spec_end() }; @@ -1040,7 +1090,9 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } - pc->wire_transfer_deadline = GNUNET_TIME_absolute_add (pc->timestamp, wire_transfer_delay); + pc->wire_transfer_deadline + = GNUNET_TIME_absolute_add (pc->timestamp, + wire_transfer_delay); if (pc->wire_transfer_deadline.abs_value_us < pc->refund_deadline.abs_value_us) { @@ -1052,8 +1104,72 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p } } - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed timestamps\n"); + /* NOTE: In the future, iterate over all wire hashes + available to a given instance here! (#4939) */ + if (0 != memcmp (&pc->h_wire, + &pc->mi->h_wire, + sizeof (struct GNUNET_HashCode))) + { + GNUNET_break (0); + GNUNET_JSON_parse_free (spec); + return TMH_RESPONSE_reply_internal_error (connection, + TALER_EC_PAY_WIRE_HASH_UNKNOWN, + "Did not find matching wire details"); + } + + /* parse optional details */ + if (NULL != json_object_get (pc->proposal_data, + "max_wire_fee")) + { + struct GNUNET_JSON_Specification espec[] = { + TALER_JSON_spec_amount ("max_wire_fee", + &pc->max_wire_fee), + GNUNET_JSON_spec_end() + }; + + res = TMH_PARSE_json_data (connection, + pc->proposal_data, + espec); + if (GNUNET_YES != res) + { + GNUNET_break_op (0); /* invalid input, use default */ + /* default is we cover no fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (pc->max_fee.currency, + &pc->max_wire_fee)); + } + } + else + { + /* default is we cover no fee */ + GNUNET_assert (GNUNET_OK == + TALER_amount_get_zero (pc->max_fee.currency, + &pc->max_wire_fee)); + } + if (NULL != json_object_get (pc->proposal_data, + "wire_fee_amortization")) + { + struct GNUNET_JSON_Specification espec[] = { + GNUNET_JSON_spec_uint32 ("wire_fee_amortization", + &pc->wire_fee_amortization), + GNUNET_JSON_spec_end() + }; + res = TMH_PARSE_json_data (connection, + pc->proposal_data, + espec); + if ( (GNUNET_YES != res) || + (0 == pc->wire_fee_amortization) ) + { + GNUNET_break_op (0); /* invalid input, use default */ + /* default is no amortization */ + pc->wire_fee_amortization = 1; + } + } + else + { + pc->wire_fee_amortization = 1; + } pc->coins_cnt = json_array_size (coins); if (0 == pc->coins_cnt) @@ -1086,53 +1202,40 @@ parse_pay (struct MHD_Connection *connection, json_t *root, struct PayContext *p if (GNUNET_YES != res) { GNUNET_JSON_parse_free (spec); - json_decref (root); GNUNET_break (0); return (GNUNET_NO == res) ? MHD_YES : MHD_NO; } - { - char *s; - - s = TALER_amount_to_string (&dc->amount_with_fee); - GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, - "Coin #%i has f %s\n", - coins_index, - s); - GNUNET_free (s); - } - dc->index = coins_index; dc->pc = pc; } - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed coins\n"); - pc->pending = pc->coins_cnt; - GNUNET_JSON_parse_free (spec); - return GNUNET_OK; } /** * Process a payment for a proposal. + * + * @param connection HTTP connection we are receiving payment on + * @param root JSON upload with payment data + * @param pc context we use to handle the payment + * @return value to return to MHD (#MHD_NO to drop connection, + * #MHD_YES to keep handling it) */ static int handler_pay_json (struct MHD_Connection *connection, - json_t *root, + const json_t *root, struct PayContext *pc) { int ret; - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "about to parse '/pay' body\n"); - - ret = parse_pay (connection, root, pc); + ret = parse_pay (connection, + root, + pc); if (GNUNET_OK != ret) - return ret; - - GNUNET_log (GNUNET_ERROR_TYPE_INFO, "parsed '/pay' body\n"); + return (GNUNET_NO == ret) ? MHD_YES : MHD_NO; /* Check if this payment attempt has already succeeded */ if (GNUNET_SYSERR == @@ -1224,18 +1327,13 @@ handler_pay_json (struct MHD_Connection *connection, TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR, "Merchant database error"); } - if (GNUNET_OK != db->find_transaction (db->cls, - &pc->h_proposal_data, - &pc->mi->pubkey, - &transaction_double_check, - NULL)) - GNUNET_break (0); } MHD_suspend_connection (connection); /* Find the responsible exchange, this may take a while... */ pc->fo = TMH_EXCHANGES_find_exchange (pc->chosen_exchange, + pc->mi->wire_method, &process_pay_with_exchange, pc); @@ -1248,7 +1346,7 @@ handler_pay_json (struct MHD_Connection *connection, pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT, &handle_pay_timeout, pc); - return GNUNET_OK; + return MHD_YES; } @@ -1301,11 +1399,8 @@ MH_handler_pay (struct TMH_RequestHandler *rh, res = MHD_queue_response (connection, pc->response_code, pc->response); - if (NULL != pc->response) - { - MHD_destroy_response (pc->response); - pc->response = NULL; - } + 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, @@ -1329,10 +1424,13 @@ MH_handler_pay (struct TMH_RequestHandler *rh, GNUNET_break (0); return TMH_RESPONSE_reply_invalid_json (connection); } - if ((GNUNET_NO == res) || (NULL == root)) + if ( (GNUNET_NO == res) || + (NULL == root) ) return MHD_YES; /* the POST's body has to be further fetched */ - res = handler_pay_json (connection, root, pc); + res = handler_pay_json (connection, + root, + pc); json_decref (root); if (GNUNET_SYSERR == res) return MHD_NO; diff --git a/src/backend/taler-merchant-httpd_proposal.c b/src/backend/taler-merchant-httpd_proposal.c @@ -127,8 +127,9 @@ get_instance (struct json_t *json); * @param order to process * @return MHD result code */ -int -proposal_put (struct MHD_Connection *connection, json_t *order) +static int +proposal_put (struct MHD_Connection *connection, + json_t *order) { int res; struct MerchantInstance *mi; @@ -169,35 +170,67 @@ proposal_put (struct MHD_Connection *connection, json_t *order) time (&timer); tm_info = localtime (&timer); - off = strftime (buf, sizeof (buf), "%H:%M:%S", tm_info); + off = strftime (buf, + sizeof (buf), + "%H:%M:%S", + tm_info); snprintf (buf + off, sizeof (buf) - off, - "-%llX", - (long long unsigned) GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, UINT64_MAX)); - json_object_set (order, "order_id", json_string (buf)); + "-%llX", + (long long unsigned) GNUNET_CRYPTO_random_u64 (GNUNET_CRYPTO_QUALITY_WEAK, + UINT64_MAX)); + json_object_set (order, + "order_id", + json_string (buf)); } - if (NULL == json_string_value (json_object_get (order, "timestamp"))) + if (NULL == json_object_get (order, + "timestamp")) { struct GNUNET_TIME_Absolute now = GNUNET_TIME_absolute_get (); + (void) GNUNET_TIME_round_abs (&now); - json_object_set (order, "timestamp", GNUNET_JSON_from_time_abs (now)); + json_object_set (order, + "timestamp", + GNUNET_JSON_from_time_abs (now)); } - if (NULL == json_string_value (json_object_get (order, "refund_deadline"))) + if (NULL == json_object_get (order, + "refund_deadline")) { struct GNUNET_TIME_Absolute zero = { 0 }; - json_object_set (order, "refund_deadline", GNUNET_JSON_from_time_abs (zero)); + + json_object_set (order, + "refund_deadline", + GNUNET_JSON_from_time_abs (zero)); } - if (NULL == json_string_value (json_object_get (order, "pay_deadline"))) + if (NULL == json_object_get (order, + "pay_deadline")) { struct GNUNET_TIME_Absolute t; + /* FIXME: read the delay for pay_deadline from config */ - t = GNUNET_TIME_absolute_add (GNUNET_TIME_absolute_get (), GNUNET_TIME_UNIT_HOURS); + t = GNUNET_TIME_relative_to_absolute (GNUNET_TIME_UNIT_HOURS); (void) GNUNET_TIME_round_abs (&t); json_object_set (order, "pay_deadline", GNUNET_JSON_from_time_abs (t)); } + if (NULL == json_object_get (order, + "max_wire_fee")) + { + json_object_set (order, + "max_wire_fee", + TALER_JSON_from_amount (&default_max_wire_fee)); + } + + if (NULL == json_object_get (order, + "wire_fee_amortization")) + { + json_object_set (order, + "wire_fee_amortization", + json_integer ((json_int_t) default_wire_fee_amortization)); + } + /* extract fields we need to sign separately */ res = TMH_PARSE_json_data (connection, order, spec); if (GNUNET_NO == res) @@ -210,7 +243,7 @@ proposal_put (struct MHD_Connection *connection, json_t *order) TALER_EC_NONE, "Impossible to parse the order"); } - + /* check contract is well-formed */ if (GNUNET_OK != check_products (products)) @@ -225,7 +258,7 @@ proposal_put (struct MHD_Connection *connection, json_t *order) if (NULL == mi) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, - "Not able to find the specified instance\n"); + "Not able to find the specified instance\n"); return TMH_RESPONSE_reply_not_found (connection, TALER_EC_CONTRACT_INSTANCE_UNKNOWN, "Unknown instance given"); @@ -272,7 +305,7 @@ proposal_put (struct MHD_Connection *connection, json_t *order) TALER_EC_PROPOSAL_STORE_DB_ERROR, "db error: could not store this proposal's data into db"); } - + res = TMH_RESPONSE_reply_json_pack (connection, MHD_HTTP_OK, @@ -347,6 +380,7 @@ MH_handler_proposal_put (struct TMH_RequestHandler *rh, return res; } + /** * Manage a GET /proposal request. Query the db and returns the * proposal's data related to the transaction id given as the URL's @@ -396,7 +430,7 @@ MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, order_id, &mi->pubkey); if (GNUNET_NO == res) - return TMH_RESPONSE_reply_not_found (connection, + return TMH_RESPONSE_reply_not_found (connection, TALER_EC_PROPOSAL_LOOKUP_NOT_FOUND, "unknown transaction id"); @@ -405,10 +439,10 @@ MH_handler_proposal_lookup (struct TMH_RequestHandler *rh, TALER_EC_PROPOSAL_LOOKUP_DB_ERROR, "An error occurred while retrieving proposal data from db"); - + return TMH_RESPONSE_reply_json (connection, proposal_data, - MHD_HTTP_OK); + MHD_HTTP_OK); } diff --git a/src/backend/taler-merchant-httpd_track-transaction.c b/src/backend/taler-merchant-httpd_track-transaction.c @@ -340,6 +340,7 @@ trace_coins (struct TrackTransactionContext *tctx); * not provide any (set only if @a http_status is #MHD_HTTP_OK) * @param total_amount total amount of the wire transfer, or NULL if the exchange could * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange * @param details_length length of the @a details array * @param details array with details about the combined transactions */ @@ -352,16 +353,17 @@ wire_deposits_cb (void *cls, const struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, unsigned int details_length, const struct TALER_TrackTransferDetails *details) - { - struct TrackTransactionContext *tctx = cls; - struct TrackCoinContext *tcc; - unsigned int i; +{ + struct TrackTransactionContext *tctx = cls; + struct TrackCoinContext *tcc; + unsigned int i; - tctx->wdh = NULL; - if (MHD_HTTP_OK != http_status) - { + tctx->wdh = NULL; + if (MHD_HTTP_OK != http_status) + { resume_track_transaction_with_response (tctx, MHD_HTTP_FAILED_DEPENDENCY, @@ -509,7 +511,7 @@ wtid_cb (void *cls, /* WARNING: if two transactions got aggregated under the same WTID, then this branch is always taken (when attempting to track the second transaction). */ - if (GNUNET_OK == + if (GNUNET_OK == db->find_proof_by_wtid (db->cls, tctx->exchange_uri, wtid, @@ -677,11 +679,13 @@ trace_coins (struct TrackTransactionContext *tctx) * * @param cls the `struct TrackTransactionContext` * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config */ static void process_track_transaction_with_exchange (void *cls, struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, int exchange_trusted) { struct TrackTransactionContext *tctx = cls; @@ -985,6 +989,7 @@ MH_handler_track_transaction (struct TMH_RequestHandler *rh, "Suspending /track/transaction handling while working with the exchange\n"); MHD_suspend_connection (connection); tctx->fo = TMH_EXCHANGES_find_exchange (tctx->exchange_uri, + NULL, &process_track_transaction_with_exchange, tctx); diff --git a/src/backend/taler-merchant-httpd_track-transfer.c b/src/backend/taler-merchant-httpd_track-transfer.c @@ -267,6 +267,7 @@ check_transfer (void *cls, * @param execution_time time when the exchange claims to have performed the wire transfer * @param total_amount total amount of the wire transfer, or NULL if the exchange could * not provide any @a wtid (set only if @a http_status is #MHD_HTTP_OK) + * @param wire_fee wire fee that was charged by the exchange * @param details_length length of the @a details array * @param details array with details about the combined transactions */ @@ -279,6 +280,7 @@ wire_transfer_cb (void *cls, const struct GNUNET_HashCode *h_wire, struct GNUNET_TIME_Absolute execution_time, const struct TALER_Amount *total_amount, + const struct TALER_Amount *wire_fee, unsigned int details_length, const struct TALER_TrackTransferDetails *details) { @@ -413,11 +415,13 @@ wire_transfer_cb (void *cls, * * @param cls the `struct TrackTransferContext` * @param eh NULL if exchange was not found to be acceptable + * @param wire_fee NULL (we did not specify a wire method) * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config */ static void process_track_transfer_with_exchange (void *cls, struct TALER_EXCHANGE_Handle *eh, + const struct TALER_Amount *wire_fee, int exchange_trusted) { struct TrackTransferContext *rctx = cls; @@ -621,6 +625,7 @@ MH_handler_track_transfer (struct TMH_RequestHandler *rh, "Suspending /track/transfer handling while working with the exchange\n"); MHD_suspend_connection (connection); rctx->fo = TMH_EXCHANGES_find_exchange (uri, + NULL, &process_track_transfer_with_exchange, rctx); rctx->timeout_task diff --git a/src/backenddb/plugin_merchantdb_postgres.c b/src/backenddb/plugin_merchantdb_postgres.c @@ -1428,7 +1428,7 @@ libtaler_plugin_merchantdb_postgres_init (void *cls) pg->conn = GNUNET_POSTGRES_connect (cfg, "merchantdb-postgres"); if (NULL == pg->conn) { - GNUNET_break (0); + GNUNET_free (pg); return NULL; } plugin = GNUNET_new (struct TALER_MERCHANTDB_Plugin); diff --git a/src/lib/test_merchant_api.c b/src/lib/test_merchant_api.c @@ -565,6 +565,11 @@ struct Command char *expected_transfer_ref; /** + * Wire fee we expect to pay for this transaction. + */ + const char *wire_fee; + + /** * Handle to a /track/transaction operation */ struct TALER_MERCHANT_TrackTransactionHandle *tth; @@ -1315,7 +1320,7 @@ proposal_lookup_cb (void *cls, { struct InterpreterState *is = cls; struct Command *cmd = &is->commands[is->ip]; - + cmd->details.proposal_lookup.plo = NULL; if (cmd->expected_response_code != http_status) @@ -1363,6 +1368,7 @@ track_transaction_cb (void *cls, { const struct Command *ref; struct TALER_Amount ea; + struct TALER_Amount wire_fee; struct TALER_Amount coin_contribution; GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, @@ -1401,9 +1407,16 @@ track_transaction_cb (void *cls, TALER_string_to_amount (ref->details.check_bank_transfer.amount, &ea)); GNUNET_assert (GNUNET_OK == + TALER_string_to_amount (cmd->details.track_transaction.wire_fee, + &wire_fee)); + GNUNET_assert (GNUNET_OK == TALER_amount_subtract (&coin_contribution, &transfers[0].coins[0].amount_with_fee, &transfers[0].coins[0].deposit_fee)); + GNUNET_assert (GNUNET_OK == + TALER_amount_subtract (&coin_contribution, + &coin_contribution, + &wire_fee)); if (0 != TALER_amount_cmp (&ea, &coin_contribution)) @@ -1524,7 +1537,7 @@ interpreter_run (void *cls) GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Switching instance: '%s'\n", instance); - + is->task = GNUNET_SCHEDULER_add_now (interpreter_run, is); return; @@ -1532,11 +1545,11 @@ interpreter_run (void *cls) case OC_PROPOSAL_LOOKUP: { const char *order_id; - + GNUNET_assert (NULL != cmd->details.proposal_lookup.proposal_reference); ref = find_command (is, cmd->details.proposal_lookup.proposal_reference); GNUNET_assert (NULL != ref); - + order_id = json_string_value (json_object_get (ref->details.proposal.proposal_data, "order_id")); @@ -1549,7 +1562,7 @@ interpreter_run (void *cls) proposal_lookup_cb, is))); } - + return; case OC_ADMIN_ADD_INCOMING: @@ -1919,6 +1932,7 @@ interpreter_run (void *cls) &amount, cmd->details.check_bank_transfer.account_debit, cmd->details.check_bank_transfer.account_credit, + EXCHANGE_URI, &cmd->details.check_bank_transfer.wtid)) { GNUNET_break (0); @@ -2372,7 +2386,7 @@ run (void *cls) /* Proposal lookup */ { - .oc = OC_PROPOSAL_LOOKUP, + .oc = OC_PROPOSAL_LOOKUP, .label = "fetch-proposal-2", .expected_response_code = MHD_HTTP_OK, .details.proposal_lookup.proposal_reference = "create-proposal-2" }, @@ -2388,8 +2402,8 @@ run (void *cls) /* Obtain WTID of the transfer */ { .oc = OC_CHECK_BANK_TRANSFER, - .label = "check_bank_transfer-499c", - .details.check_bank_transfer.amount = "EUR:4.99", + .label = "check_bank_transfer-498c", + .details.check_bank_transfer.amount = "EUR:4.98", .details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */ .details.check_bank_transfer.account_credit = 62 /* merchant */ }, @@ -2401,21 +2415,22 @@ run (void *cls) { .oc = OC_TRACK_TRANSACTION, .label = "track-transaction-1", .expected_response_code = MHD_HTTP_OK, - .details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c", - .details.track_transaction.pay_ref = "deposit-simple" + .details.track_transaction.expected_transfer_ref = "check_bank_transfer-498c", + .details.track_transaction.pay_ref = "deposit-simple", + .details.track_transaction.wire_fee = "EUR:0.01", }, /* Trace the WTID back to the original transaction */ { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-1", .expected_response_code = MHD_HTTP_OK, - .details.track_transfer.check_bank_ref = "check_bank_transfer-499c", + .details.track_transfer.check_bank_ref = "check_bank_transfer-498c", .details.track_transfer.expected_pay_ref = "deposit-simple" }, { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-1-again", .expected_response_code = MHD_HTTP_OK, - .details.track_transfer.check_bank_ref = "check_bank_transfer-499c", + .details.track_transfer.check_bank_ref = "check_bank_transfer-498c", .details.track_transfer.expected_pay_ref = "deposit-simple" }, @@ -2434,8 +2449,8 @@ run (void *cls) /* Obtain WTID of the transfer */ { .oc = OC_CHECK_BANK_TRANSFER, - .label = "check_bank_transfer-499c-2", - .details.check_bank_transfer.amount = "EUR:4.99", + .label = "check_bank_transfer-498c-2", + .details.check_bank_transfer.amount = "EUR:4.98", .details.check_bank_transfer.account_debit = 2, /* exchange-outgoing */ .details.check_bank_transfer.account_credit = 62 /* merchant */ }, @@ -2448,21 +2463,22 @@ run (void *cls) { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-2", .expected_response_code = MHD_HTTP_OK, - .details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2", + .details.track_transfer.check_bank_ref = "check_bank_transfer-498c-2", .details.track_transfer.expected_pay_ref = "deposit-simple-2" }, { .oc = OC_TRACK_TRANSFER, .label = "track-transfer-2-again", .expected_response_code = MHD_HTTP_OK, - .details.track_transfer.check_bank_ref = "check_bank_transfer-499c-2", + .details.track_transfer.check_bank_ref = "check_bank_transfer-498c-2", .details.track_transfer.expected_pay_ref = "deposit-simple-2" }, { .oc = OC_TRACK_TRANSACTION, .label = "track-transaction-2", .expected_response_code = MHD_HTTP_OK, - .details.track_transaction.expected_transfer_ref = "check_bank_transfer-499c-2", - .details.track_transaction.pay_ref = "deposit-simple-2" + .details.track_transaction.expected_transfer_ref = "check_bank_transfer-498c-2", + .details.track_transaction.wire_fee = "EUR:0.01", + .details.track_transaction.pay_ref = "deposit-simple-2" }, { .oc = OC_HISTORY, @@ -2476,7 +2492,7 @@ run (void *cls) .label = "history-2", .expected_response_code = MHD_HTTP_OK, /*no records to be returned, as limit is in the future*/ - .details.history.date.abs_value_us = 43 * 1000LL * 1000LL, + .details.history.date.abs_value_us = 43 * 1000LL * 1000LL, .details.history.nresult = 0 }, @@ -2635,8 +2651,7 @@ main (int argc, merchantd = GNUNET_OS_start_process (GNUNET_NO, GNUNET_OS_INHERIT_STD_ALL, NULL, NULL, NULL, - "valgrind", - "valgrind", + "taler-merchant-httpd", "taler-merchant-httpd", "-c", "test_merchant_api.conf", "-L", "DEBUG", diff --git a/src/lib/test_merchant_api.conf b/src/lib/test_merchant_api.conf @@ -19,9 +19,6 @@ CURRENCY = EUR # Which port do we run the backend on? (HTTP server) PORT = 8082 -# FIXME: is this one used? -HOSTNAME = localhost - # How quickly do we want the exchange to send us our money? # Used only if the frontend does not specify a value. # FIXME: EDATE is a bit short, 'execution_delay'? @@ -30,7 +27,7 @@ EDATE = 3 week # Which plugin (backend) do we use for the DB. DB = postgres -# Which wireformat do we use? +# Wire format supported by the merchant. WIREFORMAT = test # This option is a list of instances which are to be used @@ -39,6 +36,25 @@ WIREFORMAT = test # section like X-wireformat and merchant-instance-X INSTANCES = tor default + +[exchange-wire-test] +# Enable 'test' for testing of the actual coin operations. +ENABLE = YES + +# Fees for the forseeable future... +# If you see this after 2017, update to match the next 10 years... +WIRE-FEE-2017 = EUR:0.01 +WIRE-FEE-2018 = EUR:0.01 +WIRE-FEE-2019 = EUR:0.01 +WIRE-FEE-2020 = EUR:0.01 +WIRE-FEE-2021 = EUR:0.01 +WIRE-FEE-2022 = EUR:0.01 +WIRE-FEE-2023 = EUR:0.01 +WIRE-FEE-2024 = EUR:0.01 +WIRE-FEE-2025 = EUR:0.01 +WIRE-FEE-2026 = EUR:0.01 + + [merchant-exchange-test] URI = http://localhost:8081/ MASTER_KEY = 98NJW3CQHZQGQXTY3K85K531XKPAPAVV4Q5V8PYYRR00NJGZWNVG @@ -89,10 +105,10 @@ ADDRESS = "Garching" [exchange] # How to access our database DB = postgres + # HTTP port the exchange listens to PORT = 8081 -# Wire format supported by the exchange -WIREFORMAT = test + # Our public key MASTER_PUBLIC_KEY = T1VVFQZZARQ1CMF4BN58EE7SKTW5AV2BS18S87ZEGYS4S29J6DNG