summaryrefslogtreecommitdiff
path: root/src/backend/taler-merchant-httpd_pay.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/backend/taler-merchant-httpd_pay.c')
-rw-r--r--src/backend/taler-merchant-httpd_pay.c1251
1 files changed, 781 insertions, 470 deletions
diff --git a/src/backend/taler-merchant-httpd_pay.c b/src/backend/taler-merchant-httpd_pay.c
index 1315b9c5..8ae6f7d5 100644
--- a/src/backend/taler-merchant-httpd_pay.c
+++ b/src/backend/taler-merchant-httpd_pay.c
@@ -40,9 +40,9 @@
#define PAY_TIMEOUT (GNUNET_TIME_relative_multiply (GNUNET_TIME_UNIT_SECONDS, 30))
/**
- * How often do we retry the simple INSERT database transaction?
+ * How often do we retry the (complex!) database transaction?
*/
-#define MAX_RETRIES 3
+#define MAX_RETRIES 5
/**
* Information we keep for an individual call to the /pay handler.
@@ -67,6 +67,11 @@ struct DepositConfirmation
struct TALER_EXCHANGE_DepositHandle *dh;
/**
+ * URL of the exchange that issued this coin.
+ */
+ char *exchange_url;
+
+ /**
* Denomination of this coin.
*/
struct TALER_DenominationPublicKey denom;
@@ -88,6 +93,11 @@ struct DepositConfirmation
struct TALER_Amount refund_fee;
/**
+ * Wire fee charged by the exchange of this coin.
+ */
+ struct TALER_Amount wire_fee;
+
+ /**
* Public key of the coin.
*/
struct TALER_CoinSpendPublicKeyP coin_pub;
@@ -185,14 +195,14 @@ struct PayContext
struct TMH_EXCHANGES_FindOperation *fo;
/**
- * Placeholder for #TMH_PARSE_post_json() to keep its internal state.
+ * URL of the exchange used for the last @e fo.
*/
- void *json_parse_context;
-
+ const char *current_exchange;
+
/**
- * Exchange URI given in @e root.
+ * Placeholder for #TMH_PARSE_post_json() to keep its internal state.
*/
- char *chosen_exchange;
+ void *json_parse_context;
/**
* Transaction ID given in @e root.
@@ -211,6 +221,16 @@ struct PayContext
struct GNUNET_HashCode h_wire;
/**
+ * Total wire fees charged by all exchanges involved. Note: there
+ * is a sublte issue with this value not being correctly calculated
+ * if /pay is called a second time, as then some deposits that are
+ * already in the DB are no longer mapped to an exchange (and thus
+ * no fee is looked up). Fixing this would be rather complicated,
+ * and is likely simply no worth it.
+ */
+ struct TALER_Amount total_wire_fee;
+
+ /**
* 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
@@ -244,6 +264,26 @@ struct PayContext
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? (Can be given by the frontend
* or be determined by our configuration via #wire_transfer_delay.)
@@ -281,6 +321,11 @@ struct PayContext
unsigned int coins_cnt;
/**
+ * How often have we retried the 'main' transaction?
+ */
+ unsigned int retry_counter;
+
+ /**
* Number of transactions still pending. Initially set to
* @e coins_cnt, decremented on each transaction that
* successfully finished.
@@ -288,6 +333,14 @@ struct PayContext
unsigned int pending;
/**
+ * Number of transactions still pending for the currently selected
+ * exchange. Initially set to the number of coins started at the
+ * exchange, decremented on each transaction that successfully
+ * finished. Once it hits zero, we pick the next exchange.
+ */
+ unsigned int pending_at_ce;
+
+ /**
* HTTP status code to use for the reply, i.e 200 for "OK".
* Special value UINT_MAX is used to indicate hard errors
* (no reply, return #MHD_NO).
@@ -295,14 +348,6 @@ struct PayContext
unsigned int response_code;
/**
- * #GNUNET_NO if the transaction is not in our database,
- * #GNUNET_YES if the transaction is known to our database,
- * #GNUNET_SYSERR if the transaction ID is used for a different
- * transaction in our database.
- */
- int transaction_exists;
-
- /**
* #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
@@ -409,16 +454,18 @@ sign_success_response (struct PayContext *pc)
json_t *refunds;
enum TALER_ErrorCode ec;
const char *errmsg;
-
- refunds = TM_get_refund_json (pc->mi, &pc->h_contract_terms, &ec, &errmsg);
-
- if (NULL == refunds) {
- return TMH_RESPONSE_make_internal_error (ec, errmsg);
- }
-
struct GNUNET_CRYPTO_EddsaSignature sig;
struct PaymentResponsePS mr;
+ refunds = TM_get_refund_json (pc->mi,
+ &pc->h_contract_terms,
+ &ec,
+ &errmsg);
+
+ if (NULL == refunds)
+ return TMH_RESPONSE_make_error (ec,
+ errmsg);
+
mr.purpose.purpose = htonl (TALER_SIGNATURE_MERCHANT_PAYMENT_OK);
mr.purpose.size = htonl (sizeof (mr));
mr.h_contract_terms = pc->h_contract_terms;
@@ -441,140 +488,23 @@ sign_success_response (struct PayContext *pc)
/**
- * Callback to handle a deposit permission's response.
+ * Resume payment processing with an error.
*
- * @param cls a `struct DepositConfirmation` (i.e. a pointer
- * into the global array of confirmations and an index for this call
- * in that array). That way, the last executed callback can detect
- * that no other confirmations are on the way, and can pack a response
- * for the wallet
- * @param http_status HTTP response code, #MHD_HTTP_OK
- * (200) for successful deposit; 0 if the exchange's reply is bogus (fails
- * to follow the protocol)
- * @param ec taler-specific error code, #TALER_EC_NONE on success
- * @param sign_key which key did the exchange use to sign the @a proof
- * @param proof the received JSON reply,
- * should be kept as proof (and, in case of errors, be forwarded to
- * the customer)
- */
+ * @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
-deposit_cb (void *cls,
- unsigned int http_status,
- enum TALER_ErrorCode ec,
- const struct TALER_ExchangePublicKeyP *sign_key,
- const json_t *proof)
+resume_pay_with_error (struct PayContext *pc,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const char *msg)
{
- struct DepositConfirmation *dc = cls;
- struct PayContext *pc = dc->pc;
- enum GNUNET_DB_QueryStatus qs;
-
- dc->dh = NULL;
- GNUNET_assert (GNUNET_YES == pc->suspended);
- pc->pending--;
- if (MHD_HTTP_OK != http_status)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Deposit operation failed with HTTP code %u\n",
- http_status);
- /* Transaction failed; stop all other ongoing deposits */
- abort_deposit (pc);
- db->rollback (db->cls);
-
- if (NULL == proof)
- {
- /* We can't do anything meaningful here, the exchange did something wrong */
- resume_pay_with_response (pc,
- MHD_HTTP_SERVICE_UNAVAILABLE,
- TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:I, s:s}",
- "error", "exchange failed",
- "code", (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED,
- "exchange-code", (json_int_t) ec,
- "exchange-http-status", (json_int_t) http_status,
- "hint", "The exchange provided an unexpected response"));
- }
- else
- {
- /* Forward error, adding the "coin_pub" for which the
- error was being generated */
- json_t *eproof;
-
- eproof = json_copy ((json_t *) proof);
- json_object_set_new (eproof,
- "coin_pub",
- GNUNET_JSON_from_data_auto (&dc->coin_pub));
- resume_pay_with_response (pc,
- http_status,
- TMH_RESPONSE_make_json (eproof));
- json_decref (eproof);
- }
- return;
- }
- /* store result to DB */
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Storing successful payment for h_contract_terms `%s' and merchant `%s'\n",
- GNUNET_h2s (&pc->h_contract_terms),
- TALER_B2S (&pc->mi->pubkey));
- for (unsigned int i=0;i<MAX_RETRIES;i++)
- {
- qs = db->store_deposit (db->cls,
- &pc->h_contract_terms,
- &pc->mi->pubkey,
- &dc->coin_pub,
- pc->chosen_exchange,
- &dc->amount_with_fee,
- &dc->deposit_fee,
- &dc->refund_fee,
- sign_key,
- proof);
- if (GNUNET_DB_STATUS_SOFT_ERROR != qs)
- break;
- }
- if (0 > qs)
- {
- /* Special report if retries insufficient */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
- /* internal error */
- abort_deposit (pc);
- db->rollback (db->cls);
- /* Forward error including 'proof' for the body */
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_DB_STORE_PAY_ERROR,
- "Merchant database error"));
- return;
- }
-
- if (0 != pc->pending)
- return; /* still more to do */
-
- qs = db->mark_proposal_paid (db->cls,
- &pc->h_contract_terms,
- &pc->mi->pubkey);
- if (0 > qs)
- {
- abort_deposit (pc);
- db->rollback (db->cls);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR,
- "Merchant database error: could not mark proposal as 'paid'"));
- return;
- }
- qs = db->commit (db->cls);
- if (0 > qs)
- {
- abort_deposit (pc);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR,
- "Merchant database error: could not commit"));
- return;
- }
resume_pay_with_response (pc,
- MHD_HTTP_OK,
- sign_success_response (pc));
+ http_status,
+ TMH_RESPONSE_make_error (ec,
+ msg));
}
@@ -613,6 +543,7 @@ pay_context_cleanup (struct TM_HandlerContext *hc)
GNUNET_CRYPTO_rsa_signature_free (dc->ub_sig.rsa_signature);
dc->ub_sig.rsa_signature = NULL;
}
+ GNUNET_free_non_null (dc->exchange_url);
}
GNUNET_free_non_null (pc->dc);
if (NULL != pc->fo)
@@ -625,11 +556,6 @@ pay_context_cleanup (struct TM_HandlerContext *hc)
MHD_destroy_response (pc->response);
pc->response = NULL;
}
- if (NULL != pc->chosen_exchange)
- {
- GNUNET_free (pc->chosen_exchange);
- pc->chosen_exchange = NULL;
- }
if (NULL != pc->contract_terms)
{
json_decref (pc->contract_terms);
@@ -643,99 +569,36 @@ pay_context_cleanup (struct TM_HandlerContext *hc)
/**
- * Function called with the result of our exchange lookup.
+ * Check whether the amount paid is sufficient to cover
+ * the contract.
*
- * @param cls the `struct PayContext`
- * @param mh NULL if exchange was not found to be acceptable
- * @param wire_fee current applicable fee for dealing with @a mh, NULL if not available
- * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config
+ * @param pc payment context to check
+ * @return taler error code, #TALER_EC_NONE if amount is sufficient
*/
-static void
-process_pay_with_exchange (void *cls,
- struct TALER_EXCHANGE_Handle *mh,
- const struct TALER_Amount *wire_fee,
- int exchange_trusted)
+static enum TALER_ErrorCode
+check_payment_sufficient (struct PayContext *pc)
{
- struct PayContext *pc = cls;
struct TALER_Amount acc_fee;
struct TALER_Amount acc_amount;
struct TALER_Amount wire_fee_delta;
struct TALER_Amount wire_fee_customer_contribution;
- const struct TALER_EXCHANGE_Keys *keys;
- enum GNUNET_DB_QueryStatus qs;
- pc->fo = NULL;
- GNUNET_assert (GNUNET_YES == pc->suspended);
- if (NULL == mh)
- {
- /* The exchange on offer is not in the set of our (trusted)
- exchanges. Reject the payment. */
- GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_PRECONDITION_FAILED,
- TMH_RESPONSE_make_external_error (TALER_EC_PAY_EXCHANGE_REJECTED,
- "exchange not supported"));
- return;
- }
- pc->mh = mh;
- keys = TALER_EXCHANGE_get_keys (mh);
- if (NULL == keys)
- {
- GNUNET_break (0);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_EXCHANGE_KEYS_FAILURE,
- "no keys"));
- return;
- }
-
- /* Total up the fees and the value of the deposited coins! */
GNUNET_assert (0 != pc->coins_cnt);
for (unsigned int i=0;i<pc->coins_cnt;i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
- const struct TALER_EXCHANGE_DenomPublicKey *denom_details;
- denom_details = TALER_EXCHANGE_get_denomination_key (keys,
- &dc->denom);
- if (NULL == denom_details)
- {
- GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o, s:o}",
- "error", "denomination not found",
- "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND,
- "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key),
- "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh)));
- return;
- }
- if (GNUNET_OK !=
- TMH_AUDITORS_check_dk (mh,
- denom_details,
- exchange_trusted))
- {
- GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o}",
- "error", "invalid denomination",
- "code", (json_int_t) TALER_EC_PAY_DENOMINATION_KEY_AUDITOR_FAILURE,
- "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key)));
- return;
- }
- dc->deposit_fee = denom_details->fee_deposit;
- dc->refund_fee = denom_details->fee_refund;
+ GNUNET_assert (GNUNET_YES == dc->found_in_db);
if (0 == i)
{
- acc_fee = denom_details->fee_deposit;
+ acc_fee = dc->deposit_fee;
acc_amount = dc->amount_with_fee;
}
else
{
if ( (GNUNET_OK !=
TALER_amount_add (&acc_fee,
- &denom_details->fee_deposit,
+ &dc->deposit_fee,
&acc_fee)) ||
(GNUNET_OK !=
TALER_amount_add (&acc_amount,
@@ -744,11 +607,7 @@ process_pay_with_exchange (void *cls,
{
GNUNET_break_op (0);
/* Overflow in these amounts? Very strange. */
- resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_AMOUNT_OVERFLOW,
- "Overflow adding up amounts"));
- return;
+ return TALER_EC_PAY_AMOUNT_OVERFLOW;
}
}
if (1 ==
@@ -757,33 +616,22 @@ process_pay_with_exchange (void *cls,
{
GNUNET_break_op (0);
/* fee higher than residual coin value, makes no sense. */
- resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o, s:o}",
- "hint", "fee higher than coin value",
- "code", (json_int_t) TALER_EC_PAY_FEES_EXCEED_PAYMENT,
- "f" /* FIXME */, TALER_JSON_from_amount (&dc->amount_with_fee),
- "fee_deposit", TALER_JSON_from_amount (&denom_details->fee_deposit)));
- return;
+ return TALER_EC_PAY_FEES_EXCEED_PAYMENT;
}
}
/* Now compare exchange wire fee compared to what we are willing to pay */
if (GNUNET_YES !=
- TALER_amount_cmp_currency (wire_fee,
+ TALER_amount_cmp_currency (&pc->total_wire_fee,
&pc->max_wire_fee))
{
GNUNET_break (0);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_WIRE_FEE_CURRENCY_MISSMATCH,
- "wire_fee"));
- return;
+ return TALER_EC_PAY_WIRE_FEE_CURRENCY_MISSMATCH;
}
if (GNUNET_OK ==
TALER_amount_subtract (&wire_fee_delta,
- wire_fee,
+ &pc->total_wire_fee,
&pc->max_wire_fee))
{
/* Actual wire fee is indeed higher than our maximum, compute
@@ -795,7 +643,7 @@ process_pay_with_exchange (void *cls,
else
{
GNUNET_assert (GNUNET_OK ==
- TALER_amount_get_zero (wire_fee->currency,
+ TALER_amount_get_zero (pc->total_wire_fee.currency,
&wire_fee_customer_contribution));
}
@@ -820,11 +668,7 @@ process_pay_with_exchange (void *cls,
&pc->amount))
{
GNUNET_break (0);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_AMOUNT_OVERFLOW,
- "overflow"));
- return;
+ return TALER_EC_PAY_AMOUNT_OVERFLOW;
}
/* add wire fee contribution to the total */
if (GNUNET_OK ==
@@ -837,11 +681,7 @@ process_pay_with_exchange (void *cls,
&total_needed))
{
GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_METHOD_NOT_ACCEPTABLE,
- TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES,
- "insufficient funds (including excessive exchange fees to be covered by customer)"));
- return;
+ return TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES;
}
}
else
@@ -871,11 +711,7 @@ process_pay_with_exchange (void *cls,
&wire_fee_customer_contribution))
{
GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_METHOD_NOT_ACCEPTABLE,
- TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES,
- "insufficient funds (including excessive exchange fees to be covered by customer)"));
- return;
+ return TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES;
}
}
@@ -884,173 +720,310 @@ process_pay_with_exchange (void *cls,
&pc->amount))
{
GNUNET_break_op (0);
- resume_pay_with_response (pc,
- MHD_HTTP_METHOD_NOT_ACCEPTABLE,
- TMH_RESPONSE_make_external_error (TALER_EC_PAY_PAYMENT_INSUFFICIENT,
- "insufficient funds"));
- return;
+ return TALER_EC_PAY_PAYMENT_INSUFFICIENT;
}
}
-
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Exchange and fee structure OK. Initiating deposit operation for coins\n");
+ return TALER_EC_NONE;
+}
- if (GNUNET_OK != db->start (db->cls))
+/**
+ * Generate full error response based on the @a ec
+ *
+ * @param pc context for which to generate the error
+ * @param ec error code identifying the issue
+ */
+static void
+generate_error_response (struct PayContext *pc,
+ enum TALER_ErrorCode ec)
+{
+ switch (ec)
{
+ case TALER_EC_PAY_AMOUNT_OVERFLOW:
+ resume_pay_with_error (pc,
+ MHD_HTTP_BAD_REQUEST,
+ ec,
+ "Overflow adding up amounts");
+ break;
+ case TALER_EC_PAY_FEES_EXCEED_PAYMENT:
+ resume_pay_with_error (pc,
+ MHD_HTTP_BAD_REQUEST,
+ ec,
+ "Deposit fees exceed coin's contribution");
+ break;
+ case TALER_EC_PAY_PAYMENT_INSUFFICIENT_DUE_TO_FEES:
+ resume_pay_with_error (pc,
+ MHD_HTTP_METHOD_NOT_ACCEPTABLE,
+ ec,
+ "insufficient funds (including excessive exchange fees to be covered by customer)");
+ break;
+ case TALER_EC_PAY_PAYMENT_INSUFFICIENT:
+ resume_pay_with_error (pc,
+ MHD_HTTP_METHOD_NOT_ACCEPTABLE,
+ ec,
+ "insufficient funds");
+ break;
+ case TALER_EC_PAY_WIRE_FEE_CURRENCY_MISSMATCH:
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ ec,
+ "wire_fee currency does not match");
+ break;
+ default:
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ ec,
+ "unexpected error code");
GNUNET_break (0);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_json_pack ("{s:s, s:I}",
- "hint", "Merchant database error: could not start transaction",
- "code", (json_int_t) TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR));
- return;
+ break;
}
+}
- /* Check if transaction is already known, if not store it. */
+
+/**
+ * Find the exchange we need to talk to for the next
+ * pending deposit permission.
+ *
+ * @param pc payment context we are processing
+ */
+static void
+find_next_exchange (struct PayContext *pc);
+
+
+/**
+ * Begin of the DB transaction. If required (from
+ * soft/serialization errors), the transaction can be
+ * restarted here.
+ *
+ * @param pc payment context to transact
+ */
+static void
+begin_transaction (struct PayContext *pc);
+
+
+/**
+ * Callback to handle a deposit permission's response.
+ *
+ * @param cls a `struct DepositConfirmation` (i.e. a pointer
+ * into the global array of confirmations and an index for this call
+ * in that array). That way, the last executed callback can detect
+ * that no other confirmations are on the way, and can pack a response
+ * for the wallet
+ * @param http_status HTTP response code, #MHD_HTTP_OK
+ * (200) for successful deposit; 0 if the exchange's reply is bogus (fails
+ * to follow the protocol)
+ * @param ec taler-specific error code, #TALER_EC_NONE on success
+ * @param sign_key which key did the exchange use to sign the @a proof
+ * @param proof the received JSON reply,
+ * should be kept as proof (and, in case of errors, be forwarded to
+ * the customer)
+ */
+static void
+deposit_cb (void *cls,
+ unsigned int http_status,
+ enum TALER_ErrorCode ec,
+ const struct TALER_ExchangePublicKeyP *sign_key,
+ const json_t *proof)
+{
+ struct DepositConfirmation *dc = cls;
+ struct PayContext *pc = dc->pc;
+ enum GNUNET_DB_QueryStatus qs;
+
+ dc->dh = NULL;
+ GNUNET_assert (GNUNET_YES == pc->suspended);
+ pc->pending_at_ce--;
+ if (MHD_HTTP_OK != http_status)
{
- struct GNUNET_HashCode h_xwire;
- struct GNUNET_TIME_Absolute xtimestamp;
- struct GNUNET_TIME_Absolute xrefund;
- struct TALER_Amount xtotal_amount;
-
- qs = db->find_transaction (db->cls,
- &pc->h_contract_terms,
- &pc->mi->pubkey,
- &h_xwire,
- &xtimestamp,
- &xrefund,
- &xtotal_amount);
- if (0 > qs)
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Deposit operation failed with HTTP code %u\n",
+ http_status);
+ /* Transaction failed; stop all other ongoing deposits */
+ abort_deposit (pc);
+ db->rollback (db->cls);
+
+ if (NULL == proof)
{
- /* single, read-only SQL statements should never cause
- serialization problems */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
- /* Always report on hard error as well to enable diagnostics */
- GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
- /* FIXME: factor common logic of these calls into a function! */
+ /* We can't do anything meaningful here, the exchange did something wrong */
resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR,
- "hint", "Merchant database error"));
- return;
+ MHD_HTTP_SERVICE_UNAVAILABLE,
+ TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:I, s:s}",
+ "error", "exchange failed",
+ "code", (json_int_t) TALER_EC_PAY_EXCHANGE_FAILED,
+ "exchange-code", (json_int_t) ec,
+ "exchange-http-status", (json_int_t) http_status,
+ "hint", "The exchange provided an unexpected response"));
}
- if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == pc->transaction_exists) &&
- ( (0 != memcmp (&h_xwire,
- &pc->mi->h_wire,
- sizeof (struct GNUNET_HashCode))) ||
- (xtimestamp.abs_value_us != pc->timestamp.abs_value_us) ||
- (xrefund.abs_value_us != pc->refund_deadline.abs_value_us) ||
- (0 != TALER_amount_cmp (&xtotal_amount,
- &pc->amount) ) ) )
+ else
{
- GNUNET_break (0);
- /* FIXME: factor common logic of these calls into a function! */
+ /* Forward error, adding the "coin_pub" for which the
+ error was being generated */
+ json_t *eproof;
+
+ eproof = json_copy ((json_t *) proof);
+ json_object_set_new (eproof,
+ "coin_pub",
+ GNUNET_JSON_from_data_auto (&dc->coin_pub));
resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_DB_TRANSACTION_ID_CONFLICT,
- "hint", "Transaction ID reused with different transaction details"));
- return;
+ http_status,
+ TMH_RESPONSE_make_json (eproof));
+ json_decref (eproof);
}
+ return;
}
-
- if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == pc->transaction_exists)
+ /* store result to DB */
+ GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
+ "Storing successful payment for h_contract_terms `%s' and merchant `%s'\n",
+ GNUNET_h2s (&pc->h_contract_terms),
+ TALER_B2S (&pc->mi->pubkey));
+ /* NOTE: not run in any transaction block, simply as a
+ transaction by itself! */
+ qs = db->store_deposit (db->cls,
+ &pc->h_contract_terms,
+ &pc->mi->pubkey,
+ &dc->coin_pub,
+ dc->exchange_url,
+ &dc->amount_with_fee,
+ &dc->deposit_fee,
+ &dc->refund_fee,
+ sign_key,
+ proof);
+ if (0 > qs)
{
- struct GNUNET_TIME_Absolute now;
- enum GNUNET_DB_QueryStatus qs_st;
-
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Dealing with new transaction `%s'\n",
- GNUNET_h2s (&pc->h_contract_terms));
-
- now = GNUNET_TIME_absolute_get ();
- if (now.abs_value_us > pc->pay_deadline.abs_value_us)
+ /* Special report if retries insufficient */
+ abort_deposit (pc);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
{
- /* Time expired, we don't accept this payment now! */
- const char *pd_str;
-
- pd_str = GNUNET_STRINGS_absolute_time_to_string (pc->pay_deadline);
- GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
- "Attempt to pay coins for expired contract. Deadline: `%s'\n",
- pd_str);
- resume_pay_with_response (pc,
- MHD_HTTP_BAD_REQUEST,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_OFFER_EXPIRED,
- "hint", "The time to pay for this contract has expired."));
+ begin_transaction (pc);
return;
}
+ /* Always report on hard error as well to enable diagnostics */
+ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
+ /* Forward error including 'proof' for the body */
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_DB_STORE_PAY_ERROR,
+ "Merchant database error");
+ return;
+ }
+ dc->found_in_db = GNUNET_YES;
+ pc->pending--;
- qs_st = db->store_transaction (db->cls,
- &pc->h_contract_terms,
- &pc->mi->pubkey,
- &pc->mi->h_wire,
- pc->timestamp,
- pc->refund_deadline,
- &pc->amount);
- /* Only retry if SOFT error occurred. Exit in case of OK or HARD failure */
- if (GNUNET_DB_STATUS_SOFT_ERROR == qs_st)
- {
- db->rollback (db->cls);
- GNUNET_break (0); // FIXME: implement proper retries!
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
- "hint", "Soft merchant database error: retries not implemented"));
- return;
- }
- /* Exit in case of HARD failure */
- if (GNUNET_DB_STATUS_HARD_ERROR == qs_st)
- {
- GNUNET_break (0);
- db->rollback (db->cls);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
- "hint", "Merchant database error: hard error while storing transaction"));
- }
+ if (0 != pc->pending_at_ce)
+ return; /* still more to do with current exchange */
+ find_next_exchange (pc);
+}
+
+
+/**
+ * Function called with the result of our exchange lookup.
+ *
+ * @param cls the `struct PayContext`
+ * @param mh NULL if exchange was not found to be acceptable
+ * @param wire_fee current applicable fee for dealing with @a mh, NULL if not available
+ * @param exchange_trusted #GNUNET_YES if this exchange is trusted by config
+ */
+static void
+process_pay_with_exchange (void *cls,
+ struct TALER_EXCHANGE_Handle *mh,
+ const struct TALER_Amount *wire_fee,
+ int exchange_trusted)
+{
+ struct PayContext *pc = cls;
+ const struct TALER_EXCHANGE_Keys *keys;
+
+ pc->fo = NULL;
+ GNUNET_assert (GNUNET_YES == pc->suspended);
+ if (NULL == mh)
+ {
+ /* The exchange on offer is not in the set of our (trusted)
+ exchanges. Reject the payment. */
+ GNUNET_break_op (0);
+ resume_pay_with_error (pc,
+ MHD_HTTP_PRECONDITION_FAILED,
+ TALER_EC_PAY_EXCHANGE_REJECTED,
+ "exchange not supported");
+ return;
+ }
+ if (GNUNET_OK !=
+ TALER_amount_add (&pc->total_wire_fee,
+ &pc->total_wire_fee,
+ wire_fee))
+ {
+ GNUNET_break_op (0);
+ resume_pay_with_error (pc,
+ MHD_HTTP_PRECONDITION_FAILED,
+ TALER_EC_PAY_EXCHANGE_REJECTED,
+ "exchange charges incompatible wire fee");
+ return;
+ }
+ pc->mh = mh;
+ keys = TALER_EXCHANGE_get_keys (mh);
+ if (NULL == keys)
+ {
+ GNUNET_break (0);
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_EXCHANGE_KEYS_FAILURE,
+ "no keys");
+ return;
+ }
- /**
- * Break if we couldn't modify one, and only one line; this
- * includes hard errors.
- */
- if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs_st)
- {
- GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
- "Unexpected query status %d while storing /pay transaction!\n",
- (int) qs_st);
- resume_pay_with_response (pc,
- MHD_HTTP_INTERNAL_SERVER_ERROR,
- TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
- "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
- "hint", "Merchant database error: failed to store transaction"));
- return;
- }
- } /* end of if (GNUNET_NO == pc->transaction_exists) */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Found transaction data for proposal `%s' of merchant `%s', initiating deposits\n",
GNUNET_h2s (&pc->h_contract_terms),
TALER_B2S (&pc->mi->pubkey));
-
- /* Initiate /deposit operation for all coins */
+ /* Initiate /deposit operation for all coins of
+ the current exchange (!) */
+ GNUNET_assert (0 == pc->pending_at_ce);
for (unsigned int i=0;i<pc->coins_cnt;i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
+ const struct TALER_EXCHANGE_DenomPublicKey *denom_details;
if (GNUNET_YES == dc->found_in_db)
continue;
+ if (0 != strcmp (dc->exchange_url,
+ pc->current_exchange))
+ continue;
+ denom_details = TALER_EXCHANGE_get_denomination_key (keys,
+ &dc->denom);
+ if (NULL == denom_details)
+ {
+ GNUNET_break_op (0);
+ resume_pay_with_response (pc,
+ MHD_HTTP_BAD_REQUEST,
+ TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o, s:o}",
+ "error", "denomination not found",
+ "code", TALER_EC_PAY_DENOMINATION_KEY_NOT_FOUND,
+ "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key),
+ "exchange_keys", TALER_EXCHANGE_get_keys_raw (mh)));
+ return;
+ }
+ if (GNUNET_OK !=
+ TMH_AUDITORS_check_dk (mh,
+ denom_details,
+ exchange_trusted))
+ {
+ GNUNET_break_op (0);
+ resume_pay_with_response (pc,
+ MHD_HTTP_BAD_REQUEST,
+ TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:o}",
+ "error", "invalid denomination",
+ "code", (json_int_t) TALER_EC_PAY_DENOMINATION_KEY_AUDITOR_FAILURE,
+ "denom_pub", GNUNET_JSON_from_rsa_public_key (dc->denom.rsa_public_key)));
+ return;
+ }
+ dc->deposit_fee = denom_details->fee_deposit;
+ dc->refund_fee = denom_details->fee_refund;
+ dc->wire_fee = *wire_fee;
+
GNUNET_assert (NULL != pc->mi->j_wire);
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Timing for this payment, wire_deadline: %llu, refund_deadline: %llu\n",
(unsigned long long) pc->wire_transfer_deadline.abs_value_us,
(unsigned long long) pc->refund_deadline.abs_value_us);
-
dc->dh = TALER_EXCHANGE_deposit (mh,
&dc->amount_with_fee,
pc->wire_transfer_deadline,
@@ -1070,7 +1043,6 @@ process_pay_with_exchange (void *cls,
/* Signature was invalid. If the exchange was unavailable,
* we'd get that information in the callback. */
GNUNET_break_op (0);
- db->rollback (db->cls);
resume_pay_with_response (pc,
MHD_HTTP_UNAUTHORIZED,
TMH_RESPONSE_make_json_pack ("{s:s, s:I, s:i}",
@@ -1080,7 +1052,47 @@ process_pay_with_exchange (void *cls,
"coin_idx", i));
return;
}
+ pc->pending_at_ce++;
+ }
+}
+
+
+/**
+ * Find the exchange we need to talk to for the next
+ * pending deposit permission.
+ *
+ * @param pc payment context we are processing
+ */
+static void
+find_next_exchange (struct PayContext *pc)
+{
+ for (unsigned int i=0;i<pc->coins_cnt;i++)
+ {
+ struct DepositConfirmation *dc = &pc->dc[i];
+
+ if (GNUNET_YES != dc->found_in_db)
+ {
+ pc->current_exchange = dc->exchange_url;
+ pc->fo = TMH_EXCHANGES_find_exchange (pc->current_exchange,
+ pc->mi->wire_method,
+ &process_pay_with_exchange,
+ pc);
+ if (NULL == pc->fo)
+ {
+ GNUNET_break (0);
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_EXCHANGE_FAILED,
+ "Failed to lookup exchange by URL");
+ return;
+ }
+ return;
+ }
}
+ pc->current_exchange = NULL;
+ /* We are done with all the HTTP requests, go back and try
+ the 'big' database transaction! (It should work now!) */
+ begin_transaction (pc);
}
@@ -1103,10 +1115,10 @@ handle_pay_timeout (void *cls)
TMH_EXCHANGES_find_exchange_cancel (pc->fo);
pc->fo = NULL;
}
- resume_pay_with_response (pc,
- MHD_HTTP_SERVICE_UNAVAILABLE,
- TMH_RESPONSE_make_internal_error (TALER_EC_PAY_EXCHANGE_TIMEOUT,
- "exchange not reachable"));
+ resume_pay_with_error (pc,
+ MHD_HTTP_SERVICE_UNAVAILABLE,
+ TALER_EC_PAY_EXCHANGE_TIMEOUT,
+ "exchange not reachable");
}
@@ -1130,6 +1142,7 @@ check_coin_paid (void *cls,
const struct TALER_Amount *amount_with_fee,
const struct TALER_Amount *deposit_fee,
const struct TALER_Amount *refund_fee,
+ // FIXME: also store AND fetch wire fee!
const json_t *exchange_proof)
{
struct PayContext *pc = cls;
@@ -1144,6 +1157,9 @@ check_coin_paid (void *cls,
for (unsigned int i=0;i<pc->coins_cnt;i++)
{
struct DepositConfirmation *dc = &pc->dc[i];
+
+ if (GNUNET_YES == dc->found_in_db)
+ continue; /* processed earlier */
/* Get matching coin from results*/
if ( (0 != memcmp (coin_pub,
&dc->coin_pub,
@@ -1151,16 +1167,35 @@ check_coin_paid (void *cls,
(0 != TALER_amount_cmp (amount_with_fee,
&dc->amount_with_fee)) )
continue;
-
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Coin (%s) already found in our DB.\n",
- TALER_b2s (coin_pub, sizeof (*coin_pub)));
-
+ TALER_b2s (coin_pub,
+ sizeof (*coin_pub)));
+ if (GNUNET_OK !=
+ TALER_amount_add (&pc->total_paid,
+ &pc->total_paid,
+ amount_with_fee))
+ {
+ /* We accepted this coin for payment on this contract before,
+ and now we can't even add the amount!? */
+ GNUNET_break (0);
+ continue;
+ }
+ if (GNUNET_OK !=
+ TALER_amount_add (&pc->total_fees_paid,
+ &pc->total_fees_paid,
+ deposit_fee))
+ {
+ /* We accepted this coin for payment on this contract before,
+ and now we can't even add the amount!? */
+ GNUNET_break (0);
+ continue;
+ }
+ dc->deposit_fee = *deposit_fee;
+ dc->refund_fee = *refund_fee;
+ // dc->wire_fee = *wire_fee; // TBD...
+ dc->amount_with_fee = *amount_with_fee;
dc->found_in_db = GNUNET_YES;
- /**
- * What happens if a (mad) wallet sends new coins on a
- * contract that it already paid for?
- */
pc->pending--;
}
}
@@ -1187,15 +1222,16 @@ parse_pay (struct MHD_Connection *connection,
json_t *coin;
json_t *merchant;
unsigned int coins_index;
- const char *chosen_exchange;
const char *order_id;
struct TALER_MerchantPublicKeyP merchant_pub;
int res;
struct GNUNET_JSON_Specification spec[] = {
- GNUNET_JSON_spec_json ("coins", &coins),
- GNUNET_JSON_spec_string ("exchange", &chosen_exchange),
- GNUNET_JSON_spec_string ("order_id", &order_id),
- GNUNET_JSON_spec_fixed_auto ("merchant_pub", &merchant_pub),
+ GNUNET_JSON_spec_json ("coins",
+ &coins),
+ GNUNET_JSON_spec_string ("order_id",
+ &order_id),
+ GNUNET_JSON_spec_fixed_auto ("merchant_pub",
+ &merchant_pub),
GNUNET_JSON_spec_end()
};
enum GNUNET_DB_QueryStatus qs;
@@ -1295,7 +1331,6 @@ parse_pay (struct MHD_Connection *connection,
GNUNET_STRINGS_data_to_string_alloc (&pc->mi->pubkey,
sizeof (pc->mi->pubkey)));
- pc->chosen_exchange = GNUNET_strdup (chosen_exchange);
{
struct GNUNET_JSON_Specification espec[] = {
GNUNET_JSON_spec_absolute_time ("refund_deadline",
@@ -1379,6 +1414,10 @@ parse_pay (struct MHD_Connection *connection,
TALER_amount_get_zero (pc->max_fee.currency,
&pc->max_wire_fee));
}
+ /* Initialize wire fee total */
+ GNUNET_assert (GNUNET_OK ==
+ TALER_amount_get_zero (pc->max_fee.currency,
+ &pc->total_wire_fee));
if (NULL != json_object_get (pc->contract_terms,
"wire_fee_amortization"))
{
@@ -1420,12 +1459,20 @@ parse_pay (struct MHD_Connection *connection,
json_array_foreach (coins, coins_index, coin)
{
struct DepositConfirmation *dc = &pc->dc[coins_index];
+ const char *exchange_url;
struct GNUNET_JSON_Specification spec[] = {
- TALER_JSON_spec_denomination_public_key ("denom_pub", &dc->denom),
- TALER_JSON_spec_amount ("f" /* FIXME */, &dc->amount_with_fee),
- GNUNET_JSON_spec_fixed_auto ("coin_pub", &dc->coin_pub),
- TALER_JSON_spec_denomination_signature ("ub_sig", &dc->ub_sig),
- GNUNET_JSON_spec_fixed_auto ("coin_sig", &dc->coin_sig),
+ TALER_JSON_spec_denomination_public_key ("denom_pub",
+ &dc->denom),
+ TALER_JSON_spec_amount ("contribution",
+ &dc->amount_with_fee),
+ GNUNET_JSON_spec_string ("exchange_url",
+ &exchange_url),
+ GNUNET_JSON_spec_fixed_auto ("coin_pub",
+ &dc->coin_pub),
+ TALER_JSON_spec_denomination_signature ("ub_sig",
+ &dc->ub_sig),
+ GNUNET_JSON_spec_fixed_auto ("coin_sig",
+ &dc->coin_sig),
GNUNET_JSON_spec_end()
};
@@ -1436,9 +1483,9 @@ parse_pay (struct MHD_Connection *connection,
{
GNUNET_JSON_parse_free (spec);
GNUNET_break_op (0);
- return (GNUNET_NO == res) ? MHD_YES : MHD_NO;
+ return res;
}
-
+ dc->exchange_url = GNUNET_strdup (exchange_url);
dc->index = coins_index;
dc->pc = pc;
}
@@ -1449,29 +1496,80 @@ parse_pay (struct MHD_Connection *connection,
/**
- * Process a payment for a proposal.
+ * 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 connection HTTP connection we are receiving payment on
- * @param root JSON upload with payment data
- * @param pc context we use to handle the payment
- * @return value to return to MHD (#MHD_NO to drop connection,
- * #MHD_YES to keep handling it)
+ * @param cls closure with a `struct PayContext`
+ * @param coin_pub public coin from which the refund comes from
+ * @param rtransaction_id identificator of the refund
+ * @param reason human-readable explaination of the refund
+ * @param refund_amount refund amount which is being taken from coin_pub
+ * @param refund_fee cost of this refund operation
*/
-static int
-handler_pay_json (struct MHD_Connection *connection,
- const json_t *root,
- struct PayContext *pc)
+static void
+check_coin_refunded (void *cls,
+ const struct TALER_CoinSpendPublicKeyP *coin_pub,
+ uint64_t rtransaction_id,
+ const char *reason,
+ const struct TALER_Amount *refund_amount,
+ const struct TALER_Amount *refund_fee)
{
- int ret;
+ struct PayContext *pc = cls;
+
+ /* FIXME: to be implemented (#5158) */
+ (void) pc;
+}
+
+
+/**
+ * Begin of the DB transaction. If required (from
+ * soft/serialization errors), the transaction can be
+ * restarted here.
+ *
+ * @param pc payment context to transact
+ */
+static void
+begin_transaction (struct PayContext *pc)
+{
enum GNUNET_DB_QueryStatus qs;
- ret = parse_pay (connection,
- root,
- pc);
- if (GNUNET_OK != ret)
- return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
+ /* Avoid re-trying transactions on soft errors forever! */
+ if (pc->retry_counter++ > MAX_RETRIES)
+ {
+ GNUNET_break (0);
+ resume_pay_with_response (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
+ "hint", "Soft merchant database error: retry counter exceeded"));
+ return;
+ }
+
+ GNUNET_assert (GNUNET_YES == pc->suspended);
- /* Check if this payment attempt has already succeeded */
+ /* First, try to see if we have all we need already done */
+ if (GNUNET_OK != db->start (db->cls))
+ {
+ GNUNET_break (0);
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR,
+ "Merchant database error (could not start transaction)");
+ return;
+ }
+ GNUNET_break (GNUNET_OK ==
+ TALER_amount_get_zero (pc->amount.currency,
+ &pc->total_paid));
+ GNUNET_break (GNUNET_OK ==
+ TALER_amount_get_zero (pc->amount.currency,
+ &pc->total_fees_paid));
+ GNUNET_break (GNUNET_OK ==
+ TALER_amount_get_zero (pc->amount.currency,
+ &pc->total_refunded));
+
+ /* Check if some of these coins already succeeded */
qs = db->find_payments (db->cls,
&pc->h_contract_terms,
&pc->mi->pubkey,
@@ -1479,50 +1577,269 @@ handler_pay_json (struct MHD_Connection *connection,
pc);
if (0 > qs)
{
- /* single, read-only SQL statements should never cause
- serialization problems */
- GNUNET_break (GNUNET_DB_STATUS_SOFT_ERROR != qs);
+ db->rollback (db->cls);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ begin_transaction (pc);
+ return;
+ }
/* Always report on hard error as well to enable diagnostics */
GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
- return TMH_RESPONSE_reply_internal_error (connection,
- TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR,
- "Merchant database error");
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR,
+ "Merchant database error");
+ return;
+ }
+
+ /* Check if we refunded some of the coins */
+ qs = db->get_refunds_from_contract_terms_hash (db->cls,
+ &pc->mi->pubkey,
+ &pc->h_contract_terms,
+ &check_coin_refunded,
+ pc);
+ if (0 > qs)
+ {
+ db->rollback (db->cls);
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ begin_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_PAY_DB_FETCH_TRANSACTION_ERROR,
+ "Merchant database error");
+ return;
}
+
+ /* FIXME: check if wallet is going for a refund,
+ (on aborted operation), or for a payment! #5158 */
+
+
+ /* Final termination case: all coins already known, just
+ generate ultimate outcome. */
if (0 == pc->pending)
{
- struct MHD_Response *resp;
- int ret;
+ enum TALER_ErrorCode ec;
+
+ ec = check_payment_sufficient (pc);
+ if (TALER_EC_NONE == ec)
+ {
+ /* Payment succeeded, commit! */
+ qs = db->mark_proposal_paid (db->cls,
+ &pc->h_contract_terms,
+ &pc->mi->pubkey);
+ if (0 <= qs)
+ qs = db->commit (db->cls);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ begin_transaction (pc);
+ return;
+ }
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR,
+ "Merchant database error: could not mark proposal as 'paid'");
+ return;
+ }
+ resume_pay_with_response (pc,
+ MHD_HTTP_OK,
+ sign_success_response (pc));
+ return;
+ }
+ generate_error_response (pc,
+ ec);
+ return;
+ }
+
+
+ /* Check if transaction is already known, if not store it. */
+ {
+ struct GNUNET_HashCode h_xwire;
+ struct GNUNET_TIME_Absolute xtimestamp;
+ struct GNUNET_TIME_Absolute xrefund;
+ struct TALER_Amount xtotal_amount;
+
+ qs = db->find_transaction (db->cls,
+ &pc->h_contract_terms,
+ &pc->mi->pubkey,
+ &h_xwire,
+ &xtimestamp,
+ &xrefund,
+ &xtotal_amount);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ db->rollback (db->cls);
+ begin_transaction (pc);
+ return;
+ }
+ /* Always report on hard error as well to enable diagnostics */
+ GNUNET_break (GNUNET_DB_STATUS_HARD_ERROR == qs);
+ db->rollback (db->cls);
+ resume_pay_with_response (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_DB_FETCH_TRANSACTION_ERROR,
+ "hint", "Merchant database error"));
+ return;
+ }
+ if ( (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT == qs) &&
+ ( (0 != memcmp (&h_xwire,
+ &pc->mi->h_wire,
+ sizeof (struct GNUNET_HashCode))) ||
+ (xtimestamp.abs_value_us != pc->timestamp.abs_value_us) ||
+ (xrefund.abs_value_us != pc->refund_deadline.abs_value_us) ||
+ (0 != TALER_amount_cmp (&xtotal_amount,
+ &pc->amount) ) ) )
+ {
+ GNUNET_break (0);
+ db->rollback (db->cls);
+ resume_pay_with_response (pc,
+ MHD_HTTP_BAD_REQUEST,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_DB_TRANSACTION_ID_CONFLICT,
+ "hint", "Transaction ID reused with different transaction details"));
+ return;
+ }
+ }
+
+ if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
+ {
+ struct GNUNET_TIME_Absolute now;
+ enum GNUNET_DB_QueryStatus qs_st;
- /* Payment succeeded in the past; take short cut
- and accept immediately */
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Payment succeeded in the past; taking short cut");
- resp = sign_success_response (pc);
- ret = MHD_queue_response (connection,
- MHD_HTTP_OK,
- resp);
- MHD_destroy_response (resp);
- return ret;
+ "Dealing with new transaction `%s'\n",
+ GNUNET_h2s (&pc->h_contract_terms));
+
+ now = GNUNET_TIME_absolute_get ();
+ if (now.abs_value_us > pc->pay_deadline.abs_value_us)
+ {
+ /* Time expired, we don't accept this payment now! */
+ const char *pd_str;
+
+ pd_str = GNUNET_STRINGS_absolute_time_to_string (pc->pay_deadline);
+ GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
+ "Attempt to pay coins for expired contract. Deadline: `%s'\n",
+ pd_str);
+ db->rollback (db->cls);
+ resume_pay_with_response (pc,
+ MHD_HTTP_BAD_REQUEST,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_OFFER_EXPIRED,
+ "hint", "The time to pay for this contract has expired."));
+ return;
+ }
+
+ qs_st = db->store_transaction (db->cls,
+ &pc->h_contract_terms,
+ &pc->mi->pubkey,
+ &pc->mi->h_wire,
+ pc->timestamp,
+ pc->refund_deadline,
+ &pc->amount);
+ /* Only retry if SOFT error occurred. Exit in case of OK or HARD failure */
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs_st)
+ {
+ db->rollback (db->cls);
+ begin_transaction (pc);
+ return;
+ }
+ /* Exit in case of HARD failure */
+ if (GNUNET_DB_STATUS_HARD_ERROR == qs_st)
+ {
+ GNUNET_break (0);
+ db->rollback (db->cls);
+ resume_pay_with_response (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
+ "hint", "Merchant database error: hard error while storing transaction"));
+ }
+
+ /**
+ * Break if we couldn't modify one, and only one line; this
+ * includes hard errors.
+ */
+ if (GNUNET_DB_STATUS_SUCCESS_ONE_RESULT != qs_st)
+ {
+ GNUNET_log (GNUNET_ERROR_TYPE_ERROR,
+ "Unexpected query status %d while storing /pay transaction!\n",
+ (int) qs_st);
+ db->rollback (db->cls);
+ resume_pay_with_response (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TMH_RESPONSE_make_json_pack ("{s:I, s:s}",
+ "code", (json_int_t) TALER_EC_PAY_DB_STORE_TRANSACTION_ERROR,
+ "hint", "Merchant database error: failed to store transaction"));
+ return;
+ }
+
+ qs = db->commit (db->cls);
+ if (0 > qs)
+ {
+ if (GNUNET_DB_STATUS_SOFT_ERROR == qs)
+ {
+ db->rollback (db->cls);
+ begin_transaction (pc);
+ return;
+ }
+ resume_pay_with_error (pc,
+ MHD_HTTP_INTERNAL_SERVER_ERROR,
+ TALER_EC_PAY_DB_STORE_PAYMENTS_ERROR,
+ "Merchant database error: could not commit");
+ return;
+ }
+ } /* end of if (GNUNET_NO == qs) */
+ else
+ {
+ /* transaction record already existed, we made no DB changes,
+ so we can just rollback */
+ db->rollback (db->cls);
}
+
+ /* Ok, we need to first go to the network.
+ Do that interaction in *tiny* transactions. */
+ find_next_exchange (pc);
+}
- MHD_suspend_connection (connection);
- pc->suspended = GNUNET_YES;
- /* Find the responsible exchange, this may take a while... */
- pc->fo = TMH_EXCHANGES_find_exchange (pc->chosen_exchange,
- pc->mi->wire_method,
- &process_pay_with_exchange,
- pc);
+/**
+ * Process a payment for a proposal.
+ *
+ * @param connection HTTP connection we are receiving payment on
+ * @param root JSON upload with payment data
+ * @param pc context we use to handle the payment
+ * @return value to return to MHD (#MHD_NO to drop connection,
+ * #MHD_YES to keep handling it)
+ */
+static int
+handler_pay_json (struct MHD_Connection *connection,
+ const json_t *root,
+ struct PayContext *pc)
+{
+ int ret;
- /* ... so we suspend connection until the last coin has been ack'd
- or until we have encountered a hard error. Eventually, we will
- resume the connection and send back a response using
- #resume_pay_with_response(). */
+ ret = parse_pay (connection,
+ root,
+ pc);
+ if (GNUNET_OK != ret)
+ return (GNUNET_NO == ret) ? MHD_YES : MHD_NO;
+ MHD_suspend_connection (connection);
+ pc->suspended = GNUNET_YES;
GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
"Suspending /pay handling while working with the exchange\n");
pc->timeout_task = GNUNET_SCHEDULER_add_delayed (PAY_TIMEOUT,
&handle_pay_timeout,
pc);
+ begin_transaction (pc);
return MHD_YES;
}
@@ -1587,13 +1904,7 @@ MH_handler_pay (struct TMH_RequestHandler *rh,
res ? "OK" : "FAILED");
return res;
}
- if (NULL != pc->chosen_exchange)
- {
- // FIXME: explain in comment why this could happen!
- GNUNET_log (GNUNET_ERROR_TYPE_DEBUG,
- "Shouldn't be here. Old MHD version?\n");
- return MHD_YES;
- }
+
res = TMH_PARSE_post_json (connection,
&pc->json_parse_context,
upload_data,