/*
This file is part of TALER
(C) 2014-2024 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
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include "taler-merchant-httpd.h"
#include "taler-merchant-httpd_exchanges.h"
#include "taler-merchant-httpd_contract.h"
#include "taler-merchant-httpd_helper.h"
#include "taler-merchant-httpd_post-orders-ID-pay.h"
#include "taler-merchant-httpd_private-get-orders.h"
#include "taler_merchantdb_plugin.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
/**
* Maximum number of tokens that we allow as inputs per transaction
*/
#define MAX_TOKEN_ALLOWED_INPUTs 128
/**
* Maximum number of tokens that we allow as outputs per transaction
*/
#define MAX_TOKEN_ALLOWED_OUTPUTs 128
/**
* 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;
/**
* Different phases of processing the /pay request.
*/
enum PayPhase
{
/**
* Initial phase where the request is parsed.
*/
PP_INIT = 0,
/**
* Parse wallet data object from the pay request.
*/
PP_PARSE_WALLET_DATA,
/**
* Check database state for the given order.
*/
PP_CHECK_CONTRACT,
/**
* Validate provided tokens and token evelopes.
*/
PP_VALIDATE_TOKENS,
/**
* Contract has been paid.
*/
PP_CONTRACT_PAID,
/**
* Execute payment transaction.
*/
PP_PAY_TRANSACTION,
/**
* Notify other processes about successful payment.
*/
PP_PAYMENT_NOTIFICATION,
/**
* Create final success response.
*/
PP_SUCCESS_RESPONSE,
/**
* Perform batch deposits with exchange(s).
*/
PP_BATCH_DEPOSITS,
/**
* Return response in payment context.
*/
PP_RETURN_RESPONSE,
/**
* An exchange denied a deposit, fail for
* legal reasons.
*/
PP_FAIL_LEGAL_REASONS,
/**
* Return #MHD_YES to end processing.
*/
PP_END_YES,
/**
* Return #MHD_NO to end processing.
*/
PP_END_NO
};
/**
* 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;
/**
* 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;
};
struct TokenUseConfirmation
{
/**
* Signature on the deposit request made using the token use private key.
*/
struct TALER_TokenUseSignatureP sig;
/**
* Token use public key. This key was blindly signed by the merchant during
* the token issuance process.
*/
struct TALER_TokenUsePublicKeyP pub;
/**
* Unblinded signature on the token use public key done by the merchant.
*/
struct TALER_TokenIssueSignatureP unblinded_sig;
/**
* Hash of the token issue public key associated with this token.
* Note this is set in the validate_tokens phase.
*/
struct TALER_TokenIssuePublicKeyHashP h_issue;
/**
* true if we found this token in the database.
*/
bool found_in_db;
};
/**
* Information about a token envelope.
*/
struct TokenEnvelope
{
/**
* Blinded token use public keys waiting to be signed.
*/
struct TALER_TokenEnvelope blinded_token;
};
/**
* (Blindly) signed token to be returned to the wallet.
*/
struct SignedOutputToken
{
/**
* Blinded token use public keys waiting to be signed.
*/
struct TALER_TokenIssueBlindSignatureP sig;
/**
* Hash of token issue public key.
*/
struct TALER_TokenIssuePublicKeyHashP h_issue;
};
/**
* 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_KeysOperation *fo;
/**
* URL of the exchange that issued this coin. Aliases
* the exchange URL of one of the coins, do not free!
*/
const char *exchange_url;
/**
* Total deposit amount in this exchange group.
*/
struct TALER_Amount total;
/**
* Wire fee that applies to this exchange for the
* given payment context's wire method.
*/
struct TALER_Amount wire_fee;
/**
* true if we already tried a forced /keys download.
*/
bool tried_force_keys;
/**
* Did this exchange deny the transaction for legal reasons?
*/
bool got_451;
};
/**
* 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;
/**
* Array with @e tokens_cnt input tokens passed to this request.
*/
struct TokenUseConfirmation *tokens;
/**
* Array with @e output_tokens_cnt signed tokens returned in
* the response to the wallet.
*/
struct SignedOutputToken *output_tokens;
/**
* Array with @e token_envelopes_cnt (blinded) token envelopes.
*/
struct TokenEnvelope *token_envelopes;
/**
* Array with @e choices_len choices from the contract terms.
*/
struct TALER_MerchantContractChoice *choices;
/**
* Array with @e token_families_len token families from the contract terms.
*/
struct TALER_MerchantContractTokenFamily *token_families;
/**
* 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 #phase_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;
/**
* Index of selected choice in the @e contract_terms choices array.
*/
int64_t choice_index;
/**
* Our contract (or NULL if not available).
*/
json_t *contract_terms;
/**
* Wallet data json object from the request. Containing additional
* wallet data such as the selected choice_index.
*/
const json_t *wallet_data;
/**
* Hash of the canonicalized wallet data json object.
*/
struct GNUNET_HashCode h_wallet_data;
/**
* Output commitment hash calculated from the 'tokens_evs' field of the request.
*/
struct GNUNET_HashCode h_outputs;
/**
* 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;
/**
* 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;
/**
* Minimum age required for this purchase.
*/
unsigned int minimum_age;
/**
* Number of coins this payment is made of. Length
* of the @e dc array.
*/
size_t coins_cnt;
/**
* Number of input tokens passed to this request. Length
* of the @e tokens array.
*/
size_t tokens_cnt;
/**
* Number of token envelopes passed to this request.
* Length of the @e token_envelopes array.
*/
size_t token_envelopes_cnt;
/**
* Number of output tokens to return in the response.
* Length of the @e output_tokens array.
*/
unsigned int output_tokens_len;
/**
* Length of the @e choices array.
*/
unsigned int choices_len;
/**
* Length of the @e token_families array.
*/
unsigned int token_families_len;
/**
* 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;
/**
* Payment processing phase we are in.
*/
enum PayPhase phase;
/**
* #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;
/**
* Set to true if the deposit currency of a coin
* does not match the contract currency.
*/
bool deposit_currency_mismatch;
/**
* Set to true if the database contains a (bogus)
* refund for a different currency.
*/
bool refund_currency_mismatch;
/**
* Did any exchange deny a deposit for legal reasons?
*/
bool got_451;
};
/**
* Head of active pay context DLL.
*/
static struct PayContext *pc_head;
/**
* Tail of active pay context DLL.
*/
static struct PayContext *pc_tail;
void
TMH_force_pc_resume ()
{
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 payment processing.
*
* @param[in,out] pc payment process to resume
*/
static void
pay_resume (struct PayContext *pc)
{
GNUNET_assert (GNUNET_YES == pc->suspended);
pc->suspended = GNUNET_NO;
MHD_resume_connection (pc->connection);
TALER_MHD_daemon_trigger (); /* we resumed, kick MHD */
}
/**
* 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);
for (unsigned int i = 0; inum_exchanges; i++)
{
struct ExchangeGroup *eg = pc->egs[i];
if (NULL != eg->fo)
{
TMH_EXCHANGES_keys4exchange_cancel (eg->fo);
eg->fo = NULL;
pc->pending_at_eg--;
}
if (NULL != eg->bdh)
{
TALER_EXCHANGE_batch_deposit_cancel (eg->bdh);
eg->bdh = NULL;
pc->pending_at_eg--;
}
}
GNUNET_assert (0 == pc->pending_at_eg);
if (NULL != pc->timeout_task)
{
GNUNET_SCHEDULER_cancel (pc->timeout_task);
pc->timeout_task = NULL;
}
pc->phase = PP_RETURN_RESPONSE;
pay_resume (pc);
}
/**
* Resume payment processing with an error.
*
* @param pc operation to resume
* @param ec taler error code to return
* @param msg human readable error message
*/
static void
resume_pay_with_error (struct PayContext *pc,
enum TALER_ErrorCode ec,
const char *msg)
{
resume_pay_with_response (
pc,
TALER_ErrorCode_get_http_status_safe (ec),
TALER_MHD_make_error (ec,
msg));
}
/**
* Conclude payment processing for @a pc with the
* given @a res MHD status code.
*
* @param[in,out] pc payment context for final state transition
* @param res MHD return code to end with
*/
static void
pay_end (struct PayContext *pc,
MHD_RESULT res)
{
pc->phase = (MHD_YES == res)
? PP_END_YES
: PP_END_NO;
}
/**
* Return response stored in @a pc.
*
* @param[in,out] pc payment context we are processing
*/
static void
phase_return_response (struct PayContext *pc)
{
GNUNET_assert (0 != pc->response_code);
/* We are *done* processing the request, just queue the response (!) */
if (UINT_MAX == pc->response_code)
{
GNUNET_break (0);
pay_end (pc,
MHD_NO); /* hard error */
return;
}
pay_end (pc,
MHD_queue_response (pc->connection,
pc->response_code,
pc->response));
}
/**
* Return a response indicating failure for legal reasons.
*
* @param[in,out] pc payment context we are processing
*/
static void
phase_fail_for_legal_reasons (struct PayContext *pc)
{
json_t *exchanges;
GNUNET_assert (0 == pc->pending);
GNUNET_assert (pc->got_451);
exchanges = json_array ();
GNUNET_assert (NULL != exchanges);
for (unsigned int i = 0; inum_exchanges; i++)
{
struct ExchangeGroup *eg = pc->egs[i];
GNUNET_assert (NULL == eg->fo);
GNUNET_assert (NULL == eg->bdh);
if (! eg->got_451)
continue;
GNUNET_assert (
0 ==
json_array_append_new (
exchanges,
json_string (eg->exchange_url)));
}
pay_end (pc,
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS,
GNUNET_JSON_pack_array_steal ("exchange_base_urls",
exchanges)));
}
/**
* Do database transaction for a completed batch deposit.
*
* @param eg group that completed
* @param dr response from the server
* @return transaction status
*/
static enum GNUNET_DB_QueryStatus
batch_deposit_transaction (const struct ExchangeGroup *eg,
const struct TALER_EXCHANGE_BatchDepositResult *dr)
{
const struct PayContext *pc = eg->pc;
enum GNUNET_DB_QueryStatus qs;
struct TALER_Amount total_without_fees;
uint64_t b_dep_serial;
uint32_t off = 0;
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&total_without_fees));
for (size_t i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
struct TALER_Amount amount_without_fees;
/* might want to group deposits by batch more explicitly ... */
if (0 != strcmp (eg->exchange_url,
dc->exchange_url))
continue;
if (dc->found_in_db)
continue;
GNUNET_assert (0 <=
TALER_amount_subtract (&amount_without_fees,
&dc->cdd.amount,
&dc->deposit_fee));
GNUNET_assert (0 <=
TALER_amount_add (&total_without_fees,
&total_without_fees,
&amount_without_fees));
}
qs = TMH_db->insert_deposit_confirmation (
TMH_db->cls,
pc->hc->instance->settings.id,
dr->details.ok.deposit_timestamp,
&pc->h_contract_terms,
eg->exchange_url,
pc->wire_transfer_deadline,
&total_without_fees,
&eg->wire_fee,
&pc->wm->h_wire,
dr->details.ok.exchange_sig,
dr->details.ok.exchange_pub,
&b_dep_serial);
if (qs <= 0)
return qs; /* Entire batch already known or failure, we're done */
for (size_t i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
/* might want to group deposits by batch more explicitly ... */
if (0 != strcmp (eg->exchange_url,
dc->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. */
qs = TMH_db->insert_deposit (
TMH_db->cls,
off++, /* might want to group deposits by batch more explicitly ... */
b_dep_serial,
&dc->cdd.coin_pub,
&dc->cdd.coin_sig,
&dc->cdd.amount,
&dc->deposit_fee,
&dc->refund_fee);
if (qs < 0)
return qs;
GNUNET_break (qs > 0);
}
return qs;
}
/**
* 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;
}
qs = batch_deposit_transaction (eg,
dr);
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,
TALER_EC_GENERIC_DB_COMMIT_FAILED,
"batch_deposit_transaction");
}
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,
TALER_EC_GENERIC_DB_COMMIT_FAILED,
"insert_deposit");
}
break; /* DB transaction succeeded */
}
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
resume_pay_with_error (pc,
TALER_EC_GENERIC_DB_SOFT_FAILURE,
"insert_deposit");
return;
}
/* Transaction is done, mark affected coins as complete as well. */
for (size_t 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--;
}
}
/**
* Notify taler-merchant-kyccheck that we got a KYC
* rule violation notification and should start to
* check our KYC status.
*
* @param eg exchange group we were notified for
*/
static void
notify_kyc_required (const struct ExchangeGroup *eg)
{
struct GNUNET_DB_EventHeaderP es = {
.size = htons (sizeof (es)),
.type = htons (TALER_DBEVENT_MERCHANT_EXCHANGE_KYC_RULE_TRIGGERED)
};
char *hws;
char *extra;
hws = GNUNET_STRINGS_data_to_string_alloc (
&eg->pc->h_wire,
sizeof (eg->pc->h_wire));
GNUNET_asprintf (&extra,
"%s %s",
hws,
eg->exchange_url);
GNUNET_free (hws);
TMH_db->event_notify (TMH_db->cls,
&es,
extra,
strlen (extra) + 1);
GNUNET_free (extra);
}
/**
* 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;
pc->pending_at_eg--;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Batch deposit completed with status %u\n",
dr->hr.http_status);
GNUNET_assert (GNUNET_YES == pc->suspended);
switch (dr->hr.http_status)
{
case MHD_HTTP_OK:
handle_batch_deposit_ok (eg,
dr);
if (0 == pc->pending_at_eg)
{
pc->phase = PP_PAY_TRANSACTION;
pay_resume (pc);
}
return;
case MHD_HTTP_UNAVAILABLE_FOR_LEGAL_REASONS:
notify_kyc_required (eg);
eg->got_451 = true;
pc->got_451 = true;
/* update pc->pending */
for (size_t 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;
pc->pending--;
}
if (0 == pc->pending_at_eg)
{
pc->phase = PP_PAY_TRANSACTION;
pay_resume (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_string ("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_string ("exchange_url",
eg->exchange_url)));
return;
} /* end switch */
}
/**
* Force re-downloading keys for @a eg.
*
* @param[in,out] eg group to re-download keys for
*/
static void
force_keys (struct ExchangeGroup *eg);
/**
* Function called with the result of our exchange keys lookup.
*
* @param cls the `struct ExchangeGroup`
* @param keys the keys of the exchange
* @param exchange representation of the exchange
*/
static void
process_pay_with_keys (
void *cls,
struct TALER_EXCHANGE_Keys *keys,
struct TMH_Exchange *exchange)
{
struct ExchangeGroup *eg = cls;
struct PayContext *pc = eg->pc;
struct TMH_HandlerContext *hc = pc->hc;
unsigned int group_size;
struct TALER_Amount max_amount;
eg->fo = NULL;
pc->pending_at_eg--;
GNUNET_SCHEDULER_begin_async_scope (&hc->async_scope_id);
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);
resume_pay_with_error (
pc,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
NULL);
return;
}
if (! TMH_EXCHANGES_is_below_limit (keys,
TALER_KYCLOGIC_KYC_TRIGGER_TRANSACTION,
&eg->total))
{
GNUNET_break_op (0);
resume_pay_with_error (
pc,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION,
eg->exchange_url);
return;
}
max_amount = eg->total;
if (GNUNET_OK !=
TMH_exchange_check_debit (
pc->hc->instance->settings.id,
exchange,
pc->wm,
&max_amount))
{
if (eg->tried_force_keys)
{
GNUNET_break_op (0);
resume_pay_with_error (
pc,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED,
NULL);
return;
}
force_keys (eg);
return;
}
if (-1 ==
TALER_amount_cmp (&max_amount,
&eg->total))
{
/* max_amount < eg->total */
GNUNET_break_op (0);
resume_pay_with_error (
pc,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_EXCHANGE_TRANSACTION_LIMIT_VIOLATION,
eg->exchange_url);
return;
}
if (GNUNET_OK !=
TMH_EXCHANGES_lookup_wire_fee (exchange,
pc->wm->wire_method,
&eg->wire_fee))
{
if (eg->tried_force_keys)
{
GNUNET_break_op (0);
resume_pay_with_error (
pc,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_METHOD_UNSUPPORTED,
pc->wm->wire_method);
return;
}
force_keys (eg);
return;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Got wire data for %s\n",
eg->exchange_url);
/* Initiate /batch-deposit operation for all coins of
the current exchange (!) */
group_size = 0;
for (size_t 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)
{
if (eg->tried_force_keys)
{
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_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;
}
force_keys (eg);
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");
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)
{
GNUNET_break_op (0);
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. */
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);
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)
{
pc->phase = PP_PAY_TRANSACTION;
pay_resume (pc);
return;
}
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,
.wallet_data_hash = pc->h_wallet_data,
.wallet_timestamp = pc->timestamp,
.merchant_pub = hc->instance->merchant_pub,
.refund_deadline = pc->refund_deadline
};
enum TALER_ErrorCode ec;
size_t off = 0;
TALER_merchant_contract_sign (&pc->h_contract_terms,
&pc->hc->instance->merchant_priv,
&dcd.merchant_sig);
for (size_t i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (dc->found_in_db)
continue;
if (0 != strcmp (dc->exchange_url,
eg->exchange_url))
continue;
GNUNET_assert (off < group_size);
cdds[off++] = dc->cdd;
}
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Initiating batch deposit with %u coins\n",
group_size);
eg->bdh = TALER_EXCHANGE_batch_deposit (
TMH_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. */
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;
}
pc->pending_at_eg++;
if (TMH_force_audit)
TALER_EXCHANGE_batch_deposit_force_dc (eg->bdh);
}
}
static void
force_keys (struct ExchangeGroup *eg)
{
struct PayContext *pc = eg->pc;
eg->tried_force_keys = true;
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Forcing /keys download (once) as wire fees are unknown\n");
eg->fo = TMH_EXCHANGES_keys4exchange (
eg->exchange_url,
true,
&process_pay_with_keys,
eg);
if (NULL == eg->fo)
{
GNUNET_break_op (0);
resume_pay_with_error (pc,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED,
eg->exchange_url);
return;
}
pc->pending_at_eg++;
}
/**
* 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,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_TIMEOUT,
NULL);
}
/**
* 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-Performance-Optimization: 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;
}
/**
* Start batch deposits for all exchanges involved
* in this payment.
*
* @param[in,out] pc payment context we are processing
*/
static void
phase_batch_deposits (struct PayContext *pc)
{
for (unsigned int i = 0; inum_exchanges; i++)
{
struct ExchangeGroup *eg = pc->egs[i];
bool have_coins = false;
for (size_t 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 /keys for %s\n",
eg->exchange_url);
eg->fo = TMH_EXCHANGES_keys4exchange (
eg->exchange_url,
false,
&process_pay_with_keys,
eg);
if (NULL == eg->fo)
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_GENERIC_EXCHANGE_UNTRUSTED,
eg->exchange_url));
return;
}
pc->pending_at_eg++;
}
if (0 == pc->pending_at_eg)
{
pc->phase = PP_PAY_TRANSACTION;
pay_resume (pc);
return;
}
/* Suspend while we interact with the exchange */
MHD_suspend_connection (pc->connection);
pc->suspended = GNUNET_YES;
GNUNET_assert (NULL == pc->timeout_task);
pc->timeout_task
= GNUNET_SCHEDULER_add_delayed (get_pay_timeout (pc->coins_cnt),
&handle_pay_timeout,
pc);
}
/**
* Build JSON array of blindly signed token envelopes,
* to be used in the response to the wallet.
*
* @param[in,out] pc payment context to use
*/
static json_t *
build_token_sigs (struct PayContext *pc)
{
json_t *token_sigs = json_array ();
GNUNET_assert (NULL != token_sigs);
for (unsigned int i = 0; i < pc->output_tokens_len; i++)
{
GNUNET_assert (0 ==
json_array_append_new (
token_sigs,
GNUNET_JSON_PACK (
GNUNET_JSON_pack_blinded_sig (
"blind_sig",
pc->output_tokens[i].sig.signature)
)));
}
return token_sigs;
}
/**
* Generate response (payment successful)
*
* @param[in,out] pc payment context where the payment was successful
*/
static void
phase_success_response (struct PayContext *pc)
{
struct TALER_MerchantSignatureP sig;
char *pos_confirmation;
json_t *token_sigs;
/* 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);
token_sigs = (0 >= pc->output_tokens_len)
? NULL
: build_token_sigs (pc);
pay_end (pc,
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_string ("pos_confirmation",
pos_confirmation)),
GNUNET_JSON_pack_allow_null (
GNUNET_JSON_pack_array_steal ("token_sigs",
token_sigs)),
GNUNET_JSON_pack_data_auto ("sig",
&sig)));
GNUNET_free (pos_confirmation);
}
/**
* Use database to notify other clients about the
* payment being completed.
*
* @param[in,out] pc context to trigger notification for
*/
static void
phase_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);
}
pc->phase = PP_SUCCESS_RESPONSE;
}
/**
* 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
*/
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)
{
struct PayContext *pc = cls;
for (size_t 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)) ||
(GNUNET_OK !=
TALER_amount_cmp_currency (amount_with_fee,
&dc->cdd.amount)) ||
(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));
if ( (GNUNET_OK !=
TALER_amount_cmp_currency (&pc->total_paid,
amount_with_fee)) ||
(GNUNET_OK !=
TALER_amount_cmp_currency (&pc->total_fees_paid,
deposit_fee)) )
{
GNUNET_break_op (0);
pc->deposit_currency_mismatch = true;
break;
}
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->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 (size_t 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;
if (GNUNET_OK !=
TALER_amount_cmp_currency (&pc->total_refunded,
refund_amount))
{
GNUNET_break (0);
pc->refund_currency_mismatch = true;
break;
}
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 total_wire_fee;
struct TALER_Amount total_needed;
if (0 == pc->coins_cnt)
return TALER_amount_is_zero (&pc->amount);
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&total_wire_fee));
for (unsigned int i = 0; i < pc->num_exchanges; i++)
{
if (GNUNET_OK !=
TALER_amount_cmp_currency (&total_wire_fee,
&pc->egs[i]->wire_fee))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
total_wire_fee.currency));
return false;
}
if (0 >
TALER_amount_add (&total_wire_fee,
&total_wire_fee,
&pc->egs[i]->wire_fee))
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
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;
}
}
/**
* This loops calculates what are the deposit fee / total
* amount with fee / and wire fee, for all the coins.
*/
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&acc_fee));
GNUNET_assert (GNUNET_OK ==
TALER_amount_set_zero (pc->amount.currency,
&acc_amount));
for (size_t i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
GNUNET_assert (dc->found_in_db);
if ( (GNUNET_OK !=
TALER_amount_cmp_currency (&acc_fee,
&dc->deposit_fee)) ||
(GNUNET_OK !=
TALER_amount_cmp_currency (&acc_amount,
&dc->cdd.amount)) )
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
dc->deposit_fee.currency));
return false;
}
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. */
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_FEES_EXCEED_PAYMENT,
"Deposit fees exceed coin's contribution"));
return false;
}
} /* end 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,
"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,
&acc_fee))
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_CURRENCY_MISMATCH,
total_wire_fee.currency));
return false;
}
/* add wire fee to the total fees */
if (0 >
TALER_amount_add (&acc_fee,
&acc_fee,
&total_wire_fee))
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_PAYMENT_REQUIRED,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_REFUNDED,
"contract not paid up due to refunds"));
return false;
}
if (-1 < TALER_amount_cmp (&acc_amount,
&pc->amount))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_INSUFFICIENT_DUE_TO_FEES,
"contract not paid up due to fees (client may have calculated them badly)"));
return false;
}
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_PAYMENT_INSUFFICIENT,
"payment insufficient"));
return false;
}
return true;
}
/**
* Execute the DB transaction. If required (from
* soft/serialization errors), the transaction can be
* restarted here.
*
* @param[in,out] pc payment context to transact
*/
static void
phase_execute_pay_transaction (struct PayContext *pc)
{
struct TMH_HandlerContext *hc = pc->hc;
const char *instance_id = hc->instance->settings.id;
if (pc->got_451)
{
pc->phase = PP_FAIL_LEGAL_REASONS;
return;
}
/* Avoid re-trying transactions on soft errors forever! */
if (pc->retry_counter++ > MAX_RETRIES)
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_SOFT_FAILURE,
NULL));
return;
}
/* 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 (size_t 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);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_START_FAILED,
NULL));
return;
}
for (size_t i = 0; itokens_cnt; i++)
{
struct TokenUseConfirmation *tuc = &pc->tokens[i];
enum GNUNET_DB_QueryStatus qs;
/* Insert used token into database, the unique constraint will
case an error if this token was used before. */
qs = TMH_db->insert_spent_token (TMH_db->cls,
&pc->h_contract_terms,
&tuc->h_issue,
&tuc->pub,
&tuc->sig,
&tuc->unblinded_sig);
if (0 > qs)
{
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return; /* do it again */
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert used token"));
return;
}
else if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
/* UNIQUE constraint violation, meaning this token was already used. */
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_CONFLICT,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID,
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)
return; /* do it again */
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup deposits"));
return;
}
if (pc->deposit_currency_mismatch)
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
pc->amount.currency));
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)
return; /* do it again */
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup refunds"));
return;
}
if (pc->refund_currency_mismatch)
{
TMH_db->rollback (TMH_db->cls);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"refund currency in database does not match order currency"));
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). */
pc->phase = PP_BATCH_DEPOSITS;
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)
return; /* do it again */
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"mark contract paid"));
return;
}
}
/* Store signed output tokens in database. */
for (size_t i = 0; ioutput_tokens_len; i++)
{
struct SignedOutputToken *output = &pc->output_tokens[i];
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->insert_issued_token (TMH_db->cls,
&pc->h_contract_terms,
&output->h_issue,
&output->sig);
if (0 >= qs)
{
TMH_db->rollback (TMH_db->cls);
if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
return; /* do it again */
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_STORE_FAILED,
"insert output token"));
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)
return; /* do it again */
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
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)
return; /* do it again */
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_COMMIT_FAILED,
NULL));
return;
}
}
pc->phase = PP_PAYMENT_NOTIFICATION;
}
/**
* Ensures that the expected number of tokens for a @e key
* are provided as inputs and have valid signatures.
*/
static enum GNUNET_GenericReturnValue
find_valid_input_tokens (struct PayContext *pc,
struct TALER_MerchantContractTokenFamilyKey *key,
unsigned int index,
unsigned int expected_num)
{
unsigned int num_validated = 0;
struct TokenUseConfirmation *tuc = NULL;
for (unsigned int j = 0; j < expected_num; j++)
{
tuc = &pc->tokens[index + j];
if (NULL == tuc)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Input token for public key with "
"valid_after `%s' not found\n",
GNUNET_TIME_timestamp2s (key->valid_after));
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'tokens' array is missing required input token"));
return GNUNET_NO;
}
tuc->h_issue.hash = key->pub.public_key->pub_key_hash;
if (GNUNET_OK !=
TALER_token_issue_verify (&tuc->pub,
&key->pub,
&tuc->unblinded_sig))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Input token for public key with valid_after "
"`%s' has invalid issue signature\n",
GNUNET_TIME_timestamp2s (key->valid_after));
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ISSUE_SIG_INVALID,
NULL));
return GNUNET_NO;
}
if (GNUNET_OK !=
TALER_wallet_token_use_verify (&pc->h_contract_terms,
&pc->h_wallet_data,
&tuc->pub,
&tuc->sig))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Input token for public key with valid_after "
"`%s' has invalid use signature\n",
GNUNET_TIME_timestamp2s (key->valid_after));
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_USE_SIG_INVALID,
NULL));
return GNUNET_NO;
}
num_validated++;
}
if (num_validated != expected_num)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Expected %d tokens for public key with valid_after "
"`%s', but found %d\n",
expected_num,
GNUNET_TIME_timestamp2s (key->valid_after),
num_validated);
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_COUNT_MISMATCH,
NULL));
return GNUNET_NO;
}
return GNUNET_YES;
}
static enum GNUNET_GenericReturnValue
sign_token_envelopes (struct PayContext *pc,
struct TALER_MerchantContractTokenFamilyKey *key,
struct TALER_TokenIssuePrivateKeyP *priv,
bool critical,
unsigned int index,
unsigned int expected_num)
{
unsigned int num_signed = 0;
for (unsigned int j = 0; jtoken_envelopes[pos];
struct SignedOutputToken *output = &pc->output_tokens[pos];
if (pos > pc->token_envelopes_cnt || pos > pc->output_tokens_len)
{
GNUNET_assert (0); /* this should not happen */
return GNUNET_NO;
}
if (NULL == env->blinded_token.blinded_pub)
{
if (! critical)
continue;
/* critical token families require a token envelope. */
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"Token envelope for critical token family missing"));
return GNUNET_NO;
}
TALER_token_issue_sign (priv,
&env->blinded_token,
&output->sig);
output->h_issue.hash = key->pub.public_key->pub_key_hash;
num_signed++;
}
if (num_signed != expected_num)
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Expected %d token envelopes for public key with valid_after "
"'%s', but found %d\n",
expected_num,
GNUNET_TIME_timestamp2s (key->valid_after),
num_signed);
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_ENVELOPE_COUNT_MISMATCH,
NULL));
return GNUNET_NO;
}
return GNUNET_OK;
}
/**
* Validate tokens and token envelopes. First, we check if all tokens listed
* in the 'inputs' array of the selected choice are present in the 'tokens'
* array of the request. Then, we validate the signatures of each provided
* token.
*
* @param[in,out] pc context we use to handle the payment
*/
static void
phase_validate_tokens (struct PayContext *pc)
{
if (NULL == pc->choices || 0 >= pc->choices_len)
{
/* No tokens to validate */
pc->phase = PP_PAY_TRANSACTION;
return;
}
if (pc->choice_index < 0)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order `%s' has non-empty choices array but"
"request is missing 'choice_index' field\n",
pc->order_id);
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_MISSING,
NULL));
return;
}
if (pc->choice_index >= pc->choices_len)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order `%s' has choices array with %u elements but "
"request has 'choice_index' field with value %ld\n",
pc->order_id,
pc->choices_len,
pc->choice_index);
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_CHOICE_INDEX_OUT_OF_BOUNDS,
NULL));
return;
}
{
struct TALER_MerchantContractChoice selected;
struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
selected = pc->choices[pc->choice_index];
for (unsigned int i = 0; itoken_families,
pc->token_families_len,
&family,
&key))
{
/* this should never happen, since the choices and
token families are validated on insert. */
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
NULL));
return;
}
/* Ensure tokens signed by this key are valid at the current time. */
if (GNUNET_TIME_timestamp_cmp (key.valid_after, >, now) ||
GNUNET_TIME_timestamp_cmp (key.valid_before, <=, now))
{
GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
"Token family key validity period from %s to %s "
"is not valid at the current time\n",
GNUNET_TIME_timestamp2s (key.valid_after),
GNUNET_TIME_timestamp2s (key.valid_before));
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_CONFLICT,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_TOKEN_INVALID,
NULL));
return;
}
if (GNUNET_NO == find_valid_input_tokens (pc,
&key,
i,
input.details.token.count))
{
/* Error is already scheduled from find_valid_input_token. */
return;
}
}
GNUNET_array_grow (pc->output_tokens,
pc->output_tokens_len,
selected.outputs_len);
for (unsigned int i = 0; itoken_families,
pc->token_families_len,
&family,
&key))
{
/* this should never happen, since the choices and
token families are validated on insert. */
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_INTERNAL_INVARIANT_FAILURE,
NULL));
return;
}
qs = TMH_db->lookup_token_family_key (TMH_db->cls,
pc->hc->instance->settings.id,
family.slug,
key.valid_after,
key.valid_after,
&details);
if (qs <= 0)
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
NULL));
return;
}
GNUNET_assert (NULL != details.priv.private_key);
if (GNUNET_OK !=
sign_token_envelopes (pc,
&key,
&details.priv,
/* TODO: Use critical field stored in database here instead. */
details.token_family.kind ==
TALER_MERCHANTDB_TFK_Subscription,
i,
output.details.token.count))
{
/* Error is already scheduled from sign_token_envelopes. */
return;
}
}
}
pc->phase = PP_PAY_TRANSACTION;
}
/**
* 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 h_wire hash of merchant's wire details
* @param deposit_timestamp when was the deposit made
* @param amount_with_fee amount the exchange will deposit for this coin
* @param deposit_fee fee the exchange will charge for this coin
* @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,
struct GNUNET_TIME_Timestamp deposit_timestamp,
const struct TALER_Amount *amount_with_fee,
const struct TALER_Amount *deposit_fee,
const struct TALER_CoinSpendPublicKeyP *coin_pub)
{
struct PayContext *pc = cls;
for (size_t 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)) &&
(GNUNET_YES ==
TALER_amount_cmp_currency (&dci->cdd.amount,
amount_with_fee)) &&
(0 ==
TALER_amount_cmp (&dci->cdd.amount,
amount_with_fee)) )
{
dci->matched_in_db = true;
break;
}
}
}
static void
input_tokens_paid_check (
void *cls,
uint64_t spent_token_serial,
const struct TALER_PrivateContractHashP *h_contract_terms,
const struct TALER_TokenIssuePublicKeyHashP *h_issue_pub,
const struct TALER_TokenUsePublicKeyP *use_pub,
const struct TALER_TokenUseSignatureP *use_sig,
const struct TALER_TokenIssueSignatureP *issue_sig)
{
struct PayContext *pc = cls;
for (size_t i = 0; itokens_cnt; i++)
{
struct TokenUseConfirmation *tuc = &pc->tokens[i];
if ( (0 ==
GNUNET_memcmp (&tuc->pub, use_pub)) &&
(0 ==
GNUNET_memcmp (&tuc->sig, use_sig)) &&
(0 ==
GNUNET_memcmp (&tuc->unblinded_sig, issue_sig)) )
{
tuc->found_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
*/
static void
phase_contract_paid (struct PayContext *pc)
{
json_t *refunds;
bool unmatched = false;
{
enum GNUNET_DB_QueryStatus qs;
qs = TMH_db->lookup_deposits_by_order (TMH_db->cls,
pc->order_serial,
&deposit_paid_check,
pc);
/* Since orders with choices can have a price of zero,
0 is also a valid query state */
if (qs < 0)
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_deposits_by_order"));
return;
}
}
for (size_t i = 0; icoins_cnt && ! unmatched; i++)
{
struct DepositConfirmation *dci = &pc->dc[i];
if (! dci->matched_in_db)
unmatched = true;
}
/* Check if provided input tokens match token in the database */
{
enum GNUNET_DB_QueryStatus qs;
/* TODO: Use h_contract instead of order_serial here? */
qs = TMH_db->lookup_spent_tokens_by_order (TMH_db->cls,
pc->order_serial,
&input_tokens_paid_check,
pc);
if (qs < 0)
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"lookup_spent_tokens_by_order"));
return;
}
}
for (size_t i = 0; itokens_cnt && ! unmatched; i++)
{
struct TokenUseConfirmation *tuc = &pc->tokens[i];
if (! tuc->found_in_db)
unmatched = true;
}
if (! unmatched)
{
/* Everything fine, idempotent request */
struct TALER_MerchantSignatureP 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);
/* TODO: Add token_sigs to response body. */
pay_end (pc,
TALER_MHD_REPLY_JSON_PACK (
pc->connection,
MHD_HTTP_OK,
GNUNET_JSON_pack_data_auto ("sig",
&sig)));
return;
}
/* Conflict, double-payment detected! */
/* TODO: What should we do with input tokens?
Currently there is no refund for tokens. */
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 (size_t 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))));
}
pay_end (pc,
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)));
}
/**
* Check the database state for the given order.
* Schedules an error response in the connection on failure.
*
* @param[in,out] pc context we use to handle the payment
*/
static void
phase_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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"contract terms"));
return;
}
if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
{
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_NOT_FOUND,
TALER_EC_MERCHANT_GENERIC_ORDER_UNKNOWN,
pc->order_id));
return;
}
/* hash contract (needed later) */
json_dumpf (pc->contract_terms,
stderr,
JSON_INDENT (2));
if (GNUNET_OK !=
TALER_JSON_contract_hash (pc->contract_terms,
&pc->h_contract_terms))
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_FAILED_COMPUTE_JSON_HASH,
NULL));
return;
}
if (paid)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Order `%s' paid, checking for double-payment\n",
pc->order_id);
pc->phase = PP_CONTRACT_PAID;
return;
}
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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_MERCHANT_FIELD_MISSING,
NULL));
return;
}
/* Get details from contract and check fundamentals */
{
const char *fulfillment_url = NULL;
struct GNUNET_JSON_Specification espec[] = {
TALER_JSON_spec_amount_any ("amount",
&pc->amount),
GNUNET_JSON_spec_mark_optional (
/* This one does not have to be a Web URL */
GNUNET_JSON_spec_string ("fulfillment_url",
&fulfillment_url),
NULL),
TALER_JSON_spec_amount_any ("max_fee",
&pc->max_fee),
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_mark_optional (
TALER_JSON_spec_choices ("choices",
&pc->choices,
&pc->choices_len),
NULL),
GNUNET_JSON_spec_mark_optional (
TALER_JSON_spec_token_families ("token_families",
&pc->token_families,
&pc->token_families_len),
NULL),
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);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
}
if (GNUNET_OK !=
TALER_amount_cmp_currency (&pc->max_fee,
&pc->amount))
{
GNUNET_break (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_GENERIC_DB_FETCH_FAILED,
"'max_fee' in database does not match currency of contract price"));
return;
}
for (size_t i = 0; icoins_cnt; i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
if (GNUNET_OK !=
TALER_amount_cmp_currency (&dc->cdd.amount,
&pc->amount))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_CONFLICT,
TALER_EC_MERCHANT_GENERIC_CURRENCY_MISMATCH,
pc->amount.currency));
return;
}
}
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);
pay_end (pc,
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));
return;
}
if (GNUNET_TIME_absolute_is_past (pc->pay_deadline.abs_time))
{
/* too late */
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_GONE,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_OFFER_EXPIRED,
NULL));
return;
}
/* 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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_WIRE_HASH_UNKNOWN,
NULL));
return;
}
pc->wm = wm;
}
pc->phase = PP_VALIDATE_TOKENS;
}
/**
* Try to parse the wallet_data object of the pay request into
* the given context. Schedules an error response in the connection
* on failure.
*
* @param[in,out] pc context we use to handle the payment
*/
static void
phase_parse_wallet_data (struct PayContext *pc)
{
const json_t *tokens_evs;
struct GNUNET_JSON_Specification spec[] = {
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_int64 ("choice_index",
&pc->choice_index),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("tokens_evs",
&tokens_evs),
NULL),
GNUNET_JSON_spec_end ()
};
pc->choice_index = -1;
if (NULL == pc->wallet_data)
{
pc->phase = PP_CHECK_CONTRACT;
return;
}
{
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (pc->connection,
pc->wallet_data,
spec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
}
pc->token_envelopes_cnt = json_array_size (tokens_evs);
if (pc->token_envelopes_cnt > MAX_TOKEN_ALLOWED_OUTPUTs)
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'tokens_evs' array too long"));
return;
}
if (0 < pc->token_envelopes_cnt)
{
/* Calculate output commitment to be verified later. */
TALER_json_hash (tokens_evs,
&pc->h_outputs);
}
pc->token_envelopes = GNUNET_new_array (pc->token_envelopes_cnt,
struct TokenEnvelope);
{
unsigned int tokens_ev_index;
json_t *token_ev;
json_array_foreach (tokens_evs,
tokens_ev_index,
token_ev)
{
struct TokenEnvelope *ev = &pc->token_envelopes[tokens_ev_index];
struct GNUNET_JSON_Specification ispec[] = {
TALER_JSON_spec_token_envelope ("token_ev",
&ev->blinded_token),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
if (json_is_null (token_ev))
continue;
res = TALER_MHD_parse_json_data (pc->connection,
token_ev,
ispec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
for (unsigned int j = 0; jblinded_token.blinded_pub,
pc->token_envelopes[j].blinded_token.blinded_pub))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"duplicate token envelope in list"));
return;
}
}
}
}
TALER_json_hash (pc->wallet_data,
&pc->h_wallet_data);
pc->phase = PP_CHECK_CONTRACT;
}
/**
* 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
*/
static void
phase_parse_pay (struct PayContext *pc)
{
const char *session_id = NULL;
const json_t *coins;
const json_t *tokens;
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_mark_optional (
GNUNET_JSON_spec_object_const ("wallet_data",
&pc->wallet_data),
NULL),
GNUNET_JSON_spec_mark_optional (
GNUNET_JSON_spec_array_const ("tokens",
&tokens),
NULL),
GNUNET_JSON_spec_end ()
};
GNUNET_assert (PP_INIT == pc->phase);
{
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);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
}
/* 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);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'coins' array too long"));
return;
}
/* 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_any ("contribution",
&dc->cdd.amount),
TALER_JSON_spec_web_url ("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;
struct ExchangeGroup *eg = NULL;
res = TALER_MHD_parse_json_data (pc->connection,
coin,
ispec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
for (unsigned int j = 0; jcdd.coin_pub,
&pc->dc[j].cdd.coin_pub))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"duplicate coin in list"));
return;
}
}
dc->exchange_url = GNUNET_strdup (exchange_url);
dc->index = coins_index;
dc->pc = pc;
/* Check the consistency of the (potential) age restriction
* information. */
if (dc->no_age_commitment != dc->no_minimum_age_sig)
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"inconsistent: 'age_commitment' vs. 'minimum_age_sig'"
));
return;
}
/* Setup exchange group */
for (unsigned int i = 0; inum_exchanges; i++)
{
if (0 ==
strcmp (pc->egs[i]->exchange_url,
exchange_url))
{
eg = pc->egs[i];
break;
}
}
if (NULL == eg)
{
eg = GNUNET_new (struct ExchangeGroup);
eg->pc = pc;
eg->exchange_url = dc->exchange_url;
eg->total = dc->cdd.amount;
GNUNET_array_append (pc->egs,
pc->num_exchanges,
eg);
}
else
{
if (0 >
TALER_amount_add (&eg->total,
&eg->total,
&dc->cdd.amount))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_INTERNAL_SERVER_ERROR,
TALER_EC_MERCHANT_POST_ORDERS_ID_PAY_AMOUNT_OVERFLOW,
"Overflow adding up amounts"));
return;
}
}
}
}
pc->tokens_cnt = json_array_size (tokens);
if (pc->tokens_cnt > MAX_TOKEN_ALLOWED_INPUTs)
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (
pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"'tokens' array too long"));
return;
}
pc->tokens = GNUNET_new_array (pc->tokens_cnt,
struct TokenUseConfirmation);
/* This look populates the array 'tokens' in 'pc' */
{
unsigned int tokens_index;
json_t *token;
json_array_foreach (tokens, tokens_index, token)
{
struct TokenUseConfirmation *tuc = &pc->tokens[tokens_index];
struct GNUNET_JSON_Specification ispec[] = {
GNUNET_JSON_spec_fixed_auto ("token_sig",
&tuc->sig),
GNUNET_JSON_spec_fixed_auto ("token_pub",
&tuc->pub),
TALER_JSON_spec_token_issue_sig ("ub_sig",
&tuc->unblinded_sig),
GNUNET_JSON_spec_end ()
};
enum GNUNET_GenericReturnValue res;
res = TALER_MHD_parse_json_data (pc->connection,
token,
ispec);
if (GNUNET_YES != res)
{
GNUNET_break_op (0);
pay_end (pc,
(GNUNET_NO == res)
? MHD_YES
: MHD_NO);
return;
}
for (unsigned int j = 0; jpub,
&pc->tokens[j].pub))
{
GNUNET_break_op (0);
pay_end (pc,
TALER_MHD_reply_with_error (pc->connection,
MHD_HTTP_BAD_REQUEST,
TALER_EC_GENERIC_PARAMETER_MALFORMED,
"duplicate token in list"));
return;
}
}
}
}
pc->phase = PP_PARSE_WALLET_DATA;
}
/**
* 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);
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);
}
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);
pc->connection = connection;
pc->hc = hc;
pc->order_id = hc->infix;
hc->ctx = pc;
hc->cc = &pay_context_cleanup;
GNUNET_CONTAINER_DLL_insert (pc_head,
pc_tail,
pc);
}
while (1)
{
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Processing /pay in phase %d\n",
(int) pc->phase);
switch (pc->phase)
{
case PP_INIT:
phase_parse_pay (pc);
break;
case PP_PARSE_WALLET_DATA:
phase_parse_wallet_data (pc);
break;
case PP_CHECK_CONTRACT:
phase_check_contract (pc);
break;
case PP_CONTRACT_PAID:
phase_contract_paid (pc);
break;
case PP_VALIDATE_TOKENS:
phase_validate_tokens (pc);
break;
case PP_PAY_TRANSACTION:
phase_execute_pay_transaction (pc);
break;
case PP_BATCH_DEPOSITS:
phase_batch_deposits (pc);
break;
case PP_PAYMENT_NOTIFICATION:
phase_payment_notification (pc);
break;
case PP_SUCCESS_RESPONSE:
phase_success_response (pc);
break;
case PP_RETURN_RESPONSE:
phase_return_response (pc);
break;
case PP_FAIL_LEGAL_REASONS:
phase_fail_for_legal_reasons (pc);
break;
case PP_END_YES:
return MHD_YES;
case PP_END_NO:
return MHD_NO;
}
switch (pc->suspended)
{
case GNUNET_SYSERR:
/* during shutdown, we don't generate any more replies */
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Processing /pay ends due to shutdown in phase %d\n",
(int) pc->phase);
return MHD_NO;
case GNUNET_NO:
/* continue to next phase */
break;
case GNUNET_YES:
GNUNET_log (GNUNET_ERROR_TYPE_INFO,
"Processing /pay suspended in phase %d\n",
(int) pc->phase);
return MHD_YES;
}
}
/* impossible to get here */
GNUNET_assert (0);
return MHD_YES;
}
/* end of taler-merchant-httpd_post-orders-ID-pay.c */