/*
This file is part of TALER
(C) 2014-2021 Taler Systems SA
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 Foundation; either version 3,
or (at your option) any later version.
TALER is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public
License along with TALER; see the file COPYING. If not,
see
*/
/**
* @file taler-merchant-httpd_post-orders-ID-pay.c
* @brief handling of POST /orders/$ID/pay requests
* @author Marcello Stanisci
* @author Christian Grothoff
* @author Florian Dold
*/
#include "platform.h"
#include
#include
#include
#include
#include "taler-merchant-httpd_auditors.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_post-orders-ID-pay.h"
#include "taler-merchant-httpd_private-get-orders.h"
/**
* How often do we retry the (complex!) database transaction?
*/
#define MAX_RETRIES 5
/**
* Maximum number of coins that we allow per transaction
*/
#define MAX_COIN_ALLOWED_COINS 1024
/**
* Information we keep for an individual call to the pay handler.
*/
struct PayContext;
/**
* Information kept during a pay request for each coin.
*/
struct DepositConfirmation
{
/**
* Reference to the main PayContext
*/
struct PayContext *pc;
/**
* Handle to the deposit operation we are performing for
* this coin, NULL after the operation is done.
*/
struct TALER_EXCHANGE_DepositHandle *dh;
/**
* URL of the exchange that issued this coin.
*/
char *exchange_url;
/**
* Hash of the denomination of this coin.
*/
struct TALER_DenominationHash h_denom;
/**
* Amount this coin contributes to the total purchase price.
* This amount includes the deposit fee.
*/
struct TALER_Amount amount_with_fee;
/**
* Fee charged by the exchange for the deposit operation of this coin.
*/
struct TALER_Amount deposit_fee;
/**
* Fee charged by the exchange for the refund operation of this coin.
*/
struct TALER_Amount refund_fee;
/**
* Wire fee charged by the exchange of this coin.
*/
struct TALER_Amount wire_fee;
/**
* Public key of the coin.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
/**
* Signature using the @e denom key over the @e coin_pub.
*/
struct TALER_DenominationSignature ub_sig;
/**
* Signature of the coin's private key over the contract.
*/
struct TALER_CoinSpendSignatureP coin_sig;
/**
* Offset of this coin into the `dc` array of all coins in the
* @e pc.
*/
unsigned int index;
/**
* true if we found this coin in the database.
*/
bool found_in_db;
};
/**
* Information we keep for an individual call to the /pay handler.
*/
struct PayContext
{
/**
* Stored in a DLL.
*/
struct PayContext *next;
/**
* Stored in a DLL.
*/
struct PayContext *prev;
/**
* Array with @e coins_cnt coins we are despositing.
*/
struct DepositConfirmation *dc;
/**
* MHD connection to return to
*/
struct MHD_Connection *connection;
/**
* Details about the client's request.
*/
struct TMH_HandlerContext *hc;
/**
* What wire method (of the @e mi) was selected by the wallet?
* Set in #parse_pay().
*/
struct TMH_WireMethod *wm;
/**
* Task called when the (suspended) processing for
* the /pay request times out.
* Happens when we don't get a response from the exchange.
*/
struct GNUNET_SCHEDULER_Task *timeout_task;
/**
* Response to return, NULL if we don't have one yet.
*/
struct MHD_Response *response;
/**
* Handle for operation to lookup /keys (and auditors) from
* the exchange used for this transaction; NULL if no operation is
* pending.
*/
struct TMH_EXCHANGES_FindOperation *fo;
/**
* URL of the exchange used for the last @e fo.
*/
const char *current_exchange;
/**
* Placeholder for #TALER_MHD_parse_post_json() to keep its internal state.
*/
void *json_parse_context;
/**
* Optional session id given in @e root.
* NULL if not given.
*/
char *session_id;
/**
* Transaction ID given in @e root.
*/
const char *order_id;
/**
* Fulfillment URL from the contract, or NULL if we don't have one.
*/
char *fulfillment_url;
/**
* Serial number of this order in the database (set once we did the lookup).
*/
uint64_t order_serial;
/**
* Hashed proposal.
*/
struct TALER_PrivateContractHash h_contract_terms;
/**
* "h_wire" from @e contract_terms. Used to identify
* the instance's wire transfer method.
*/
struct TALER_MerchantWireHash h_wire;
/**
* Maximum fee the merchant is willing to pay, from @e root.
* Note that IF the total fee of the exchange is higher, that is
* acceptable to the merchant if the customer is willing to
* pay the difference
* (i.e. amount - max_fee <= actual-amount - actual-fee).
*/
struct TALER_Amount max_fee;
/**
* 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;
/**
* Amount from @e root. This is the amount the merchant expects
* to make, minus @e max_fee.
*/
struct TALER_Amount amount;
/**
* Considering all the coins with the "found_in_db" flag
* set, what is the total amount we were so far paid on
* this contract?
*/
struct TALER_Amount total_paid;
/**
* Considering all the coins with the "found_in_db" flag
* set, what is the total amount we had to pay in deposit
* fees so far on this contract?
*/
struct TALER_Amount total_fees_paid;
/**
* Considering all the coins with the "found_in_db" flag
* set, what is the total amount we already refunded?
*/
struct TALER_Amount total_refunded;
/**
* Wire transfer deadline. How soon would the merchant like the
* wire transfer to be executed?
*/
struct GNUNET_TIME_Absolute wire_transfer_deadline;
/**
* Timestamp from @e contract_terms.
*/
struct GNUNET_TIME_Absolute timestamp;
/**
* Refund deadline from @e contract_terms.
*/
struct GNUNET_TIME_Absolute refund_deadline;
/**
* Deadline for the customer to pay for this proposal.
*/
struct GNUNET_TIME_Absolute pay_deadline;
/**
* 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;
/**
* Number of coins this payment is made of. Length
* of the @e dc array.
*/
unsigned int coins_cnt;
/**
* How often have we retried the 'main' transaction?
*/
unsigned int retry_counter;
/**
* Number of transactions still pending. Initially set to
* @e coins_cnt, decremented on each transaction that
* successfully finished.
*/
unsigned int pending;
/**
* Number of transactions still pending for the currently selected
* exchange. Initially set to the number of coins started at the
* exchange, decremented on each transaction that successfully
* finished. Once it hits zero, we pick the next exchange.
*/
unsigned int pending_at_ce;
/**
* HTTP status code to use for the reply, i.e 200 for "OK".
* Special value UINT_MAX is used to indicate hard errors
* (no reply, return #MHD_NO).
*/
unsigned int response_code;
/**
* #GNUNET_NO if the @e connection was not suspended,
* #GNUNET_YES if the @e connection was suspended,
* #GNUNET_SYSERR if @e connection was resumed to as
* part of #MH_force_pc_resume during shutdown.
*/
enum GNUNET_GenericReturnValue suspended;
/**
* true if we already tried a forced /keys download.
*/
bool tried_force_keys;
};
/**
* Head of active pay context DLL.
*/
static struct PayContext *pc_head;
/**
* Tail of active pay context DLL.
*/
static struct PayContext *pc_tail;
/**
* Compute the timeout for a /pay request based on the number of coins
* involved.
*
* @param num_coins number of coins
* @returns timeout for the /pay request
*/
static struct GNUNET_TIME_Relative
get_pay_timeout (unsigned int num_coins)
{
struct GNUNET_TIME_Relative t;
/* FIXME: Do some benchmarking to come up with a better timeout.
* We've increased this value so the wallet integration test passes again
* on my (Florian) machine.
*/
t = GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS,
15 * (1 + (num_coins / 5)));
return t;
}
/**
* Abort all pending /deposit operations.
*
* @param pc pay context to abort
*/
static void
abort_active_deposits (struct PayContext *pc)
{
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Aborting pending /deposit operations\n");
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dci = &pc->dc[i];
if (NULL != dci->dh)
{
TALER_EXCHANGE_deposit_cancel (dci->dh);
dci->dh = NULL;
}
}
}
void
TMH_force_pc_resume ()
{
for (struct PayContext *pc = pc_head;
NULL != pc;
pc = pc->next)
{
abort_active_deposits (pc);
if (NULL != pc->timeout_task)
{
GNUNET_SCHEDULER_cancel (pc->timeout_task);
pc->timeout_task = NULL;
}
if (GNUNET_YES == pc->suspended)
{
pc->suspended = GNUNET_SYSERR;
MHD_resume_connection (pc->connection);
}
}
}
/**
* Resume the given pay context and send the given response.
* Stores the response in the @a pc and signals MHD to resume
* the connection. Also ensures MHD runs immediately.
*
* @param pc payment context
* @param response_code response code to use
* @param response response data to send back
*/
static void
resume_pay_with_response (struct PayContext *pc,
unsigned int response_code,
struct MHD_Response *response)
{
abort_active_deposits (pc);
pc->response_code = response_code;
pc->response = response;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Resuming /pay handling as exchange interaction is done (%u)\n",
response_code);
if (NULL != pc->timeout_task)
{
GNUNET_SCHEDULER_cancel (pc->timeout_task);
pc->timeout_task = NULL;
}
GNUNET_assert (GNUNET_YES == pc->suspended);
pc->suspended = GNUNET_NO;
MHD_resume_connection (pc->connection);
TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
}
/**
* Resume payment processing with an error.
*
* @param pc operation to resume
* @param http_status http status code to return
* @param ec taler error code to return
* @param msg human readable error message
*/
static void
resume_pay_with_error (struct PayContext *pc,
unsigned int http_status,
enum TALER_ErrorCode ec,
const char *msg)
{
resume_pay_with_response (pc,
http_status,
TALER_MHD_make_error (ec,
msg));
}
/**
* Custom cleanup routine for a `struct PayContext`.
*
* @param cls the `struct PayContext` to clean up.
*/
static void
pay_context_cleanup (void *cls)
{
struct PayContext *pc = cls;
if (NULL != pc->timeout_task)
{
GNUNET_SCHEDULER_cancel (pc->timeout_task);
pc->timeout_task = NULL;
}
abort_active_deposits (pc);
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
TALER_denom_sig_free (&dc->ub_sig);
GNUNET_free (dc->exchange_url);
}
GNUNET_free (pc->dc);
if (NULL != pc->fo)
{
TMH_EXCHANGES_find_exchange_cancel (pc->fo);
pc->fo = NULL;
}
if (NULL != pc->response)
{
MHD_destroy_response (pc->response);
pc->response = NULL;
}
GNUNET_free (pc->fulfillment_url);
GNUNET_free (pc->session_id);
GNUNET_CONTAINER_DLL_remove (pc_head,
pc_tail,
pc);
GNUNET_free (pc);
}
/**
* Find the exchange we need to talk to for the next
* pending deposit permission.
*
* @param pc payment context we are processing
*/
static void
find_next_exchange (struct PayContext *pc);
/**
* Execute the DB transaction. If required (from
* soft/serialization errors), the transaction can be
* restarted here.
*
* @param pc payment context to transact
*/
static void
execute_pay_transaction (struct PayContext *pc);
/**
* Callback to handle a deposit permission's response.
*
* @param cls a `struct DepositConfirmation` (i.e. a pointer
* into the global array of confirmations and an index for this call
* in that array). That way, the last executed callback can detect
* that no other confirmations are on the way, and can pack a response
* for the wallet
* @param hr HTTP response code details
* @param deposit_timestamp time when the exchange generated the deposit confirmation
* @param exchange_sig signature from the exchange over the deposit confirmation
* @param exchange_pub which key did the exchange use to create the @a exchange_sig
*/
static void
deposit_cb (void *cls,
const struct TALER_EXCHANGE_DepositResult *dr)
{
struct DepositConfirmation *dc = cls;
struct PayContext *pc = dc->pc;
dc->dh = NULL;
GNUNET_assert (GNUNET_YES == pc->suspended);
pc->pending_at_ce--;
switch (dr->hr.http_status)
{
case MHD_HTTP_OK:
{
/* store result to DB */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Storing successful payment %s (%s) at instance `%s'\n",
pc->hc->infix,
GNUNET_h2s (&pc->h_contract_terms.hash),
pc->hc->instance->settings.id);
TMH_db->preflight (TMH_db->cls);
{
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->insert_deposit (TMH_db->cls,
pc->hc->instance->settings.id,
dr->details.success.deposit_timestamp,
&pc->h_contract_terms,
&dc->coin_pub,
dc->exchange_url,
&dc->amount_with_fee,
&dc->deposit_fee,
&dc->refund_fee,
&dc->wire_fee,
&pc->wm->h_wire,
dr->details.success.exchange_sig,
dr->details.success.exchange_pub);
if (0 > qs)
{
/* Special report if retries insufficient */
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
execute_pay_transaction (pc);
return;
}
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
/* Forward error including 'proof' for the body */
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"deposit");
return;
}
}
dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */
pc->pending--;
if (0 != pc->pending_at_ce)
return; /* still more to do with current exchange */
find_next_exchange (pc);
return;
}
default:
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Deposit operation failed with HTTP code %u/%d\n",
dr->hr.http_status,
(int) dr->hr.ec);
/* Transaction failed */
if (5 == dr->hr.http_status / 100)
{
/* internal server error at exchange */
resume_pay_with_response (pc,
MHD_HTTP_BAD_GATEWAY,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS),
TMH_pack_exchange_reply (&dr->hr)));
return;
}
if (NULL == dr->hr.reply)
{
/* We can't do anything meaningful here, the exchange did something wrong */
resume_pay_with_response (
pc,
MHD_HTTP_BAD_GATEWAY,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_GENERIC_EXCHANGE_REPLY_MALFORMED),
TMH_pack_exchange_reply (&dr->hr)));
return;
}
/* Forward error, adding the "coin_pub" for which the
error was being generated */
if (TALER_EC_EXCHANGE_DEPOSIT_INSUFFICIENT_FUNDS == dr->hr.ec)
{
resume_pay_with_response (
pc,
MHD_HTTP_CONFLICT,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_FUNDS),
TMH_pack_exchange_reply (&dr->hr),
GNUNET_JSON_pack_data_auto ("coin_pub",
&dc->coin_pub)));
return;
}
resume_pay_with_response (
pc,
MHD_HTTP_BAD_GATEWAY,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNEXPECTED_STATUS),
TMH_pack_exchange_reply (&dr->hr),
GNUNET_JSON_pack_data_auto ("coin_pub",
&dc->coin_pub)));
return;
} /* end switch */
}
/**
* Function called with the result of our exchange lookup.
*
* @param cls the `struct PayContext`
* @param hr HTTP response details
* @param exchange_handle NULL if exchange was not found to be acceptable
* @param payto_uri payto://-URI of the exchange
* @param wire_fee current applicable fee for dealing with @a exchange_handle,
* NULL if not available
* @param exchange_trusted true if this exchange is
* trusted by config
*/
static void
process_pay_with_exchange (void *cls,
const struct TALER_EXCHANGE_HttpResponse *hr,
struct TALER_EXCHANGE_Handle *exchange_handle,
const char *payto_uri,
const struct TALER_Amount *wire_fee,
bool exchange_trusted)
{
struct PayContext *pc = cls;
struct TMH_HandlerContext *hc = pc->hc;
const struct TALER_EXCHANGE_Keys *keys;
(void) payto_uri;
pc->fo = NULL;
GNUNET_assert (GNUNET_YES == pc->suspended);
if (NULL == hr)
{
resume_pay_with_response (
pc,
MHD_HTTP_GATEWAY_TIMEOUT,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT)));
return;
}
if ( (MHD_HTTP_OK != hr->http_status) ||
(NULL == exchange_handle) )
{
resume_pay_with_response (
pc,
MHD_HTTP_BAD_GATEWAY,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_GENERIC_EXCHANGE_CONNECT_FAILURE),
TMH_pack_exchange_reply (hr)));
return;
}
keys = TALER_EXCHANGE_get_keys (exchange_handle);
if (NULL == keys)
{
GNUNET_break (0); /* should not be possible if HTTP status is #MHD_HTTP_OK */
resume_pay_with_error (pc,
MHD_HTTP_BAD_GATEWAY,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_KEYS_FAILURE,
NULL);
return;
}
/* Initiate /deposit operation for all coins of
the current exchange (!) */
GNUNET_assert (0 == pc->pending_at_ce);
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
const struct TALER_EXCHANGE_DenomPublicKey *denom_details;
enum TALER_ErrorCode ec;
unsigned int http_status;
if (NULL != dc->dh)
continue; /* we were here before (can happen due to
tried_force_keys logic), don't go again */
if (dc->found_in_db)
continue;
if (0 != strcmp (dc->exchange_url,
pc->current_exchange))
continue;
denom_details
= TALER_EXCHANGE_get_denomination_key_by_hash (keys,
&dc->h_denom);
if (NULL == denom_details)
{
if (! pc->tried_force_keys)
{
/* let's try *forcing* a re-download of /keys from the exchange.
Maybe the wallet has seen /keys that we missed. */
pc->tried_force_keys = true;
pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange,
pc->wm->wire_method,
GNUNET_YES,
&process_pay_with_exchange,
pc);
if (NULL != pc->fo)
return;
}
/* Forcing failed or we already did it, give up */
resume_pay_with_response (
pc,
MHD_HTTP_BAD_REQUEST,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_KEY_NOT_FOUND),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&dc->h_denom),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_incref (
"exchange_keys",
(json_t *) TALER_EXCHANGE_get_keys_raw (exchange_handle)))));
return;
}
if (GNUNET_OK !=
TMH_AUDITORS_check_dk (exchange_handle,
denom_details,
exchange_trusted,
&http_status,
&ec))
{
if (! pc->tried_force_keys)
{
/* let's try *forcing* a re-download of /keys from the exchange.
Maybe the wallet has seen auditors that we missed. */
pc->tried_force_keys = true;
pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange,
pc->wm->wire_method,
GNUNET_YES,
&process_pay_with_exchange,
pc);
if (NULL != pc->fo)
return;
}
resume_pay_with_response (
pc,
http_status,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (ec),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&denom_details->h_key)));
return;
}
dc->deposit_fee = denom_details->fee_deposit;
dc->refund_fee = denom_details->fee_refund;
dc->wire_fee = *wire_fee;
GNUNET_assert (NULL != pc->wm);
GNUNET_assert (NULL != pc->wm->j_wire);
TMH_db->preflight (TMH_db->cls);
dc->dh = TALER_EXCHANGE_deposit (exchange_handle,
&dc->amount_with_fee,
pc->wire_transfer_deadline,
pc->wm->j_wire,
&pc->h_contract_terms,
NULL, /* FIXME-Oec */
&dc->coin_pub,
&dc->ub_sig,
&denom_details->key,
pc->timestamp,
&hc->instance->merchant_pub,
pc->refund_deadline,
&dc->coin_sig,
&deposit_cb,
dc,
&ec);
if (NULL == dc->dh)
{
/* Signature was invalid or some other constraint was not satisfied. If
the exchange was unavailable, we'd get that information in the
callback. */
GNUNET_break_op (0);
resume_pay_with_response (
pc,
TALER_ErrorCode_get_http_status_safe (ec),
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (ec),
GNUNET_JSON_pack_uint64 ("coin_idx",
i)));
return;
}
if (TMH_force_audit)
TALER_EXCHANGE_deposit_force_dc (dc->dh);
pc->pending_at_ce++;
}
}
/**
* Find the exchange we need to talk to for the next
* pending deposit permission.
*
* @param pc payment context we are processing
*/
static void
find_next_exchange (struct PayContext *pc)
{
GNUNET_assert (0 == pc->pending_at_ce);
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (dc->found_in_db)
continue;
pc->current_exchange = dc->exchange_url;
pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange,
pc->wm->wire_method,
GNUNET_NO,
&process_pay_with_exchange,
pc);
if (NULL == pc->fo)
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_LOOKUP_FAILED,
"Failed to lookup exchange by URL");
return;
}
return;
}
pc->current_exchange = NULL;
/* We are done with all the HTTP requests, go back and try
the 'big' database transaction! (It should work now!) */
GNUNET_assert (0 == pc->pending);
execute_pay_transaction (pc);
}
/**
* Function called with information about a coin that was deposited.
*
* @param cls closure
* @param exchange_url exchange where @a coin_pub was deposited
* @param coin_pub public key of the coin
* @param amount_with_fee amount the exchange will deposit for this coin
* @param deposit_fee fee the exchange will charge for this coin
* @param refund_fee fee the exchange will charge for refunding this coin
* @param wire_fee wire fee the exchange of this coin charges
*/
static void
check_coin_paid (void *cls,
const char *exchange_url,
const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *amount_with_fee,
const struct TALER_Amount *deposit_fee,
const struct TALER_Amount *refund_fee,
const struct TALER_Amount *wire_fee)
{
struct PayContext *pc = cls;
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (dc->found_in_db)
continue; /* processed earlier, skip "expensive" memcmp() */
/* Get matching coin from results*/
if ( (0 != GNUNET_memcmp (coin_pub,
&dc->coin_pub)) ||
(0 !=
strcmp (exchange_url,
dc->exchange_url)) ||
(0 != TALER_amount_cmp (amount_with_fee,
&dc->amount_with_fee)) )
continue; /* does not match, skip */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Deposit of coin `%s' already in our DB.\n",
TALER_B2S (coin_pub));
GNUNET_assert (0 <=
TALER_amount_add (&pc->total_paid,
&pc->total_paid,
amount_with_fee));
GNUNET_assert (0 <=
TALER_amount_add (&pc->total_fees_paid,
&pc->total_fees_paid,
deposit_fee));
dc->deposit_fee = *deposit_fee;
dc->refund_fee = *refund_fee;
dc->wire_fee = *wire_fee;
dc->amount_with_fee = *amount_with_fee;
dc->found_in_db = true;
pc->pending--;
}
}
/**
* Function called with information about a refund. Check if this coin was
* claimed by the wallet for the transaction, and if so add the refunded
* amount to the pc's "total_refunded" amount.
*
* @param cls closure with a `struct PayContext`
* @param coin_pub public coin from which the refund comes from
* @param refund_amount refund amount which is being taken from @a coin_pub
*/
static void
check_coin_refunded (void *cls,
const struct TALER_CoinSpendPublicKeyP *coin_pub,
const struct TALER_Amount *refund_amount)
{
struct PayContext *pc = cls;
/* We look at refunds here that apply to the coins
that the customer is currently trying to pay us with.
Such refunds are not "normal" refunds, but abort-pay refunds, which are
given in the case that the wallet aborts the payment.
In the case the wallet then decides to complete the payment *after* doing
an abort-pay refund (an unusual but possible case), we need
to make sure that existing refunds are accounted for. */
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
/* Get matching coins from results. */
if (0 != GNUNET_memcmp (coin_pub,
&dc->coin_pub))
continue;
GNUNET_assert (0 <=
TALER_amount_add (&pc->total_refunded,
&pc->total_refunded,
refund_amount));
break;
}
}
/**
* Check whether the amount paid is sufficient to cover the price.
*
* @param pc payment context to check
* @return true if the payment is sufficient, false if it is
* insufficient
*/
static bool
check_payment_sufficient (struct PayContext *pc)
{
struct TALER_Amount acc_fee;
struct TALER_Amount acc_amount;
struct TALER_Amount final_amount;
struct TALER_Amount wire_fee_delta;
struct TALER_Amount wire_fee_customer_contribution;
struct TALER_Amount total_wire_fee;
struct TALER_Amount total_needed;
if (0 == pc->coins_cnt)
{
return ((0 == pc->amount.value) &&
(0 == pc->amount.fraction));
}
acc_fee = pc->dc[0].deposit_fee;
total_wire_fee = pc->dc[0].wire_fee;
acc_amount = pc->dc[0].amount_with_fee;
/**
* This loops calculates what are the deposit fee / total
* amount with fee / and wire fee, for all the coins.
*/
for (unsigned int i = 1; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
GNUNET_assert (dc->found_in_db);
if ( (0 >
TALER_amount_add (&acc_fee,
&dc->deposit_fee,
&acc_fee)) ||
(0 >
TALER_amount_add (&acc_amount,
&dc->amount_with_fee,
&acc_amount)) )
{
GNUNET_break (0);
/* Overflow in these amounts? Very strange. */
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW,
"Overflow adding up amounts");
return false;
}
if (1 ==
TALER_amount_cmp (&dc->deposit_fee,
&dc->amount_with_fee))
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT,
"Deposit fees exceed coin's contribution");
return false;
}
/* If exchange differs, add wire fee */
{
bool new_exchange = true;
for (unsigned int j = 0; jexchange_url,
pc->dc[j].exchange_url))
{
new_exchange = false;
break;
}
if (! new_exchange)
continue;
if (GNUNET_OK !=
TALER_amount_cmp_currency (&total_wire_fee,
&dc->wire_fee))
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
MHD_HTTP_CONFLICT,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
total_wire_fee.currency);
return false;
}
if (0 >
TALER_amount_add (&total_wire_fee,
&total_wire_fee,
&dc->wire_fee))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_WIRE_FEE_ADDITION_FAILED,
"could not add exchange wire fee to total");
return false;
}
}
} /* deposit loop */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Amount received from wallet: %s\n",
TALER_amount2s (&acc_amount));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Deposit fee for all coins: %s\n",
TALER_amount2s (&acc_fee));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Total wire fee: %s\n",
TALER_amount2s (&total_wire_fee));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Max wire fee: %s\n",
TALER_amount2s (&pc->max_wire_fee));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Deposit fee limit for merchant: %s\n",
TALER_amount2s (&pc->max_fee));
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Total refunded amount: %s\n",
TALER_amount2s (&pc->total_refunded));
/* Now compare exchange wire fee compared to
* what we are willing to pay */
if (GNUNET_YES !=
TALER_amount_cmp_currency (&total_wire_fee,
&pc->max_wire_fee))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_CONFLICT,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
total_wire_fee.currency);
return false;
}
switch (TALER_amount_subtract (&wire_fee_delta,
&total_wire_fee,
&pc->max_wire_fee))
{
case TALER_AAR_RESULT_POSITIVE:
/* 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);
break;
case TALER_AAR_RESULT_ZERO:
case TALER_AAR_INVALID_NEGATIVE_RESULT:
/* Wire fee threshold is still above the wire fee amount.
Customer is not going to contribute on this. */
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (total_wire_fee.currency,
&wire_fee_customer_contribution));
break;
default:
GNUNET_assert (0);
}
/* add wire fee contribution to the total fees */
if (0 >
TALER_amount_add (&acc_fee,
&acc_fee,
&wire_fee_customer_contribution))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW,
"Overflow adding up amounts");
return false;
}
if (-1 == TALER_amount_cmp (&pc->max_fee,
&acc_fee))
{
/**
* Sum of fees of *all* the different exchanges of all the coins are
* higher than the fixed limit that the merchant is willing to pay. The
* difference must be paid by the customer.
*///
struct TALER_Amount excess_fee;
/* compute fee amount to be covered by customer */
GNUNET_assert (TALER_AAR_RESULT_POSITIVE ==
TALER_amount_subtract (&excess_fee,
&acc_fee,
&pc->max_fee));
/* add that to the total */
if (0 >
TALER_amount_add (&total_needed,
&excess_fee,
&pc->amount))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW,
"Overflow adding up amounts");
return false;
}
}
else
{
/* Fees are fully covered by the merchant, all we require
is that the total payment is not below the contract's amount */
total_needed = pc->amount;
}
/* Do not count refunds towards the payment */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Subtracting total refunds from paid amount: %s\n",
TALER_amount2s (&pc->total_refunded));
if (0 >
TALER_amount_subtract (&final_amount,
&acc_amount,
&pc->total_refunded))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDS_EXCEED_PAYMENTS,
"refunded amount exceeds total payments");
return false;
}
if (-1 == TALER_amount_cmp (&final_amount,
&total_needed))
{
/* acc_amount < total_needed */
if (-1 < TALER_amount_cmp (&acc_amount,
&total_needed))
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
MHD_HTTP_PAYMENT_REQUIRED,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED,
"contract not paid up due to refunds");
}
else if (-1 < TALER_amount_cmp (&acc_amount,
&pc->amount))
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
MHD_HTTP_NOT_ACCEPTABLE,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES,
"contract not paid up due to fees (client may have calculated them badly)");
}
else
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
MHD_HTTP_NOT_ACCEPTABLE,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT,
"payment insufficient");
}
return false;
}
return true;
}
/**
* Use database to notify other clients about the
* payment being completed.
*
* @param pc context to trigger notification for
*/
static void
trigger_payment_notification (struct PayContext *pc)
{
{
struct TMH_OrderPayEventP pay_eh = {
.header.size = htons (sizeof (pay_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_ORDER_PAID),
.merchant_pub = pc->hc->instance->merchant_pub
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Notifying clients about payment of order %s\n",
pc->order_id);
GNUNET_CRYPTO_hash (pc->order_id,
strlen (pc->order_id),
&pay_eh.h_order_id);
TMH_db->event_notify (TMH_db->cls,
&pay_eh.header,
NULL,
0);
}
if ( (NULL != pc->session_id) &&
(NULL != pc->fulfillment_url) )
{
struct TMH_SessionEventP session_eh = {
.header.size = htons (sizeof (session_eh)),
.header.type = htons (TALER_DBEVENT_MERCHANT_SESSION_CAPTURED),
.merchant_pub = pc->hc->instance->merchant_pub
};
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Notifying clients about session change to %s for %s\n",
pc->session_id,
pc->fulfillment_url);
GNUNET_CRYPTO_hash (pc->session_id,
strlen (pc->session_id),
&session_eh.h_session_id);
GNUNET_CRYPTO_hash (pc->fulfillment_url,
strlen (pc->fulfillment_url),
&session_eh.h_fulfillment_url);
TMH_db->event_notify (TMH_db->cls,
&session_eh.header,
NULL,
0);
}
}
/**
*
*
*/
static void
execute_pay_transaction (struct PayContext *pc)
{
struct TMH_HandlerContext *hc = pc->hc;
const char *instance_id = hc->instance->settings.id;
/* Avoid re-trying transactions on soft errors forever! */
if (pc->retry_counter++ > MAX_RETRIES)
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_SOFT_FAILURE,
NULL);
return;
}
GNUNET_assert (GNUNET_YES == pc->suspended);
/* Initialize some amount accumulators
(used in check_coin_paid(), check_coin_refunded()
and check_payment_sufficient()). */
GNUNET_break (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&pc->total_paid));
GNUNET_break (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&pc->total_fees_paid));
GNUNET_break (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&pc->total_refunded));
for (unsigned int i = 0; icoins_cnt; i++)
pc->dc[i].found_in_db = false;
pc->pending = pc->coins_cnt;
/* First, try to see if we have all we need already done */
TMH_db->preflight (TMH_db->cls);
if (GNUNET_OK !=
TMH_db->start (TMH_db->cls,
"run pay"))
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_START_FAILED,
NULL);
return;
}
{
enum GNUNET_DB_QueryStatus qs;
/* Check if some of these coins already succeeded for _this_ contract. */
qs = TMH_db->lookup_deposits (TMH_db->cls,
instance_id,
&pc->h_contract_terms,
&check_coin_paid,
pc);
if (0 > qs)
{
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
execute_pay_transaction (pc);
return;
}
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup deposits");
return;
}
}
{
enum GNUNET_DB_QueryStatus qs;
/* Check if we refunded some of the coins */
qs = TMH_db->lookup_refunds (TMH_db->cls,
instance_id,
&pc->h_contract_terms,
&check_coin_refunded,
pc);
if (0 > qs)
{
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
execute_pay_transaction (pc);
return;
}
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup refunds");
return;
}
}
/* Check if there are coins that still need to be processed */
if (0 != pc->pending)
{
/* we made no DB changes, so we can just rollback */
TMH_db->rollback (TMH_db->cls);
/* Ok, we need to first go to the network to process more coins.
We that interaction in *tiny* transactions (hence the rollback
above). */
find_next_exchange (pc);
return;
}
/* 0 == pc->pending: all coins processed, let's see if that was enough */
if (! check_payment_sufficient (pc))
{
/* check_payment_sufficient() will have queued an error already.
We need to still abort the transaction. */
TMH_db->rollback (TMH_db->cls);
return;
}
/* Payment succeeded, save in database */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order `%s' (%s) was fully paid\n",
pc->order_id,
GNUNET_h2s (&pc->h_contract_terms.hash));
{
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->mark_contract_paid (TMH_db->cls,
instance_id,
&pc->h_contract_terms,
pc->session_id);
if (qs < 0)
{
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
execute_pay_transaction (pc);
return;
}
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"mark contract paid");
return;
}
}
{
enum GNUNET_DB_QueryStatus qs;
/* Now commit! */
qs = TMH_db->commit (TMH_db->cls);
if (0 > qs)
{
/* commit failed */
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
execute_pay_transaction (pc);
return;
}
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_COMMIT_FAILED,
NULL);
return;
}
trigger_payment_notification (pc);
}
/* Generate response (payment successful) */
{
struct GNUNET_CRYPTO_EddsaSignature sig;
/* Sign on our end (as the payment did go through, even if it may
have been refunded already) */
{
struct TALER_PaymentResponsePS mr = {
.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK),
.purpose.size = htonl (sizeof (mr)),
.h_contract_terms = pc->h_contract_terms
};
GNUNET_CRYPTO_eddsa_sign (&pc->hc->instance->merchant_priv.eddsa_priv,
&mr,
&sig);
}
/* Build the response */
resume_pay_with_response (
pc,
MHD_HTTP_OK,
TALER_MHD_MAKE_JSON_PACK (
GNUNET_JSON_pack_data_auto ("sig",
&sig)));
}
}
/**
* Try to parse the pay request into the given pay context.
* Schedules an error response in the connection on failure.
*
* @param connection HTTP connection we are receiving payment on
* @param[in,out] hc context with further information about the request
* @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 enum GNUNET_GenericReturnValue
parse_pay (struct MHD_Connection *connection,
struct TMH_HandlerContext *hc,
struct PayContext *pc)
{
/* First, parse request */
{
const char *session_id = NULL;
json_t *coins;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_json ("coins",
&coins),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("session_id",
&session_id)),
GNUNET_JSON_spec_end ()
};
{
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (connection,
hc->request_body,
spec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
return res;
}
}
/* copy session ID (if set) */
if (NULL != session_id)
{
pc->session_id = GNUNET_strdup (session_id);
}
else
{
/* use empty string as default if client didn't specify it */
pc->session_id = GNUNET_strdup ("");
}
if (! json_is_array (coins))
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MISSING,
"'coins' must be an array"))
? GNUNET_NO
: GNUNET_SYSERR;
}
pc->coins_cnt = json_array_size (coins);
if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS)
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return (MHD_YES == TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'coins' array too long"))
? GNUNET_NO
: GNUNET_SYSERR;
}
/* note: 1 coin = 1 deposit confirmation expected */
pc->dc = GNUNET_new_array (pc->coins_cnt,
struct DepositConfirmation);
/* This loop populates the array 'dc' in 'pc' */
{
unsigned int coins_index;
json_t *coin;
json_array_foreach (coins, coins_index, coin)
{
struct DepositConfirmation *dc = &pc->dc[coins_index];
const char *exchange_url;
struct GNUNET_JSON_Specification ispec[] = {
GNUNET_JSON_spec_fixed_auto ("coin_sig",
&dc->coin_sig),
GNUNET_JSON_spec_fixed_auto ("coin_pub",
&dc->coin_pub),
TALER_JSON_spec_denom_sig ("ub_sig",
&dc->ub_sig),
GNUNET_JSON_spec_fixed_auto ("h_denom",
&dc->h_denom),
TALER_JSON_spec_amount ("contribution",
TMH_currency,
&dc->amount_with_fee),
GNUNET_JSON_spec_string ("exchange_url",
&exchange_url),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (connection,
coin,
ispec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return res;
}
for (unsigned int j = 0; jcoin_pub,
&pc->dc[j].coin_pub))
{
GNUNET_break_op (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"duplicate coin in list"))
? GNUNET_NO
: GNUNET_SYSERR;
}
}
dc->exchange_url = GNUNET_strdup (exchange_url);
dc->index = coins_index;
dc->pc = pc;
if (0 !=
strcasecmp (dc->amount_with_fee.currency,
TMH_currency))
{
GNUNET_break_op (0);
GNUNET_JSON_parse_free (spec);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_CONFLICT,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
TMH_currency);
}
}
}
GNUNET_JSON_parse_free (spec);
}
/* obtain contract terms */
{
enum GNUNET_DB_QueryStatus qs;
json_t *contract_terms = NULL;
qs = TMH_db->lookup_contract_terms (TMH_db->cls,
hc->instance->settings.id,
pc->order_id,
&contract_terms,
&pc->order_serial,
NULL);
if (0 > qs)
{
/* single, read-only SQL statements should never cause
serialization problems */
GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
/* Always report on hard error to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"contract terms"))
? GNUNET_NO
: GNUNET_SYSERR;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
pc->order_id))
? GNUNET_NO
: GNUNET_SYSERR;
}
/* hash contract (needed later) */
if (GNUNET_OK !=
TALER_JSON_contract_hash (contract_terms,
&pc->h_contract_terms))
{
GNUNET_break (0);
json_decref (contract_terms);
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
NULL))
? GNUNET_NO
: GNUNET_SYSERR;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Handling payment for order `%s' with contract hash `%s'\n",
pc->order_id,
GNUNET_h2s (&pc->h_contract_terms.hash));
/* basic sanity check on the contract */
if (NULL == json_object_get (contract_terms,
"merchant"))
{
/* invalid contract */
GNUNET_break (0);
json_decref (contract_terms);
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING,
NULL))
? GNUNET_NO
: GNUNET_SYSERR;
}
/* Get details from contract and check fundamentals */
{
const char *fulfillment_url = NULL;
struct GNUNET_JSON_Specification espec[] = {
TALER_JSON_spec_amount ("amount",
TMH_currency,
&pc->amount),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("fulfillment_url",
&fulfillment_url)),
TALER_JSON_spec_amount ("max_fee",
TMH_currency,
&pc->max_fee),
TALER_JSON_spec_amount ("max_wire_fee",
TMH_currency,
&pc->max_wire_fee),
GNUNET_JSON_spec_uint32 ("wire_fee_amortization",
&pc->wire_fee_amortization),
TALER_JSON_spec_absolute_time ("timestamp",
&pc->timestamp),
TALER_JSON_spec_absolute_time ("refund_deadline",
&pc->refund_deadline),
TALER_JSON_spec_absolute_time ("pay_deadline",
&pc->pay_deadline),
TALER_JSON_spec_absolute_time ("wire_transfer_deadline",
&pc->wire_transfer_deadline),
GNUNET_JSON_spec_fixed_auto ("h_wire",
&pc->h_wire),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_internal_json_data (connection,
contract_terms,
espec);
if (NULL != fulfillment_url)
pc->fulfillment_url = GNUNET_strdup (fulfillment_url);
json_decref (contract_terms);
if (GNUNET_YES != res)
{
GNUNET_break (0);
return res;
}
}
if (pc->wire_transfer_deadline.abs_value_us <
pc->refund_deadline.abs_value_us)
{
/* This should already have been checked when creating the order! */
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUND_DEADLINE_PAST_WIRE_TRANSFER_DEADLINE,
NULL);
}
if (GNUNET_TIME_absolute_is_past (pc->pay_deadline))
{
/* too late */
return (MHD_YES ==
TALER_MHD_reply_with_error (connection,
MHD_HTTP_GONE,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED,
NULL))
? GNUNET_NO
: GNUNET_SYSERR;
}
}
/* Make sure wire method (still) exists for this instance */
{
struct TMH_WireMethod *wm;
wm = hc->instance->wm_head;
while (0 != GNUNET_memcmp (&pc->h_wire,
&wm->h_wire))
wm = wm->next;
if (NULL == wm)
{
GNUNET_break (0);
return TALER_MHD_reply_with_error (connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN,
NULL);
}
pc->wm = wm;
}
return GNUNET_OK;
}
/**
* Handle a timeout for the processing of the pay request.
*
* @param cls our `struct PayContext`
*/
static void
handle_pay_timeout (void *cls)
{
struct PayContext *pc = cls;
pc->timeout_task = NULL;
GNUNET_assert (GNUNET_YES == pc->suspended);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Resuming pay with error after timeout\n");
if (NULL != pc->fo)
{
TMH_EXCHANGES_find_exchange_cancel (pc->fo);
pc->fo = NULL;
}
resume_pay_with_error (pc,
MHD_HTTP_GATEWAY_TIMEOUT,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
NULL);
}
MHD_RESULT
TMH_post_orders_ID_pay (const struct TMH_RequestHandler *rh,
struct MHD_Connection *connection,
struct TMH_HandlerContext *hc)
{
struct PayContext *pc = hc->ctx;
GNUNET_assert (NULL != hc->infix);
if (NULL == pc)
{
pc = GNUNET_new (struct PayContext);
GNUNET_CONTAINER_DLL_insert (pc_head,
pc_tail,
pc);
pc->connection = connection;
pc->hc = hc;
pc->order_id = hc->infix;
hc->ctx = pc;
hc->cc = &pay_context_cleanup;
}
if (GNUNET_SYSERR == pc->suspended)
return MHD_NO; /* during shutdown, we don't generate any more replies */
GNUNET_assert (GNUNET_NO == pc->suspended);
if (0 != pc->response_code)
{
/* We are *done* processing the request, just queue the response (!) */
if (UINT_MAX == pc->response_code)
{
GNUNET_break (0);
return MHD_NO; /* hard error */
}
return MHD_queue_response (connection,
pc->response_code,
pc->response);
}
{
enum GNUNET_GenericReturnValue ret;
ret = parse_pay (connection,
hc,
pc);
if (GNUNET_OK != ret)
return (GNUNET_NO == ret)
? MHD_YES
: MHD_NO;
}
/* Payment not finished, suspend while we interact with the exchange */
MHD_suspend_connection (connection);
pc->suspended = GNUNET_YES;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Suspending pay handling while working with the exchange\n");
GNUNET_assert (NULL == pc->timeout_task);
pc->timeout_task
= GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt),
&handle_pay_timeout,
pc);
execute_pay_transaction (pc);
return MHD_YES;
}
/* end of taler-merchant-httpd_post-orders-ID-pay.c */