/*
This file is part of TALER
(C) 2014-2023 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_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
/**
* How often do we ask the exchange again about our
* KYC status? Very rarely, as if the user actively
* changes it, we should usually notice anyway.
*/
#define KYC_RETRY_FREQUENCY GNUNET_TIME_UNIT_WEEKS
/**
* 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;
/**
* URL of the exchange that issued this coin.
*/
char *exchange_url;
/**
* Details about the coin being deposited.
*/
struct TALER_EXCHANGE_CoinDepositDetail cdd;
/**
* 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;
/**
* If a minimum age was required (i. e. pc->minimum_age is large enough),
* this is the signature of the minimum age (as a single uint8_t), using the
* private key to the corresponding age group. Might be all zeroes for no
* age attestation.
*/
struct TALER_AgeAttestation minimum_age_sig;
/**
* If a minimum age was required (i. e. pc->minimum_age is large enough),
* this is the age commitment (i. e. age mask and vector of EdDSA public
* keys, one per age group) that went into the mining of the coin. The
* SHA256 hash of the mask and the vector of public keys was bound to the
* key.
*/
struct TALER_AgeCommitment age_commitment;
/**
* Age mask in the denomination that defines the age groups. Only
* applicable, if minimum age was required.
*/
struct TALER_AgeMask age_mask;
/**
* Offset of this coin into the `dc` array of all coins in the
* @e pc.
*/
unsigned int index;
/**
* true, if no field "age_commitment" was found in the JSON blob
*/
bool no_age_commitment;
/**
* True, if no field "minimum_age_sig" was found in the JSON blob
*/
bool no_minimum_age_sig;
/**
* true, if no field "h_age_commitment" was found in the JSON blob
*/
bool no_h_age_commitment;
/**
* true if we found this coin in the database.
*/
bool found_in_db;
/**
* true if we #deposit_paid_check() matched this coin in the database.
*/
bool matched_in_db;
};
/**
* Information kept during a pay request for each exchange.
*/
struct ExchangeGroup
{
/**
* Payment context this group is part of.
*/
struct PayContext *pc;
/**
* Handle to the batch deposit operation we are performing for this
* exchange, NULL after the operation is done.
*/
struct TALER_EXCHANGE_BatchDepositHandle *bdh;
/**
* Handle for operation to lookup /keys (and auditors) from
* the exchange used for this transaction; NULL if no operation is
* pending.
*/
struct TMH_EXCHANGES_Find2Operation *fo;
/**
* Handle for operation to lookup /wire from
* the exchange used for this transaction; NULL if no operation is
* pending.
*/
struct TMH_EXCHANGES_WireOperation *gwo;
/**
* URL of the exchange that issued this coin. Aliases
* the exchange URL of one of the coins, do not free!
*/
const char *exchange_url;
/**
* true if we already tried a forced /keys download.
*/
bool tried_force_keys;
};
/**
* 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 num_exchange exchanges we are depositing
* coins into.
*/
struct ExchangeGroup **egs;
/**
* 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;
/**
* Our contract (or NULL if not available).
*/
json_t *contract_terms;
/**
* 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_PrivateContractHashP h_contract_terms;
/**
* "h_wire" from @e contract_terms. Used to identify
* the instance's wire transfer method.
*/
struct TALER_MerchantWireHashP 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_Timestamp wire_transfer_deadline;
/**
* Timestamp from @e contract_terms.
*/
struct GNUNET_TIME_Timestamp timestamp;
/**
* Refund deadline from @e contract_terms.
*/
struct GNUNET_TIME_Timestamp refund_deadline;
/**
* Deadline for the customer to pay for this proposal.
*/
struct GNUNET_TIME_Timestamp pay_deadline;
/**
* Set to the POS key, if applicable for this order.
*/
char *pos_key;
/**
* Algorithm chosen for generating the confirmation code.
*/
enum TALER_MerchantConfirmationAlgorithm pos_alg;
/**
* 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;
/**
* Minimum age required for this purchase.
*/
unsigned int minimum_age;
/**
* Number of coins this payment is made of. Length
* of the @e dc array.
*/
unsigned int coins_cnt;
/**
* Number of exchanges involved in the payment. Length
* of the @e eg array.
*/
unsigned int num_exchanges;
/**
* How often have we retried the 'main' transaction?
*/
unsigned int retry_counter;
/**
* Number of batch transactions pending.
*/
unsigned int pending_at_eg;
/**
* Number of coin deposits pending.
*/
unsigned int pending;
/**
* 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;
};
/**
* Active KYC operation with an exchange.
*/
struct KycContext
{
/**
* Kept in a DLL.
*/
struct KycContext *next;
/**
* Kept in a DLL.
*/
struct KycContext *prev;
/**
* Looking for the exchange.
*/
struct TMH_EXCHANGES_Find2Operation *fo;
/**
* Exchange this is about.
*/
char *exchange_url;
/**
* Merchant instance this is for.
*/
struct TMH_MerchantInstance *mi;
/**
* Wire method we are checking the status of.
*/
struct TMH_WireMethod *wm;
/**
* Handle for the GET /deposits operation.
*/
struct TALER_EXCHANGE_DepositGetHandle *dg;
/**
* Contract we are looking up.
*/
struct TALER_PrivateContractHashP h_contract_terms;
/**
* Coin we are looking up.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
/**
* Initial DB timestamp.
*/
struct GNUNET_TIME_Timestamp kyc_timestamp;
/**
* Initial KYC status.
*/
bool kyc_ok;
};
/**
* Head of active pay context DLL.
*/
static struct PayContext *pc_head;
/**
* Tail of active pay context DLL.
*/
static struct PayContext *pc_tail;
/**
* Head of active KYC context DLL.
*/
static struct KycContext *kc_head;
/**
* Tail of active KYC context DLL.
*/
static struct KycContext *kc_tail;
/**
* Free resources used by @a kc.
*
* @param[in] kc object to free
*/
static void
destroy_kc (struct KycContext *kc)
{
if (NULL != kc->fo)
{
TMH_EXCHANGES_keys4exchange_cancel (kc->fo);
kc->fo = NULL;
}
if (NULL != kc->dg)
{
TALER_EXCHANGE_deposits_get_cancel (kc->dg);
kc->dg = NULL;
}
TMH_instance_decref (kc->mi);
kc->mi = NULL;
GNUNET_free (kc->exchange_url);
GNUNET_CONTAINER_DLL_remove (kc_head,
kc_tail,
kc);
GNUNET_free (kc);
}
/**
* 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;
}
void
TMH_force_pc_resume ()
{
struct KycContext *kc;
while (NULL != (kc = kc_head))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Aborting KYC check at %s\n",
kc->exchange_url);
destroy_kc (kc);
}
for (struct PayContext *pc = pc_head;
NULL != pc;
pc = pc->next)
{
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)
{
pc->response_code = response_code;
pc->response = response;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Resuming /pay handling. HTTP status for our reply is %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;
}
if (NULL != pc->contract_terms)
{
json_decref (pc->contract_terms);
pc->contract_terms = NULL;
}
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
TALER_denom_sig_free (&dc->cdd.denom_sig);
GNUNET_free (dc->exchange_url);
}
GNUNET_free (pc->dc);
for (unsigned int i = 0; inum_exchanges; i++)
{
struct ExchangeGroup *eg = pc->egs[i];
if (NULL != eg->fo)
TMH_EXCHANGES_keys4exchange_cancel (eg->fo);
if (NULL != eg->gwo)
TMH_EXCHANGES_wire4exchange_cancel (eg->gwo);
GNUNET_free (eg);
}
GNUNET_free (pc->egs);
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->pos_key);
GNUNET_free (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);
/**
* Function called with detailed wire transfer data.
*
* @param cls a `struct KycContext *`
* @param dr HTTP response data
*/
static void
deposit_get_callback (
void *cls,
const struct TALER_EXCHANGE_GetDepositResponse *dr)
{
struct KycContext *kc = cls;
enum GNUNET_DB_QueryStatus qs;
struct GNUNET_TIME_Timestamp now;
kc->dg = NULL;
now = GNUNET_TIME_timestamp_get ();
switch (dr->hr.http_status)
{
case MHD_HTTP_OK:
qs = TMH_db->account_kyc_set_status (
TMH_db->cls,
kc->mi->settings.id,
&kc->wm->h_wire,
kc->exchange_url,
0LL,
NULL, /* no signature */
NULL, /* no signature */
now,
true,
TALER_AML_NORMAL);
GNUNET_break (qs > 0);
break;
case MHD_HTTP_ACCEPTED:
qs = TMH_db->account_kyc_set_status (
TMH_db->cls,
kc->mi->settings.id,
&kc->wm->h_wire,
kc->exchange_url,
dr->details.accepted.requirement_row,
NULL, /* no signature */
NULL, /* no signature */
now,
dr->details.accepted.kyc_ok,
dr->details.accepted.aml_decision);
GNUNET_break (qs > 0);
break;
default:
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"KYC check failed at %s with unexpected status %u\n",
kc->exchange_url,
dr->hr.http_status);
}
destroy_kc (kc);
}
/**
* Function called with the result of our exchange lookup.
*
* @param cls the `struct KycContext`
* @param keys NULL if exchange was not found to be acceptable
*/
static void
process_kyc_with_exchange (
void *cls,
struct TALER_EXCHANGE_Keys *keys)
{
struct KycContext *kc = cls;
kc->fo = NULL;
if (NULL == keys)
{
destroy_kc (kc);
return;
}
kc->dg = TALER_EXCHANGE_deposits_get (
merchant_curl_ctx,
kc->exchange_url,
keys,
&kc->mi->merchant_priv,
&kc->wm->h_wire,
&kc->h_contract_terms,
&kc->coin_pub,
GNUNET_TIME_UNIT_ZERO,
&deposit_get_callback,
kc);
if (NULL == kc->dg)
{
GNUNET_break (0);
destroy_kc (kc);
}
}
/**
* Function called from ``account_kyc_get_status``
* with KYC status information for this merchant.
*
* @param cls a `struct KycContext *`
* @param h_wire hash of the wire account
* @param exchange_kyc_serial serial number for the KYC process at the exchange, 0 if unknown
* @param payto_uri payto:// URI of the merchant's bank account
* @param exchange_url base URL of the exchange for which this is a status
* @param last_check when did we last get an update on our KYC status from the exchange
* @param kyc_ok true if we satisfied the KYC requirements
* @param aml_decision latest AML decision by the exchange
*/
static void
kyc_cb (
void *cls,
const struct TALER_MerchantWireHashP *h_wire,
uint64_t exchange_kyc_serial,
const char *payto_uri,
const char *exchange_url,
struct GNUNET_TIME_Timestamp last_check,
bool kyc_ok,
enum TALER_AmlDecisionState aml_decision)
{
struct KycContext *kc = cls;
(void) h_wire;
(void) exchange_kyc_serial;
(void) payto_uri;
(void) exchange_url;
kc->kyc_timestamp = last_check;
kc->kyc_ok = kyc_ok;
/* FIXME: act on aml_decision? */
}
/**
* Check for our KYC status at @a exchange_url for the
* payment of @a pc. First checks if we already have a
* positive result from the exchange, and if not checks
* with the exchange.
*
* @param pc payment context to use as starting point
* @param eg exchange group of the exchange we are triggering on
*/
static void
check_kyc (struct PayContext *pc,
const struct ExchangeGroup *eg)
{
enum GNUNET_DB_QueryStatus qs;
struct KycContext *kc;
kc = GNUNET_new (struct KycContext);
qs = TMH_db->account_kyc_get_status (TMH_db->cls,
pc->hc->instance->settings.id,
&pc->wm->h_wire,
eg->exchange_url,
&kyc_cb,
kc);
if (qs < 0)
{
GNUNET_break (0);
GNUNET_free (kc);
return;
}
if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs)
{
if (kc->kyc_ok)
{
GNUNET_free (kc);
return; /* we are done */
}
if (GNUNET_TIME_relative_cmp (
GNUNET_TIME_absolute_get_duration (
kc->kyc_timestamp.abs_time),
<,
KYC_RETRY_FREQUENCY))
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Not re-checking KYC status at `%s', as we already recently asked\n",
eg->exchange_url);
GNUNET_free (kc);
return;
}
}
kc->mi = pc->hc->instance;
kc->mi->rc++;
kc->wm = pc->wm;
kc->exchange_url = GNUNET_strdup (eg->exchange_url);
kc->h_contract_terms = pc->h_contract_terms;
/* find one of the coins of the batch */
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (0 != strcmp (eg->exchange_url,
pc->dc[i].exchange_url))
continue;
kc->coin_pub = dc->cdd.coin_pub;
break;
}
GNUNET_CONTAINER_DLL_insert (kc_head,
kc_tail,
kc);
kc->fo = TMH_EXCHANGES_keys4exchange (kc->exchange_url,
&process_kyc_with_exchange,
kc);
if (NULL == kc->fo)
{
GNUNET_break (0);
destroy_kc (kc);
}
}
/**
* Handle case where the batch deposit completed
* with a status of #MHD_HTTP_OK.
*
* @param eg group that completed
* @param dr response from the server
*/
static void
handle_batch_deposit_ok (struct ExchangeGroup *eg,
const struct TALER_EXCHANGE_BatchDepositResult *dr)
{
struct PayContext *pc = eg->pc;
enum GNUNET_DB_QueryStatus qs
= GNUNET_DB_STATUS_SUCCESS_NO_RESULTS;
/* 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);
for (unsigned int r = 0; rpreflight (TMH_db->cls);
if (GNUNET_OK !=
TMH_db->start (TMH_db->cls,
"batch-deposit-insert-confirmation"))
{
resume_pay_with_response (
pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_GENERIC_DB_START_FAILED),
TMH_pack_exchange_reply (&dr->hr)));
return;
}
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (0 != strcmp (eg->exchange_url,
pc->dc[i].exchange_url))
continue;
if (dc->found_in_db)
continue;
/* NOTE: We might want to check if the order was fully paid concurrently
by some other wallet here, and if so, issue an auto-refund. Right now,
it is possible to over-pay if two wallets literally make a concurrent
payment, as the earlier check for 'paid' is not in the same transaction
scope as this 'insert' operation. */
GNUNET_assert (j < dr->details.ok.num_signatures);
qs = TMH_db->insert_deposit (
TMH_db->cls,
pc->hc->instance->settings.id,
dr->details.ok.deposit_timestamp,
&pc->h_contract_terms,
&dc->cdd.coin_pub,
dc->exchange_url,
&dc->cdd.amount,
&dc->deposit_fee,
&dc->refund_fee,
&dc->wire_fee,
&pc->wm->h_wire,
&dr->details.ok.exchange_sigs[j++],
dr->details.ok.exchange_pub);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
TMH_db->rollback (TMH_db->cls);
break;
}
if (0 > qs)
{
/* 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,
"insert_deposit");
return;
}
}
qs = TMH_db->commit (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
TMH_db->rollback (TMH_db->cls);
continue;
}
if (GNUNET_DB_STATUS_HARD_ERROR == qs)
{
GNUNET_break (0);
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_COMMIT_FAILED,
"insert_deposit");
}
break; /* DB transaction succeeded */
} /* FOR DB retries */
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
resume_pay_with_error (pc,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_SOFT_FAILURE,
"insert_deposit");
return;
}
/* Transaction is done, mark affected coins as complete as well. */
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (0 != strcmp (eg->exchange_url,
pc->dc[i].exchange_url))
continue;
if (dc->found_in_db)
continue;
dc->found_in_db = true; /* well, at least NOW it'd be true ;-) */
pc->pending--;
}
check_kyc (pc,
eg);
}
/**
* Callback to handle a batch deposit permission's response.
*
* @param cls a `struct ExchangeGroup`
* @param dr HTTP response code details
*/
static void
batch_deposit_cb (
void *cls,
const struct TALER_EXCHANGE_BatchDepositResult *dr)
{
struct ExchangeGroup *eg = cls;
struct PayContext *pc = eg->pc;
eg->bdh = NULL;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Batch deposit completed with status %u\n",
dr->hr.http_status);
GNUNET_assert (GNUNET_YES == pc->suspended);
pc->pending_at_eg--;
switch (dr->hr.http_status)
{
case MHD_HTTP_OK:
handle_batch_deposit_ok (eg,
dr);
if (0 == pc->pending_at_eg)
execute_pay_transaction (eg->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 "exchange_url" for which the
error was being generated */
if (TALER_EC_EXCHANGE_GENERIC_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 ("exchange_url",
&eg->exchange_url)));
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 ("exchange_url",
&eg->exchange_url)));
return;
} /* end switch */
}
/**
* Function called with the result of our exchange keys lookup.
*
* @param cls the `struct ExchangeGroup`
* @param keys the keys of the exchange
*/
static void
process_pay_with_keys (
void *cls,
struct TALER_EXCHANGE_Keys *keys)
{
struct ExchangeGroup *eg = cls;
struct PayContext *pc = eg->pc;
struct TMH_HandlerContext *hc = pc->hc;
unsigned int group_size;
eg->fo = NULL;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Processing payment with exchange %s\n",
eg->exchange_url);
GNUNET_assert (GNUNET_YES == pc->suspended);
if (NULL == keys)
{
GNUNET_break_op (0);
pc->pending_at_eg--;
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;
}
/* Initiate /batch-deposit operation for all coins of
the current exchange (!) */
group_size = 0;
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
const struct TALER_EXCHANGE_DenomPublicKey *denom_details;
bool is_age_restricted_denom = false;
if (0 != strcmp (eg->exchange_url,
pc->dc[i].exchange_url))
continue;
if (dc->found_in_db)
continue;
denom_details
= TALER_EXCHANGE_get_denomination_key_by_hash (keys,
&dc->cdd.h_denom_pub);
if (NULL == denom_details)
{
pc->pending_at_eg--;
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->cdd.h_denom_pub),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_object_steal (
"exchange_keys",
TALER_EXCHANGE_keys_to_json (keys)))));
return;
}
dc->deposit_fee = denom_details->fees.deposit;
dc->refund_fee = denom_details->fees.refund;
if (GNUNET_TIME_absolute_is_past (
denom_details->expire_deposit.abs_time))
{
GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
"Denomination key offered by client has expired for deposits\n");
pc->pending_at_eg--;
resume_pay_with_response (
pc,
MHD_HTTP_GONE,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_DENOMINATION_DEPOSIT_EXPIRED),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&denom_details->h_key)));
return;
}
/* Now that we have the details about the denomination, we can verify age
* restriction requirements, if applicable. Note that denominations with an
* age_mask equal to zero always pass the age verification. */
is_age_restricted_denom = (0 != denom_details->key.age_mask.bits);
if (is_age_restricted_denom &&
(0 < pc->minimum_age))
{
/* Minimum age given and restricted coin provided: We need to verify the
* minimum age */
unsigned int code = 0;
if (dc->no_age_commitment)
{
GNUNET_break_op (0);
code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_MISSING;
goto AGE_FAIL;
}
dc->age_commitment.mask = denom_details->key.age_mask;
if (((int) (dc->age_commitment.num + 1)) !=
__builtin_popcount (dc->age_commitment.mask.bits))
{
GNUNET_break_op (0);
code =
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_COMMITMENT_SIZE_MISMATCH;
goto AGE_FAIL;
}
if (GNUNET_OK !=
TALER_age_commitment_verify (
&dc->age_commitment,
pc->minimum_age,
&dc->minimum_age_sig))
code = TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AGE_VERIFICATION_FAILED;
AGE_FAIL:
if (0 < code)
{
pc->pending_at_eg--;
GNUNET_free (dc->age_commitment.keys);
resume_pay_with_response (
pc,
MHD_HTTP_BAD_REQUEST,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (code),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&denom_details->h_key)));
return;
}
/* Age restriction successfully verified!
* Calculate the hash of the age commitment. */
TALER_age_commitment_hash (&dc->age_commitment,
&dc->cdd.h_age_commitment);
GNUNET_free (dc->age_commitment.keys);
}
else if (is_age_restricted_denom && dc->no_h_age_commitment)
{
/* The contract did not ask for a minimum_age but the client paid
* with a coin that has age restriction enabled. We lack the hash
* of the age commitment in this case in order to verify the coin
* and to deposit it with the exchange. */
pc->pending_at_eg--;
GNUNET_break_op (0);
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_AGE_COMMITMENT_HASH_MISSING),
GNUNET_JSON_pack_data_auto ("h_denom_pub",
&denom_details->h_key)));
return;
}
group_size++;
}
if (0 == group_size)
{
GNUNET_break (0);
pc->pending_at_eg--;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Group size zero, %u batch transactions remain pending\n",
pc->pending_at_eg);
if (0 == pc->pending_at_eg)
execute_pay_transaction (pc);
return;
}
{
struct TALER_EXCHANGE_CoinDepositDetail cdds[group_size];
struct TALER_EXCHANGE_DepositContractDetail dcd = {
.wire_deadline = pc->wire_transfer_deadline,
.merchant_payto_uri = pc->wm->payto_uri,
.wire_salt = pc->wm->wire_salt,
.h_contract_terms = pc->h_contract_terms,
.policy_details = NULL, /* FIXME-oec #7270 */
.timestamp = pc->timestamp,
.merchant_pub = hc->instance->merchant_pub,
.refund_deadline = pc->refund_deadline
};
enum TALER_ErrorCode ec;
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
enum GNUNET_GenericReturnValue ret;
if (dc->found_in_db)
continue;
if (0 != strcmp (dc->exchange_url,
eg->exchange_url))
continue;
cdds[i] = dc->cdd;
ret = TMH_EXCHANGES_lookup_wire_fee (dc->exchange_url,
pc->wm->wire_method,
&dc->wire_fee);
if (GNUNET_OK != ret)
{
enum TALER_ErrorCode ec;
fprintf (stderr,
"%d\n",
ret);
ec = (GNUNET_NO == ret)
? TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED
: TALER_EC_MERCHANT_GENERIC_EXCHANGE_WIRE_REQUEST_FAILED;
pc->pending_at_eg--;
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_string ("wire_method",
pc->wm->wire_method)));
return;
}
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Initiating batch deposit with %u coins\n",
group_size);
eg->bdh = TALER_EXCHANGE_batch_deposit (
merchant_curl_ctx,
eg->exchange_url,
keys,
&dcd,
group_size,
cdds,
&batch_deposit_cb,
eg,
&ec);
if (NULL == eg->bdh)
{
/* Signature was invalid or some other constraint was not satisfied. If
the exchange was unavailable, we'd get that information in the
callback. */
pc->pending_at_eg--;
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_string ("exchange_url",
eg->exchange_url)));
return;
}
if (TMH_force_audit)
TALER_EXCHANGE_batch_deposit_force_dc (eg->bdh);
}
}
/**
* Function called with the result of our exchange lookup.
*
* @param cls the `struct ExchangeGroup`
* @param keys the keys of the exchange
*/
static void
process_pay_with_wire (
void *cls,
struct TMH_Exchange *wire)
{
struct ExchangeGroup *eg = cls;
struct PayContext *pc = eg->pc;
eg->gwo = NULL;
if (NULL == wire)
{
GNUNET_break_op (0);
pc->pending_at_eg--;
/* FIXME: define more specific error code... */
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)));
return;
}
if (GNUNET_OK !=
TMH_exchange_check_debit (wire,
pc->wm))
{
GNUNET_break_op (0);
pc->pending_at_eg--;
resume_pay_with_response (
pc,
MHD_HTTP_CONFLICT,
TALER_MHD_MAKE_JSON_PACK (
TALER_JSON_pack_ec (
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED)));
return;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Fetching /keys for %s\n",
eg->exchange_url);
eg->fo = TMH_EXCHANGES_keys4exchange (eg->exchange_url,
&process_pay_with_keys,
eg);
if (NULL == eg->fo)
{
GNUNET_break (0);
pc->pending_at_eg--;
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;
}
}
/**
* Start batch deposits for all exchanges involved
* in this payment.
*
* @param pc payment context we are processing
*/
static void
start_batch_deposits (struct PayContext *pc)
{
for (unsigned int i = 0; inum_exchanges; i++)
{
struct ExchangeGroup *eg = pc->egs[i];
bool have_coins = false;
for (unsigned int j = 0; jcoins_cnt; j++)
{
struct DepositConfirmation *dc = &pc->dc[j];
if (0 != strcmp (eg->exchange_url,
pc->dc[j].exchange_url))
continue;
if (dc->found_in_db)
continue;
have_coins = true;
break;
}
if (! have_coins)
continue; /* no coins left to deposit at this exchange */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Getting /wire details for %s\n",
eg->exchange_url);
eg->gwo = TMH_EXCHANGES_wire4exchange (eg->exchange_url,
&process_pay_with_wire,
eg);
if (NULL == eg->gwo)
{
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;
}
pc->pending_at_eg++;
}
if (0 == pc->pending_at_eg)
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->cdd.coin_pub)) ||
(0 !=
strcmp (exchange_url,
dc->exchange_url)) ||
(0 != TALER_amount_cmp (amount_with_fee,
&dc->cdd.amount)) )
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->cdd.amount = *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->cdd.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].cdd.amount;
/**
* 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->cdd.amount,
&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->cdd.amount))
{
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);
}
}
/**
* Generate response (payment successful)
*
* @param[in,out] pc payment context where the payment was successful
*/
static void
generate_success_response (struct PayContext *pc)
{
struct GNUNET_CRYPTO_EddsaSignature sig;
char *pos_confirmation;
/* Sign on our end (as the payment did go through, even if it may
have been refunded already) */
TALER_merchant_pay_sign (&pc->h_contract_terms,
&pc->hc->instance->merchant_priv,
&sig);
/* Build the response */
pos_confirmation = (NULL == pc->pos_key)
? NULL
: TALER_build_pos_confirmation (pc->pos_key,
pc->pos_alg,
&pc->amount,
pc->timestamp);
resume_pay_with_response (
pc,
MHD_HTTP_OK,
TALER_MHD_MAKE_JSON_PACK (
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("pos_confirmation",
pos_confirmation)),
GNUNET_JSON_pack_data_auto ("sig",
&sig)));
GNUNET_free (pos_confirmation);
}
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). */
start_batch_deposits (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;
}
}
TMH_notify_order_change (hc->instance,
TMH_OSF_CLAIMED | TMH_OSF_PAID,
pc->timestamp,
pc->order_serial);
{
enum GNUNET_DB_QueryStatus qs;
json_t *jhook;
jhook = GNUNET_JSON_PACK (
GNUNET_JSON_pack_object_incref ("contract_terms",
pc->contract_terms),
GNUNET_JSON_pack_string ("order_id",
pc->order_id)
);
GNUNET_assert (NULL != jhook);
qs = TMH_trigger_webhook (pc->hc->instance->settings.id,
"pay",
jhook);
json_decref (jhook);
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,
"failed to trigger webhooks");
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_success_response (pc);
}
/**
* Try to parse the pay request into the given pay context.
* Schedules an error response in the connection on failure.
*
* @param[in,out] 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 PayContext *pc)
{
const char *session_id = NULL;
const json_t *coins;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_array_const ("coins",
&coins),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_string ("session_id",
&session_id),
NULL),
GNUNET_JSON_spec_end ()
};
{
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (pc->connection,
pc->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 ("");
}
pc->coins_cnt = json_array_size (coins);
if (pc->coins_cnt > MAX_COIN_ALLOWED_COINS)
{
GNUNET_break_op (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (
pc->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->cdd.coin_sig),
GNUNET_JSON_spec_fixed_auto ("coin_pub",
&dc->cdd.coin_pub),
TALER_JSON_spec_denom_sig ("ub_sig",
&dc->cdd.denom_sig),
GNUNET_JSON_spec_fixed_auto ("h_denom",
&dc->cdd.h_denom_pub),
TALER_JSON_spec_amount ("contribution",
TMH_currency,
&dc->cdd.amount),
GNUNET_JSON_spec_string ("exchange_url",
&exchange_url),
/* if a minimum age was required, the minimum_age_sig and
* age_commitment must be provided */
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_fixed_auto ("minimum_age_sig",
&dc->minimum_age_sig),
&dc->no_minimum_age_sig),
GNUNET_JSON_spec_mark_optional (
TALER_JSON_spec_age_commitment ("age_commitment",
&dc->age_commitment),
&dc->no_age_commitment),
/* if minimum age was not required, but coin with age restriction set
* was used, h_age_commitment must be provided. */
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_fixed_auto ("h_age_commitment",
&dc->cdd.h_age_commitment),
&dc->no_h_age_commitment),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
bool have_eg = false;
res = TALER_MHD_parse_json_data (pc->connection,
coin,
ispec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
return res;
}
for (unsigned int j = 0; jcdd.coin_pub,
&pc->dc[j].cdd.coin_pub))
{
GNUNET_break_op (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->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->cdd.amount.currency,
TMH_currency))
{
GNUNET_break_op (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_CONFLICT,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
TMH_currency))
? GNUNET_NO
: GNUNET_SYSERR;
}
/* Check the consistency of the (potential) age restriction
* information. */
if (dc->no_age_commitment != dc->no_minimum_age_sig)
{
GNUNET_break_op (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"inconsistent: 'age_commitment' vs. 'minimum_age_sig'"
)
)
? GNUNET_NO
: GNUNET_SYSERR;
}
/* Setup exchange group */
for (unsigned int i = 0; inum_exchanges; i++)
{
if (0 ==
strcmp (pc->egs[i]->exchange_url,
exchange_url))
{
have_eg = true;
break;
}
}
if (! have_eg)
{
struct ExchangeGroup *eg;
eg = GNUNET_new (struct ExchangeGroup);
eg->pc = pc;
eg->exchange_url = dc->exchange_url;
GNUNET_array_append (pc->egs,
pc->num_exchanges,
eg);
}
}
}
return GNUNET_OK;
}
/**
* Function called with information about a coin that was deposited.
* Checks if this coin is in our list of deposits as well.
*
* @param cls closure with our `struct PayContext *`
* @param deposit_serial which deposit operation is this about
* @param exchange_url URL of the exchange that issued 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 h_wire hash of merchant's wire details
* @param coin_pub public key of the coin
*/
static void
deposit_paid_check (
void *cls,
uint64_t deposit_serial,
const char *exchange_url,
const struct TALER_MerchantWireHashP *h_wire,
const struct TALER_Amount *amount_with_fee,
const struct TALER_Amount *deposit_fee,
const struct TALER_CoinSpendPublicKeyP *coin_pub)
{
struct PayContext *pc = cls;
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dci = &pc->dc[i];
if ( (0 ==
GNUNET_memcmp (&dci->cdd.coin_pub,
coin_pub)) &&
(0 ==
strcmp (dci->exchange_url,
exchange_url)) &&
(0 ==
TALER_amount_cmp (&dci->cdd.amount,
amount_with_fee)) )
{
dci->matched_in_db = true;
break;
}
}
}
/**
* Handle case where contract was already paid. Either decides
* the payment is idempotent, or refunds the excess payment.
*
* @param[in,out] pc context we use to handle the payment
* @return #GNUNET_NO if response was queued with MHD
* #GNUNET_SYSERR on hard error (MHD connection must be dropped)
*/
static enum GNUNET_GenericReturnValue
handle_contract_paid (struct PayContext *pc)
{
enum GNUNET_DB_QueryStatus qs;
bool unmatched = false;
json_t *refunds;
qs = TMH_db->lookup_deposits_by_order (TMH_db->cls,
pc->order_serial,
&deposit_paid_check,
pc);
if (qs <= 0)
{
GNUNET_break (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_deposits_by_order"))
? GNUNET_NO
: GNUNET_SYSERR;
}
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dci = &pc->dc[i];
if (! dci->matched_in_db)
unmatched = true;
}
if (! unmatched)
{
/* Everything fine, idempotent request */
struct GNUNET_CRYPTO_EddsaSignature sig;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Idempotent pay request for order `%s', signing again\n",
pc->order_id);
TALER_merchant_pay_sign (&pc->h_contract_terms,
&pc->hc->instance->merchant_priv,
&sig);
return (MHD_YES ==
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_data_auto ("sig",
&sig)))
? GNUNET_NO
: GNUNET_SYSERR;
}
/* Conflict, double-payment detected! */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Client attempted to pay extra for already paid order `%s'\n",
pc->order_id);
refunds = json_array ();
GNUNET_assert (NULL != refunds);
for (unsigned int i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dci = &pc->dc[i];
struct TALER_MerchantSignatureP merchant_sig;
if (dci->matched_in_db)
continue;
TALER_merchant_refund_sign (&dci->cdd.coin_pub,
&pc->h_contract_terms,
0, /* rtransaction id */
&dci->cdd.amount,
&pc->hc->instance->merchant_priv,
&merchant_sig);
GNUNET_assert (
0 ==
json_array_append_new (
refunds,
GNUNET_JSON_PACK (
GNUNET_JSON_pack_data_auto (
"coin_pub",
&dci->cdd.coin_pub),
GNUNET_JSON_pack_data_auto (
"merchant_sig",
&merchant_sig),
TALER_JSON_pack_amount ("amount",
&dci->cdd.amount),
GNUNET_JSON_pack_uint64 ("rtransaction_id",
0))));
}
return (MHD_YES ==
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
MHD_HTTP_CONFLICT,
TALER_MHD_PACK_EC (
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_ALREADY_PAID),
GNUNET_JSON_pack_array_steal ("refunds",
refunds)))
? GNUNET_NO
: GNUNET_SYSERR;
}
/**
* Check the database state for the given order. * Schedules an error response in the connection on failure.
*
* @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
check_contract (struct PayContext *pc)
{
/* obtain contract terms */
enum GNUNET_DB_QueryStatus qs;
bool paid = false;
if (NULL != pc->contract_terms)
{
json_decref (pc->contract_terms);
pc->contract_terms = NULL;
}
qs = TMH_db->lookup_contract_terms2 (TMH_db->cls,
pc->hc->instance->settings.id,
pc->order_id,
&pc->contract_terms,
&pc->order_serial,
&paid,
NULL,
&pc->pos_key,
&pc->pos_alg);
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 (pc->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 (pc->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 (pc->contract_terms,
&pc->h_contract_terms))
{
GNUNET_break (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
NULL))
? GNUNET_NO
: GNUNET_SYSERR;
}
if (paid)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order `%s' paid, checking for double-payment\n",
pc->order_id);
return handle_contract_paid (pc);
}
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 (pc->contract_terms,
"merchant"))
{
/* invalid contract */
GNUNET_break (0);
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->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),
NULL),
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),
GNUNET_JSON_spec_timestamp ("timestamp",
&pc->timestamp),
GNUNET_JSON_spec_timestamp ("refund_deadline",
&pc->refund_deadline),
GNUNET_JSON_spec_timestamp ("pay_deadline",
&pc->pay_deadline),
GNUNET_JSON_spec_timestamp ("wire_transfer_deadline",
&pc->wire_transfer_deadline),
GNUNET_JSON_spec_fixed_auto ("h_wire",
&pc->h_wire),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_uint32 ("minimum_age",
&pc->minimum_age),
NULL),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
pc->minimum_age = 0;
res = TALER_MHD_parse_internal_json_data (pc->connection,
pc->contract_terms,
espec);
if (NULL != fulfillment_url)
pc->fulfillment_url = GNUNET_strdup (fulfillment_url);
if (GNUNET_YES != res)
{
GNUNET_break (0);
return res;
}
}
if (GNUNET_TIME_timestamp_cmp (pc->wire_transfer_deadline,
<,
pc->refund_deadline))
{
/* This should already have been checked when creating the order! */
GNUNET_break (0);
return TALER_MHD_reply_with_error (pc->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.abs_time))
{
/* too late */
return (MHD_YES ==
TALER_MHD_reply_with_error (pc->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 = pc->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 (pc->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_INFO,
"Resuming pay with error after timeout\n");
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;
enum GNUNET_GenericReturnValue ret;
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;
ret = parse_pay (pc);
if (GNUNET_OK != ret)
return (GNUNET_NO == ret)
? MHD_YES
: MHD_NO;
}
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);
}
ret = check_contract (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);
GNUNET_assert (NULL != pc->wm);
execute_pay_transaction (pc);
return MHD_YES;
}
/* end of taler-merchant-httpd_post-orders-ID-pay.c */